|
1 | 1 | import { describe, expect, it } from "vitest" |
2 | 2 |
|
3 | | -import { workspaceCitationState } from "./workspace-citation-state" |
| 3 | +import { |
| 4 | + maxPrefetchedChunkSources, |
| 5 | + workspaceCitationState, |
| 6 | +} from "./workspace-citation-state" |
4 | 7 | import type { ChatCitationView } from "@/domains/chat/types" |
5 | 8 | import type { ParsedChunkView } from "@/domains/chunks/types" |
6 | 9 | import type { SourceView } from "@/domains/sources/types" |
7 | 10 |
|
| 11 | +function makeChunk(chunkId: string, documentId: string): ParsedChunkView { |
| 12 | + return { |
| 13 | + chunkId, |
| 14 | + documentId, |
| 15 | + type: "text", |
| 16 | + content: `${chunkId} content`, |
| 17 | + sourceTitle: `${documentId}.pdf`, |
| 18 | + } |
| 19 | +} |
| 20 | + |
8 | 21 | describe("workspaceCitationState", () => { |
9 | 22 | it("finds the Source and loaded Parsed Chunk for a Citation", () => { |
10 | 23 | const source: SourceView = { |
@@ -77,4 +90,154 @@ describe("workspaceCitationState", () => { |
77 | 90 | }), |
78 | 91 | ).toBeNull() |
79 | 92 | }) |
| 93 | + |
| 94 | + describe("hasExactCitationTargetHint", () => { |
| 95 | + it("returns true when the citation has content text", () => { |
| 96 | + const citation: ChatCitationView = { |
| 97 | + chunkType: "text", |
| 98 | + score: 0.5, |
| 99 | + content: "Revenue grew this quarter.", |
| 100 | + source: { documentId: "document_1", sourceFileName: "Contract.pdf" }, |
| 101 | + } |
| 102 | + |
| 103 | + expect( |
| 104 | + workspaceCitationState.hasExactCitationTargetHint(citation), |
| 105 | + ).toBe(true) |
| 106 | + }) |
| 107 | + |
| 108 | + it("returns true when the citation has a meaningful section path", () => { |
| 109 | + const citation: ChatCitationView = { |
| 110 | + chunkType: "text", |
| 111 | + score: 0.5, |
| 112 | + source: { |
| 113 | + documentId: "document_1", |
| 114 | + sectionPath: "Revenue", |
| 115 | + }, |
| 116 | + } |
| 117 | + |
| 118 | + expect( |
| 119 | + workspaceCitationState.hasExactCitationTargetHint(citation), |
| 120 | + ).toBe(true) |
| 121 | + }) |
| 122 | + |
| 123 | + it("returns false for source-only citations with no useful target hint", () => { |
| 124 | + const citation: ChatCitationView = { |
| 125 | + chunkType: "text", |
| 126 | + score: 0.5, |
| 127 | + source: { |
| 128 | + documentId: "document_1", |
| 129 | + sourceFileName: "Contract.pdf", |
| 130 | + sectionPath: "Root", |
| 131 | + }, |
| 132 | + } |
| 133 | + |
| 134 | + expect( |
| 135 | + workspaceCitationState.hasExactCitationTargetHint(citation), |
| 136 | + ).toBe(false) |
| 137 | + }) |
| 138 | + |
| 139 | + it("returns false for empty content and missing section path", () => { |
| 140 | + const citation: ChatCitationView = { |
| 141 | + chunkType: "text", |
| 142 | + score: 0.5, |
| 143 | + content: " ", |
| 144 | + source: { documentId: "document_1" }, |
| 145 | + } |
| 146 | + |
| 147 | + expect( |
| 148 | + workspaceCitationState.hasExactCitationTargetHint(citation), |
| 149 | + ).toBe(false) |
| 150 | + }) |
| 151 | + }) |
| 152 | + |
| 153 | + describe("upsertPrefetchedChunks bounded LRU", () => { |
| 154 | + it("keeps insertion order so the newest source appears last", () => { |
| 155 | + const next = workspaceCitationState.upsertPrefetchedChunks( |
| 156 | + workspaceCitationState.upsertPrefetchedChunks({}, "source_a", [ |
| 157 | + makeChunk("a", "doc_a"), |
| 158 | + ]), |
| 159 | + "source_b", |
| 160 | + [makeChunk("b", "doc_b")], |
| 161 | + ) |
| 162 | + |
| 163 | + expect(Object.keys(next)).toEqual(["source_a", "source_b"]) |
| 164 | + }) |
| 165 | + |
| 166 | + it("refreshes recency for an existing source by moving it to the end", () => { |
| 167 | + const seed = workspaceCitationState.upsertPrefetchedChunks( |
| 168 | + workspaceCitationState.upsertPrefetchedChunks({}, "source_a", [ |
| 169 | + makeChunk("a", "doc_a"), |
| 170 | + ]), |
| 171 | + "source_b", |
| 172 | + [makeChunk("b", "doc_b")], |
| 173 | + ) |
| 174 | + |
| 175 | + const refreshed = workspaceCitationState.upsertPrefetchedChunks( |
| 176 | + seed, |
| 177 | + "source_a", |
| 178 | + [makeChunk("a", "doc_a"), makeChunk("a2", "doc_a")], |
| 179 | + ) |
| 180 | + |
| 181 | + expect(Object.keys(refreshed)).toEqual(["source_b", "source_a"]) |
| 182 | + expect(refreshed["source_a"]?.length).toBe(2) |
| 183 | + }) |
| 184 | + |
| 185 | + it(`evicts the oldest entries when more than ${maxPrefetchedChunkSources} sources are stored`, () => { |
| 186 | + const seeded = Array.from({ length: maxPrefetchedChunkSources }).reduce< |
| 187 | + Readonly<Record<string, ParsedChunkView[]>> |
| 188 | + >( |
| 189 | + (acc, _value, index) => |
| 190 | + workspaceCitationState.upsertPrefetchedChunks( |
| 191 | + acc, |
| 192 | + `source_${index}`, |
| 193 | + [makeChunk(`chunk_${index}`, `doc_${index}`)], |
| 194 | + ), |
| 195 | + {}, |
| 196 | + ) |
| 197 | + |
| 198 | + const next = workspaceCitationState.upsertPrefetchedChunks( |
| 199 | + seeded, |
| 200 | + "source_overflow", |
| 201 | + [makeChunk("chunk_overflow", "doc_overflow")], |
| 202 | + ) |
| 203 | + |
| 204 | + const keys = Object.keys(next) |
| 205 | + expect(keys.length).toBe(maxPrefetchedChunkSources) |
| 206 | + expect(keys).not.toContain("source_0") |
| 207 | + expect(keys).toContain("source_overflow") |
| 208 | + expect(keys[keys.length - 1]).toBe("source_overflow") |
| 209 | + }) |
| 210 | + }) |
| 211 | + |
| 212 | + describe("removePrefetchedChunks", () => { |
| 213 | + it("returns the same record when the source is not cached", () => { |
| 214 | + const seed = workspaceCitationState.upsertPrefetchedChunks({}, "source_a", [ |
| 215 | + makeChunk("a", "doc_a"), |
| 216 | + ]) |
| 217 | + |
| 218 | + const next = workspaceCitationState.removePrefetchedChunks( |
| 219 | + seed, |
| 220 | + "missing", |
| 221 | + ) |
| 222 | + |
| 223 | + expect(next).toBe(seed) |
| 224 | + }) |
| 225 | + |
| 226 | + it("removes only the requested source", () => { |
| 227 | + const seed = workspaceCitationState.upsertPrefetchedChunks( |
| 228 | + workspaceCitationState.upsertPrefetchedChunks({}, "source_a", [ |
| 229 | + makeChunk("a", "doc_a"), |
| 230 | + ]), |
| 231 | + "source_b", |
| 232 | + [makeChunk("b", "doc_b")], |
| 233 | + ) |
| 234 | + |
| 235 | + const next = workspaceCitationState.removePrefetchedChunks( |
| 236 | + seed, |
| 237 | + "source_a", |
| 238 | + ) |
| 239 | + |
| 240 | + expect(Object.keys(next)).toEqual(["source_b"]) |
| 241 | + }) |
| 242 | + }) |
80 | 243 | }) |
0 commit comments