1import { afterEach, assert, it, vi } from "vite-plus/test";
2
3import { appAtomRegistry } from "../../state/atom-registry";
4import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter";
5import * as ReviewModel from "./reviewModel";
6import { getCachedReviewParsedDiff, MAX_CACHED_REVIEW_SOURCE_CHARACTERS } from "./reviewState";
7import { getReviewDiffPrewarmSections, prewarmReviewDiffSection } from "./useReviewDiffPrewarming";
8
9const threadKey = "env:thread-prewarm";
10const reviewDiff = [
11 "diff --git a/src/a.ts b/src/a.ts",
12 "new file mode 100644",
13 "--- /dev/null",
14 "+++ b/src/a.ts",
15 "@@ -0,0 +1 @@",
16 "+export const value = 1;",
17].join("\n");
18
19function makeSection(index: number, diff: string | null = reviewDiff) {
20 return {
21 id: `turn:${index}`,
22 kind: "turn",
23 title: `Turn ${index}`,
24 subtitle: null,
25 isLoading: false,
26 diff,
27 } satisfies ReviewModel.ReviewSectionItem;
28}
29
30function prepareSelection(
31 sections: ReadonlyArray<ReviewModel.ReviewSectionItem>,
32 selectedSectionId: string,
33) {
34 const section = sections.find((candidate) => candidate.id === selectedSectionId);
35 assert.ok(section);
36 const input = { threadKey, sectionId: section.id, diff: section.diff };
37 const parsed = getCachedReviewParsedDiff(input);
38 const native = getCachedNativeReviewDiffData({ parsedDiff: parsed });
39 for (const pending of getReviewDiffPrewarmSections({ threadKey, sections, selectedSectionId })) {
40 prewarmReviewDiffSection({ threadKey: input.threadKey, section: pending });
41 }
42 assert.strictEqual(getCachedReviewParsedDiff(input), parsed);
43 assert.strictEqual(getCachedNativeReviewDiffData({ parsedDiff: parsed }), native);
44 return { parsed, native };
45}
46
47afterEach(() => {
48 appAtomRegistry.reset();
49 vi.restoreAllMocks();
50});
51
52it("warms the nearest sections within the remaining entry budget", () => {
53 const sections = Array.from({ length: 12 }, (_, index) => makeSection(index));
54
55 const pending = getReviewDiffPrewarmSections({
56 threadKey,
57 sections,
58 selectedSectionId: "turn:5",
59 });
60
61 assert.deepStrictEqual(
62 pending.map((section) => section.id),
63 ["turn:4", "turn:6", "turn:3", "turn:7", "turn:2", "turn:8", "turn:1"],
64 );
65});
66
67it("reserves the selected source budget and skips unloaded or oversized sections", () => {
68 const halfBudget = "x".repeat(2 * 1024 * 1024);
69 const sections = [
70 makeSection(0),
71 makeSection(1, halfBudget),
72 makeSection(2, "x".repeat(MAX_CACHED_REVIEW_SOURCE_CHARACTERS + 1)),
73 makeSection(3, halfBudget),
74 makeSection(4, null),
75 makeSection(5),
76 ];
77
78 const pending = getReviewDiffPrewarmSections({
79 threadKey,
80 sections,
81 selectedSectionId: "turn:3",
82 });
83
84 assert.deepStrictEqual(
85 pending.map((section) => section.id),
86 ["turn:1"],
87 );
88 prepareSelection(sections, "turn:3");
89});
90
91it("reserves a larger retained source when the selected input loses whitespace", () => {
92 const parsed = getCachedReviewParsedDiff({
93 threadKey,
94 sectionId: "turn:0",
95 diff: reviewDiff.padEnd(MAX_CACHED_REVIEW_SOURCE_CHARACTERS, " "),
96 });
97 const native = getCachedNativeReviewDiffData({ parsedDiff: parsed });
98
99 const selected = prepareSelection([makeSection(0), makeSection(1)], "turn:0");
100
101 assert.strictEqual(selected.parsed, parsed);
102 assert.strictEqual(selected.native, native);
103});
104
105it("reserves a larger retained source when a neighboring input loses whitespace", () => {
106 const parsed = getCachedReviewParsedDiff({
107 threadKey,
108 sectionId: "turn:1",
109 diff: reviewDiff.padEnd(MAX_CACHED_REVIEW_SOURCE_CHARACTERS - reviewDiff.length, " "),
110 });
111 const native = getCachedNativeReviewDiffData({ parsedDiff: parsed });
112
113 prepareSelection([makeSection(0), makeSection(1), makeSection(2)], "turn:0");
114
115 assert.strictEqual(
116 getCachedReviewParsedDiff({
117 threadKey,
118 sectionId: "turn:1",
119 diff: reviewDiff,
120 }),
121 parsed,
122 );
123 assert.strictEqual(getCachedNativeReviewDiffData({ parsedDiff: parsed }), native);
124});
125
126it.each([null, "turn:missing"])(
127 "does not prewarm without a selected section: %s",
128 (selectedSectionId) => {
129 assert.deepStrictEqual(
130 getReviewDiffPrewarmSections({ threadKey, sections: [makeSection(0)], selectedSectionId }),
131 [],
132 );
133 },
134);
135
136it("does not prewarm when the selected section exceeds the source budget", () => {
137 const sections = [
138 makeSection(0, "x".repeat(MAX_CACHED_REVIEW_SOURCE_CHARACTERS + 1)),
139 makeSection(1),
140 ];
141
142 assert.deepStrictEqual(
143 getReviewDiffPrewarmSections({ threadKey, sections, selectedSectionId: "turn:0" }),
144 [],
145 );
146});
147
148it("does not parse an oversized direct prewarm that the cache cannot retain", () => {
149 const parse = vi.spyOn(ReviewModel, "buildReviewParsedDiff");
150
151 prewarmReviewDiffSection({
152 threadKey,
153 section: makeSection(0, "x".repeat(MAX_CACHED_REVIEW_SOURCE_CHARACTERS + 1)),
154 });
155
156 assert.strictEqual(parse.mock.calls.length, 0);
157});
158
159it("reuses selected and native results across repeated nearby selections", () => {
160 const parse = vi.spyOn(ReviewModel, "buildReviewParsedDiff");
161 const sections = Array.from({ length: 10 }, (_, index) => makeSection(index));
162 const first = prepareSelection(sections, "turn:0");
163 assert.strictEqual(parse.mock.calls.length, 8);
164 parse.mockClear();
165 const second = prepareSelection(sections, "turn:1");
166
167 for (let pass = 0; pass < 3; pass += 1) {
168 const firstAgain = prepareSelection(sections, "turn:0");
169 assert.strictEqual(firstAgain.parsed, first.parsed);
170 assert.strictEqual(firstAgain.native, first.native);
171 const secondAgain = prepareSelection(sections, "turn:1");
172 assert.strictEqual(secondAgain.parsed, second.parsed);
173 assert.strictEqual(secondAgain.native, second.native);
174 }
175
176 assert.strictEqual(parse.mock.calls.length, 0);
177});