1import { EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts";
2import { expect, it } from "vite-plus/test";
3import { serializeAssistantCitation } from "./assistantCitations.ts";
4import { ATTACHMENT_ONLY_BOOTSTRAP_PROMPT } from "./composerContextReferences.ts";
5import { searchableMessageSegments, searchablePlanSegments } from "./threadFindText.ts";
6
7it("excludes review attachments rendered as context chips", () => {
8 const text = [
9 "Before **review**",
10 '<review_comment sectionId="turn:2" sectionTitle="Turn 2" filePath="hidden.ts" startIndex="3" endIndex="14" rangeLabel="L4">',
11 "Keep **this literal** comment.",
12 "```diff",
13 "+ hidden patch content",
14 "```",
15 "</review_comment>",
16 "After review",
17 ].join("\n");
18 expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([
19 "Before review",
20 " After review",
21 ]);
22});
23
24it("indexes single tildes in user paths as typed", () => {
25 const text = "Install as ~/a/x, then read (~/a/guide.md).";
26 expect(searchableMessageSegments({ role: "user", text, streaming: false })?.join("")).toBe(text);
27 expect(
28 searchableMessageSegments({ role: "user", text: "Drop ~~old~~", streaming: false })?.join(""),
29 ).toBe("Drop old");
30});
31
32it("excludes structured context chips without joining text across them", () => {
33 expect(
34 searchableMessageSegments({
35 role: "user",
36 streaming: false,
37 text: "before[hidden label](t3-context://v1/terminal/terminal_1)after",
38 context: { version: 1, records: [] },
39 }),
40 ).toEqual(["before", "after"]);
41});
42
43it("excludes the hidden prompt of an attachment-only message", () => {
44 expect(
45 searchableMessageSegments({
46 role: "user",
47 streaming: false,
48 text: ATTACHMENT_ONLY_BOOTSTRAP_PROMPT,
49 }),
50 ).toEqual([]);
51});
52
53it("does not upgrade literal legacy tags in messages with structured context", () => {
54 const text =
55 "<terminal_context>\n- Terminal 1 line 12:\n 12 | visible output\n</terminal_context>";
56 expect(
57 searchableMessageSegments({
58 role: "user",
59 streaming: false,
60 text,
61 context: { version: 1, records: [] },
62 })?.join("\n"),
63 ).toContain("visible output");
64 expect(searchableMessageSegments({ role: "user", streaming: false, text })).toEqual([]);
65});
66
67it("excludes repeated legacy attachments containing literal context tags", () => {
68 const context =
69 "<terminal_context>\n- Terminal 1 line 12:\n 12 | <terminal_context>literal</terminal_context>\n</terminal_context>";
70 expect(
71 searchableMessageSegments({
72 role: "user",
73 streaming: false,
74 text: `Fix this\n\n${context}\n\n${context}`,
75 }),
76 ).toEqual(["Fix this"]);
77});
78
79const assistantSegments = (text: string, cwd?: string) =>
80 searchableMessageSegments({ role: "assistant", text, streaming: false }, cwd);
81
82it("searches displayed prose and file-chip labels without hidden paths", () => {
83 expect(
84 assistantSegments("[important description](/tmp/actual.ts). `/tmp/inline-example.ts:42`"),
85 ).toEqual(["important description actual.ts. inline-example.ts · L42"]);
86 expect(assistantSegments("[label](src/main.ts#L3C2)", "/workspace/repo")).toEqual([
87 "label main.ts · L3:C2",
88 ]);
89 expect(assistantSegments("[label](src/main.ts#L3C2)")).toEqual(["label"]);
90});
91
92it("keeps fence paths literal and indexes file-chip labels in user messages", () => {
93 expect(assistantSegments("```text\n/tmp/file.ts:42\n```")).toEqual(["/tmp/file.ts:42\n"]);
94 expect(
95 searchableMessageSegments({ role: "user", text: "`/tmp/file.ts:42`", streaming: false }),
96 ).toEqual(["file.ts · L42"]);
97});
98
99it("indexes nested disclosure summaries and bodies in rendered order", () => {
100 expect(
101 assistantSegments(
102 "<details><summary>Outer</summary><p>first</p><details><summary>Inner</summary><p>second</p></details></details>",
103 ),
104 ).toEqual(["Outer", "first", "Inner", "second"]);
105});
106
107it("indexes a citation by its comment, not its quote or link text", () => {
108 const citation = {
109 version: 1 as const,
110 environmentId: EnvironmentId.make("environment"),
111 threadId: ThreadId.make("thread"),
112 messageId: MessageId.make("message"),
113 text: "cited needle",
114 start: 0,
115 end: 14,
116 prefix: "",
117 suffix: "",
118 };
119 const text = `Please fix ${serializeAssistantCitation(citation)} thanks`;
120 expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([
121 "Please fix ",
122 " thanks",
123 ]);
124 const commented = serializeAssistantCitation({ ...citation, comment: "my\nnote" });
125 expect(
126 searchableMessageSegments({ role: "user", text: `fix ${commented} thanks`, streaming: false }),
127 ).toEqual(["fix ", "my note thanks"]);
128});
129
130it("indexes insight bodies without hidden fences and keeps user and plan text literal", () => {
131 const text = "★ Insight ─────\nVisible body.\n─────";
132 expect(assistantSegments(text)).toEqual(["Visible body."]);
133 expect(searchableMessageSegments({ role: "user", text, streaming: false })?.join("\n")).toContain(
134 "★ Insight",
135 );
136 expect(searchablePlanSegments(text).join("\n")).toContain("★ Insight");
137 expect(assistantSegments(`\`\`\`text\n${text}\n\`\`\``)?.join("\n")).toContain("★ Insight");
138});