Skip to content
Source
Sign inSign up

threadLinks.test.ts

264 lines10.3 KB

1import { describe, expect, it, vi } from "vite-plus/test";
2import { EnvironmentId, ThreadId, type ScopedThreadRef } from "@t3tools/contracts";
3​
4import {
5 formatHostedThreadLink,
6 formatThreadLink,
7 guessThreadLinkRef,
8 hostedThreadLink,
9 parseThreadLinkHref,
10 relabelThreadLinks,
11 threadLinkCandidates,
12 threadLinkRefs,
13} from "./threadLinks.ts";
14​
15const ref = (environmentId: string, threadId = "chat-1") => ({
16 environmentId: EnvironmentId.make(environmentId),
17 threadId: ThreadId.make(threadId),
18});
19const v1 = (threadId: string) => ({ environmentId: null, threadId: ThreadId.make(threadId) });
20const local = EnvironmentId.make("local");
21const titlesIn =
22 (titles: Record<string, Record<string, string>>) =>
23 ({ environmentId, threadId }: ScopedThreadRef) =>
24 titles[environmentId]?.[threadId];
25​
26describe("thread link destinations", () => {
27 it("names a v1 link's thread in the message's environment, as written and then decoded", () => {
28 expect(threadLinkCandidates(v1("provider%3A1"), local)).toEqual([
29 ref("local", "provider%3A1"),
30 ref("local", "provider:1"),
31 ]);
32 });
33​
34 it("opens the unique matching chat in another environment for a v1 link", () => {
35 const destination = ref("remote");
36 expect(guessThreadLinkRef(v1("chat-1"), [ref("local", "other-chat"), destination])).toEqual(
37 destination,
38 );
39 });
40​
41 it("opens a v2 link only in its own environment", () => {
42 const there = ref("there", "a%3A1");
43 expect(threadLinkCandidates(there, local)).toEqual([there, ref("there", "a:1")]);
44 expect(guessThreadLinkRef(there, [ref("elsewhere", "a%3A1"), ref("elsewhere", "a:1")])).toBe(
45 null,
46 );
47 });
48​
49 it("leaves ambiguous or missing destinations unresolved", () => {
50 const link = v1("provider%3A1");
51 const literal = ref("remote", "provider%3A1");
52 const decoded = ref("remote", "provider:1");
53 expect(
54 guessThreadLinkRef(link, [literal, ref("other-remote", literal.threadId), decoded]),
55 ).toBeNull();
56 expect(guessThreadLinkRef(link, [decoded, ref("other-remote", decoded.threadId)])).toBeNull();
57 expect(guessThreadLinkRef(link, [ref("remote", "other-chat")])).toBeNull();
58 });
59});
60​
61describe("thread links", () => {
62 it("takes the ids verbatim, percent escapes included", () => {
63 expect(parseThreadLinkHref("t3-thread://v1/mcp:1234")).toEqual(v1("mcp:1234"));
64 expect(parseThreadLinkHref("t3-thread://v1/thread:delegated-task:mcp%3A1")).toEqual(
65 v1("thread:delegated-task:mcp%3A1"),
66 );
67 expect(parseThreadLinkHref("t3-thread://v2/env-1/thread:a/b%3A1")).toEqual(
68 ref("env-1", "thread:a/b%3A1"),
69 );
70 });
71​
72 it("rejects other links and an empty id", () => {
73 expect(parseThreadLinkHref("https://t3.codes")).toBeNull();
74 expect(parseThreadLinkHref("t3-thread://v1/")).toBeNull();
75 expect(parseThreadLinkHref("t3-thread://v1/ ")).toBeNull();
76 expect(parseThreadLinkHref("t3-thread://v2/env-1")).toBeNull();
77 expect(parseThreadLinkHref("t3-thread://v2/env-1/")).toBeNull();
78 expect(parseThreadLinkHref("t3-thread://v2//chat-1")).toBeNull();
79 });
80​
81 it("resolves a percent-encoded id when the id as written names no thread", () => {
82 const title = titlesIn({ local: { "thread:project:1": "Decoded", "provider%3A1": "Literal" } });
83 expect(
84 relabelThreadLinks(
85 "[a](t3-thread://v1/thread%3Aproject%3A1) [b](t3-thread://v1/provider%3A1)",
86 local,
87 title,
88 ),
89 ).toBe("[Decoded](t3-thread://v1/thread:project:1) [Literal](t3-thread://v1/provider%3A1)");
90 const untitled = titlesIn({ local: { "a%3A1": "", "a:1": "Other" } });
91 expect(relabelThreadLinks("[Kept](t3-thread://v1/a%3A1)", local, untitled)).toBe(
92 "[Kept](t3-thread://v1/a%3A1)",
93 );
94 });
95​
96 it("titles a v2 link from its own environment and writes it with that title", () => {
97 const title = titlesIn({ local: { t1: "Here" }, there: { t1: "There" } });
98 expect(
99 relabelThreadLinks(
100 "<t3-thread://v2/there/t1> and [old](t3-thread://v2/local/t1)",
101 local,
102 title,
103 ),
104 ).toBe("[There](t3-thread://v2/there/t1) and [Here](t3-thread://v2/local/t1)");
105 });
106​
107 it("reads an angle-bracketed destination as the labeled link, not an autolink", () => {
108 const title = titlesIn({ local: { t1: "Here" }, there: { t1: "There" } });
109 expect(
110 relabelThreadLinks(
111 "[old](<t3-thread://v2/there/t1>) and [older](<t3-thread://v2/local/t1>)",
112 local,
113 title,
114 ),
115 ).toBe("[There](t3-thread://v2/there/t1) and [Here](t3-thread://v2/local/t1)");
116 expect(
117 relabelThreadLinks("[Kept label](<t3-thread://v2/local/t1>)", local, () => undefined),
118 ).toBe("Kept label");
119 expect(threadLinkRefs("[a](<t3-thread://v2/there/t2>)", local)).toEqual([ref("there", "t2")]);
120 for (const threadId of ["mcp:launch:qa(1)", "mcp:launch:qa)"]) {
121 const thread = ref("there", threadId);
122 const link = formatThreadLink(thread, "Original");
123 expect(link).toBe(`[Original](<t3-thread://v2/there/${threadId}>)`);
124 expect(threadLinkRefs(link, local)).toEqual([thread]);
125 expect(relabelThreadLinks(link, local, () => "Renamed")).toBe(
126 `[Renamed](<t3-thread://v2/there/${threadId}>)`,
127 );
128 expect(relabelThreadLinks(link, local, () => undefined)).toBe("Original");
129 }
130 });
131​
132 it("turns a v2 link it cannot resolve into plain text, never the raw link", () => {
133 expect(
134 relabelThreadLinks(
135 "<t3-thread://v2/there/t1>, <t3-thread://v2/local/t1>, [Old title](t3-thread://v2/there/t1)",
136 local,
137 () => undefined,
138 ),
139 ).toBe("Thread on another machine, Thread no longer available, Old title");
140 expect(relabelThreadLinks("[Old](t3-thread://v1/t1)", local, () => undefined)).toBe(
141 "[Old](t3-thread://v1/t1)",
142 );
143 });
144​
145 it("leaves links inside code spans and fences as written", () => {
146 const markdown = [
147 "Live [old](t3-thread://v1/t1), literal `[old](t3-thread://v1/t1)`.",
148 "```md",
149 "<t3-thread://v2/local/t1>",
150 "```",
151 "After <t3-thread://v2/local/t1>",
152 ].join("\n");
153 expect(relabelThreadLinks(markdown, local, () => "New")).toBe(
154 [
155 "Live [New](t3-thread://v1/t1), literal `[old](t3-thread://v1/t1)`.",
156 "```md",
157 "<t3-thread://v2/local/t1>",
158 "```",
159 "After [New](t3-thread://v2/local/t1)",
160 ].join("\n"),
161 );
162 });
163​
164 it("lists the threads a message's links can name, outside code", () => {
165 expect(
166 threadLinkRefs(
167 "<t3-thread://v2/there/t1> [a](t3-thread://v1/t%3A2) `<t3-thread://v2/code/t3>`",
168 local,
169 ),
170 ).toEqual([ref("there", "t1"), ref("local", "t%3A2"), ref("local", "t:2")]);
171 });
172​
173 it("lists a message's thread links on runtimes whose matchAll omits named groups, such as Hermes", () => {
174 const matchAll = String.prototype.matchAll;
175 using _hermes = vi.spyOn(String.prototype, "matchAll").mockImplementation(function (
176 this: string,
177 regexp: RegExp,
178 ) {
179 return matchAll.call(this, regexp).map((match) => {
180 delete match.groups;
181 return match;
182 });
183 });
184 expect(threadLinkRefs("<t3-thread://v2/local/t1> [a](t3-thread://v2/there/t2)", local)).toEqual(
185 [ref("local", "t1"), ref("there", "t2")],
186 );
187 });
188​
189 it("writes a titleless autolink that opens that exact thread", () => {
190 const link = formatThreadLink(ref("env-1", "thread:delegated-task:mcp%3A1"));
191 expect(link).toBe("<t3-thread://v2/env-1/thread:delegated-task:mcp%3A1>");
192 expect(parseThreadLinkHref(link.slice(1, -1))).toEqual(
193 ref("env-1", "thread:delegated-task:mcp%3A1"),
194 );
195 });
196​
197 it("formats a label that would otherwise break the Markdown link", () => {
198 expect(formatThreadLink(ref("e", "t1"), "Fix [ci] \\ build")).toBe(
199 "[Fix ci build](t3-thread://v2/e/t1)",
200 );
201 expect(formatThreadLink(ref("e", "t1"), " ] ")).toBe("<t3-thread://v2/e/t1>");
202 });
203});
204​
205describe("hosted thread links", () => {
206 it("escapes every id character a sentence or path could claim, and reads it back", () => {
207 const thread = ref("env.1", "thread:delegated/mcp%3A1.");
208 const link = hostedThreadLink(thread);
209 expect(link).toBe("https://app.t3.codes/threads/env%2E1/thread%3Adelegated%2Fmcp%253A1%2E");
210 expect(parseThreadLinkHref(link)).toEqual(thread);
211 expect(hostedThreadLink(thread, "https://preview.t3.codes/")).toMatch(
212 /^https:\/\/preview\.t3\.codes\/threads\//,
213 );
214 });
215​
216 it("reads only a bare thread path on the hosted app", () => {
217 expect(parseThreadLinkHref("https://app.t3.codes/threads/env-1/thread-1")).toEqual(
218 ref("env-1", "thread-1"),
219 );
220 expect(parseThreadLinkHref("https://app.t3.codes/threads/env-1/thread-1?prompt=go")).toBeNull();
221 expect(parseThreadLinkHref("https://app.t3.codes/threads/env-1/thread-1/extra")).toBeNull();
222 expect(parseThreadLinkHref("https://app.t3.codes/threads/env-1")).toBeNull();
223 expect(parseThreadLinkHref("https://app.t3.codes/env-1/thread-1")).toBeNull();
224 expect(parseThreadLinkHref("https://evil.test/threads/env-1/thread-1")).toBeNull();
225 });
226​
227 it("writes a copied link a reader can follow, labeled when it has a title", () => {
228 expect(formatHostedThreadLink(ref("e", "t1"), "Fix [ci]")).toBe(
229 "[Fix ci](https://app.t3.codes/threads/e/t1)",
230 );
231 expect(formatHostedThreadLink(ref("e", "t1"), " ")).toBe("https://app.t3.codes/threads/e/t1");
232 });
233​
234 it("titles hosted links in every Markdown form and writes them as v2 links", () => {
235 const title = titlesIn({ there: { t1: "There" } });
236 expect(
237 relabelThreadLinks(
238 [
239 "See https://app.t3.codes/threads/there/t1.",
240 "[old](https://app.t3.codes/threads/there/t1)",
241 "<https://app.t3.codes/threads/there/t1>",
242 "`https://app.t3.codes/threads/there/t1`",
243 "https://app.t3.codes/threads/there/t1?prompt=go",
244 ].join("\n"),
245 local,
246 title,
247 ),
248 ).toBe(
249 [
250 "See [There](t3-thread://v2/there/t1).",
251 "[There](t3-thread://v2/there/t1)",
252 "[There](t3-thread://v2/there/t1)",
253 "`https://app.t3.codes/threads/there/t1`",
254 "https://app.t3.codes/threads/there/t1?prompt=go",
255 ].join("\n"),
256 );
257 expect(
258 relabelThreadLinks("https://app.t3.codes/threads/there/t1", local, () => undefined),
259 ).toBe("Thread on another machine");
260 expect(threadLinkRefs("Open https://app.t3.codes/threads/there/t%3A1", local)).toEqual([
261 ref("there", "t:1"),
262 ]);
263 });
264});