1import { describe, expect, it } from "vite-plus/test";
2
3import { formatThreadLink, parseThreadLinkHref, relabelThreadLinks } from "./threadLinks.ts";
4
5describe("thread links", () => {
6 it("takes the thread id verbatim, percent escapes included", () => {
7 expect(parseThreadLinkHref("t3-thread://v1/mcp:1234")).toBe("mcp:1234");
8 expect(parseThreadLinkHref("t3-thread://v1/thread:delegated-task:mcp%3A1")).toBe(
9 "thread:delegated-task:mcp%3A1",
10 );
11 });
12
13 it("rejects other links and an empty id", () => {
14 expect(parseThreadLinkHref("https://t3.codes")).toBeNull();
15 expect(parseThreadLinkHref("t3-thread://v1/")).toBeNull();
16 expect(parseThreadLinkHref("t3-thread://v1/ ")).toBeNull();
17 });
18
19 it("resolves a percent-encoded id when the id as written names no thread", () => {
20 const titles = new Map([
21 ["thread:project:1", "Decoded"],
22 ["provider%3A1", "Literal escape"],
23 ]);
24 expect(
25 relabelThreadLinks(
26 "[a](t3-thread://v1/thread%3Aproject%3A1) [b](t3-thread://v1/provider%3A1)",
27 (threadId) => titles.get(threadId),
28 ),
29 ).toBe(
30 "[Decoded](t3-thread://v1/thread:project:1) [Literal escape](t3-thread://v1/provider%3A1)",
31 );
32 // A thread with an empty title still exists, so its link is not redirected.
33 const untitled = new Map([
34 ["a%3A1", ""],
35 ["a:1", "Other"],
36 ]);
37 expect(
38 relabelThreadLinks("[Kept](t3-thread://v1/a%3A1)", (threadId) => untitled.get(threadId)),
39 ).toBe("[Kept](t3-thread://v1/a%3A1)");
40 });
41
42 it("leaves links inside code spans and fences as written", () => {
43 const markdown = [
44 "Live [old](t3-thread://v1/t1), literal `[old](t3-thread://v1/t1)`.",
45 "```md",
46 "[old](t3-thread://v1/t1)",
47 "```",
48 "After [old](t3-thread://v1/t1)",
49 ].join("\n");
50 expect(relabelThreadLinks(markdown, () => "New")).toBe(
51 [
52 "Live [New](t3-thread://v1/t1), literal `[old](t3-thread://v1/t1)`.",
53 "```md",
54 "[old](t3-thread://v1/t1)",
55 "```",
56 "After [New](t3-thread://v1/t1)",
57 ].join("\n"),
58 );
59 });
60
61 it("formats a label that would otherwise break the Markdown link", () => {
62 expect(formatThreadLink("t1", "Fix [ci] \\ build")).toBe("[Fix ci build](t3-thread://v1/t1)");
63 expect(formatThreadLink("t1", " ] ")).toBe("[t1](t3-thread://v1/t1)");
64 });
65
66 it("relabels links with the current title and leaves unknown threads alone", () => {
67 const titles = new Map([["renamed", "Fix [the] build\nnow"]]);
68 expect(
69 relabelThreadLinks(
70 "See [Old name](t3-thread://v1/renamed) and [Gone](t3-thread://v1/deleted).",
71 (threadId) => titles.get(threadId),
72 ),
73 ).toBe("See [Fix the build now](t3-thread://v1/renamed) and [Gone](t3-thread://v1/deleted).");
74 });
75});