1import { EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts";
2import { expect, it } from "vite-plus/test";
3import { serializeAssistantCitation } from "./assistantCitations.ts";
4import { searchableMessageSegments } from "./threadFindText.ts";
5
6it("excludes review attachments rendered as context chips", () => {
7 const text = [
8 "Before **review**",
9 '<review_comment sectionId="turn:2" sectionTitle="Turn 2" filePath="hidden.ts" startIndex="3" endIndex="14" rangeLabel="L4">',
10 "Keep **this literal** comment.",
11 "```diff",
12 "+ hidden patch content",
13 "```",
14 "</review_comment>",
15 "After review",
16 ].join("\n");
17 expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([
18 "Before review",
19 " After review",
20 ]);
21});
22
23it("keeps malformed review tags visible, matching the message renderer", () => {
24 const text = "<review_comment>not a valid attachment</review_comment>";
25 expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([text]);
26});
27
28it("excludes structured context chips without joining text across them", () => {
29 expect(
30 searchableMessageSegments({
31 role: "user",
32 streaming: false,
33 text: "before[hidden label](t3-context://v1/terminal/terminal_1)after",
34 context: { version: 1, records: [] },
35 }),
36 ).toEqual(["before", "after"]);
37});
38
39it("does not upgrade literal legacy tags in messages with structured context", () => {
40 const text =
41 "<terminal_context>\n- Terminal 1 line 12:\n 12 | visible output\n</terminal_context>";
42 expect(
43 searchableMessageSegments({
44 role: "user",
45 streaming: false,
46 text,
47 context: { version: 1, records: [] },
48 })?.join("\n"),
49 ).toContain("visible output");
50 expect(searchableMessageSegments({ role: "user", streaming: false, text })).toEqual([]);
51});
52
53it("keeps context reference syntax inside code searchable", () => {
54 expect(
55 searchableMessageSegments({
56 role: "user",
57 streaming: false,
58 text: "`[label](t3-context://v1/terminal/terminal_1)`",
59 }),
60 ).toEqual(["[label](t3-context://v1/terminal/terminal_1)"]);
61});
62
63it("excludes repeated legacy attachments containing literal context tags", () => {
64 const context =
65 "<terminal_context>\n- Terminal 1 line 12:\n 12 | <terminal_context>literal</terminal_context>\n</terminal_context>";
66 expect(
67 searchableMessageSegments({
68 role: "user",
69 streaming: false,
70 text: `Fix this\n\n${context}\n\n${context}`,
71 }),
72 ).toEqual(["Fix this"]);
73});
74
75const assistantSegments = (text: string, cwd?: string) =>
76 searchableMessageSegments({ role: "assistant", text, streaming: false }, cwd);
77
78it("searches displayed prose and file-chip labels without hidden paths", () => {
79 expect(
80 assistantSegments("[important description](/tmp/actual.ts). `/tmp/inline-example.ts:42`"),
81 ).toEqual(["important description actual.ts. inline-example.ts · L42"]);
82 expect(assistantSegments("[label](src/main.ts#L3C2)", "/workspace/repo")).toEqual([
83 "label main.ts · L3:C2",
84 ]);
85 expect(assistantSegments("[label](src/main.ts#L3C2)")).toEqual(["label"]);
86});
87
88it("includes the same parent suffixes for duplicate filenames as the renderer", () => {
89 expect(
90 assistantSegments(
91 "[first](src/main.ts) and `/workspace/repo/tests/main.ts:2`",
92 "/workspace/repo",
93 ),
94 ).toEqual(["first main.ts · repo/src and main.ts · repo/tests · L2"]);
95 expect(
96 assistantSegments(
97 "[first](src/main.ts) and `/workspace/repo/src/main.ts:2`",
98 "/workspace/repo",
99 ),
100 ).toEqual(["first main.ts and main.ts · L2"]);
101});
102
103it("keeps fence paths literal and indexes file-chip labels in user messages", () => {
104 expect(assistantSegments("```text\n/tmp/file.ts:42\n```")).toEqual(["/tmp/file.ts:42\n"]);
105 expect(
106 searchableMessageSegments({ role: "user", text: "`/tmp/file.ts:42`", streaming: false }),
107 ).toEqual(["file.ts · L42"]);
108});
109
110it("indexes nested disclosure summaries and bodies in rendered order", () => {
111 expect(
112 assistantSegments(
113 "<details><summary>Outer</summary><p>first</p><details><summary>Inner</summary><p>second</p></details></details>",
114 ),
115 ).toEqual(["Outer", "first", "Inner", "second"]);
116});
117
118it("uses Insight line breaks without splitting ordinary assistant prose", () => {
119 expect(assistantSegments("★ Insight ─────\nfirst line\nsecond line")).toEqual([
120 "★ Insight ─────",
121 " first line",
122 " second line",
123 ]);
124 expect(assistantSegments("first line\nsecond line")).toEqual(["first line second line"]);
125});
126
127it("indexes skill labels in prose but leaves links and code literal", () => {
128 expect(
129 searchableMessageSegments(
130 {
131 role: "assistant",
132 streaming: false,
133 text: "Use $test-t3-app now.\n\n`$test-t3-app`\n\n[$test-t3-app](https://example.com)",
134 },
135 undefined,
136 [{ name: "test-t3-app", displayName: "T3 App Testing" }],
137 ),
138 ).toEqual(["Use T3 App Testing now.", "$test-t3-app", "$test-t3-app"]);
139});
140
141it("indexes the citation chip label instead of its link text", () => {
142 const citation = {
143 version: 1 as const,
144 environmentId: EnvironmentId.make("environment"),
145 threadId: ThreadId.make("thread"),
146 messageId: MessageId.make("message"),
147 text: "cited needle",
148 start: 0,
149 end: 14,
150 prefix: "",
151 suffix: "",
152 };
153 const text = `Please fix ${serializeAssistantCitation(citation)} thanks`;
154 expect(searchableMessageSegments({ role: "user", text, streaming: false })).toEqual([
155 "Please fix cited needle thanks",
156 ]);
157 const commented = serializeAssistantCitation({ ...citation, comment: "my note" });
158 expect(searchableMessageSegments({ role: "user", text: commented, streaming: false })).toEqual([
159 "my note",
160 ]);
161});