| 1 | import { EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts"; |
| 2 | import { expect, it } from "vite-plus/test"; |
| 3 | import { serializeAssistantCitation } from "./assistantCitations.ts"; |
| 4 | import { searchableMessageSegments } from "./threadFindText.ts"; |
| 5 | |
| 6 | it("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 | |
| 23 | it("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 | |
| 28 | it("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 | |
| 39 | it("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 | |
| 53 | it("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 | |
| 63 | it("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 | |
| 75 | const assistantSegments = (text: string, cwd?: string) => |
| 76 | searchableMessageSegments({ role: "assistant", text, streaming: false }, cwd); |
| 77 | |
| 78 | it("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 | |
| 88 | it("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 | |
| 103 | it("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 | |
| 110 | it("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 | |
| 118 | it("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 | |
| 127 | it("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 | |
| 141 | it("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 | }); |