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