Open History to see the last commit
1import { EnvironmentId, ThreadId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 buildNewThreadLink,
6 parseNewThreadLink,
7 parseThreadLinkPrompt,
8 promptLinkAppPath,
9 promptLinkDeepLink,
10 sanitizeLinkedPrompt,
11 withoutPromptLinkParams,
12} from "./promptLinks.ts";
13import { hostedThreadLink } from "./threadLinks.ts";
14
15describe("sanitizeLinkedPrompt", () => {
16 it("removes characters a reader cannot see and counts them", () => {
17 const tagged = "rate me" + String.fromCodePoint(0xe0041, 0xe0042);
18 expect(sanitizeLinkedPrompt(`a\u200Bb\u202Ec\uFEFF${tagged}`)).toEqual({
19 text: "abcrate me",
20 hiddenCharacters: 5,
21 });
22 expect(sanitizeLinkedPrompt("a\u2028b\u2029c")).toEqual({
23 text: "a\nb\nc",
24 hiddenCharacters: 0,
25 });
26 });
27
28 it("keeps emoji sequences intact", () => {
29 const family = "\u{1F468}\u200D\u{1F469}\u200D\u{1F467}";
30 const heart = "\u2764\uFE0F";
31 expect(sanitizeLinkedPrompt(`${family} ${heart}`)).toEqual({
32 text: `${family} ${heart}`,
33 hiddenCharacters: 0,
34 });
35 expect(sanitizeLinkedPrompt("a\u200Db").hiddenCharacters).toBe(1);
36 expect(sanitizeLinkedPrompt("\u{1F468}\u200D").hiddenCharacters).toBe(1);
37 expect(sanitizeLinkedPrompt("\u{20000}\u200D\u{20001}").hiddenCharacters).toBe(1);
38 });
39});
40
41describe("parseNewThreadLink", () => {
42 it("reads a shared prompt with its optional repository and model", () => {
43 const result = parseNewThreadLink(
44 "?prompt=Rate+me&repo=https://github.com/pingdotgg/t3code.git&provider=Codex&model=gpt-5",
45 );
46 expect(result).toEqual({
47 ok: true,
48 value: {
49 kind: "new",
50 prompt: { text: "Rate me", hiddenCharacters: 0 },
51 repository: { owner: "pingdotgg", name: "t3code" },
52 provider: "codex",
53 model: "gpt-5",
54 },
55 });
56 });
57
58 it("reads a pull request link, whose prompt is optional", () => {
59 for (const pr of [
60 "pingdotgg/t3code/42",
61 "pingdotgg/t3code%2342",
62 "https://github.com/pingdotgg/t3code/pull/42/files",
63 ]) {
64 expect(parseNewThreadLink(`?pr=${pr}`)).toEqual({
65 ok: true,
66 value: {
67 kind: "pull-request",
68 pullRequest: { owner: "pingdotgg", name: "t3code", number: 42 },
69 prompt: null,
70 },
71 });
72 }
73 });
74
75 it("refuses links it cannot open safely", () => {
76 expect(parseNewThreadLink("?repo=a/b").ok).toBe(false);
77 expect(parseNewThreadLink("?prompt=%E2%80%8B").ok).toBe(false);
78 expect(parseThreadLinkPrompt("?prompt=%E2%80%8B").ok).toBe(false);
79 expect(parseNewThreadLink("?prompt=x&repo=../etc").ok).toBe(false);
80 expect(parseNewThreadLink(`?prompt=${"x".repeat(8_001)}`).ok).toBe(false);
81 });
82});
83
84describe("thread links", () => {
85 it("reads the prompt and leaves other parameters in place", () => {
86 expect(parseThreadLinkPrompt("?prompt=Fix+it")).toEqual({
87 ok: true,
88 value: { text: "Fix it", hiddenCharacters: 0 },
89 });
90 expect(parseThreadLinkPrompt("?panel=diff")).toEqual({ ok: true, value: null });
91 expect(withoutPromptLinkParams("?prompt=x&panel=diff")).toBe("?panel=diff");
92 });
93});
94
95describe("link round trips", () => {
96 it("builds a link that parses back to the same prompt", () => {
97 const link = buildNewThreadLink({
98 prompt: "Swear count & rating?",
99 repository: { owner: "pingdotgg", name: "t3code" },
100 provider: "claudeAgent",
101 model: "opus",
102 });
103 const path = promptLinkAppPath(link);
104 expect(path).toBe(new URL(link).pathname + new URL(link).search);
105 const parsed = parseNewThreadLink(new URL(link).search);
106 expect(parsed.ok && parsed.value.kind === "new" && parsed.value.prompt.text).toBe(
107 "Swear count & rating?",
108 );
109 });
110
111 it("opens a copied thread link in the app at that thread", () => {
112 const link = hostedThreadLink({
113 environmentId: EnvironmentId.make("env-1"),
114 threadId: ThreadId.make("mcp:1"),
115 });
116 const path = promptLinkAppPath(link);
117 expect(path).toBe("/env-1/mcp%3A1");
118 const deepLink = promptLinkDeepLink(path!);
119 expect(deepLink).toBe("t3code://threads/env-1/mcp%3A1");
120 expect(promptLinkAppPath(deepLink)).toBe(path);
121 });
122
123 it("maps app scheme and hosted links to in-app paths, and nothing else", () => {
124 expect(promptLinkAppPath("t3code://new?prompt=hi")).toBe("/new?prompt=hi");
125 expect(promptLinkAppPath("t3code-dev://threads/env-1/thread-1")).toBe("/env-1/thread-1");
126 expect(promptLinkAppPath("https://app.t3.codes/env-1/thread-1?prompt=go")).toBe(
127 "/env-1/thread-1?prompt=go",
128 );
129 expect(promptLinkAppPath("https://app.t3.codes/threads/env-1/thread-1")).toBe(
130 "/env-1/thread-1",
131 );
132 expect(promptLinkAppPath("https://app.t3.codes/threads/env-1/thread-1?prompt=go")).toBe(
133 "/env-1/thread-1?prompt=go",
134 );
135 // The web app's own thread route, reloaded, is no link.
136 expect(promptLinkAppPath("https://app.t3.codes/env-1/thread-1")).toBeNull();
137 expect(promptLinkAppPath("https://app.t3.codes/threads/env-1")).toBeNull();
138 expect(promptLinkAppPath("https://app.t3.codes/settings/general")).toBeNull();
139 expect(promptLinkAppPath("https://evil.test/new?prompt=hi")).toBeNull();
140 expect(promptLinkAppPath("t3code://app/index.html")).toBeNull();
141 });
142});