import { beforeEach, describe, expect, it, vi } from "vitest"; import type { RepoDiff, RepoDiffDeps, RepoDiffFile } from "$lib/api/repoDiff"; import { createDiffQueue, type DiffEntry } from "$lib/components/repo/diffQueue"; const { fetchSides, parseDiffInWorker, streamPatchInWorker } = vi.hoisted(() => ({ fetchSides: vi.fn(), parseDiffInWorker: vi.fn(), streamPatchInWorker: vi.fn() })); vi.mock("$lib/api/repoDiff", () => ({ fetchSides })); vi.mock("$lib/components/repo/diffRpc", () => ({ parseDiffInWorker, streamPatchInWorker })); // the real one fetches and parses in a worker, answering in batches; the mock // just replays whatever batches a test hands it const patchYielding = (...batches: { name: string; hunks: never[]; prevName?: string }[][]) => streamPatchInWorker.mockImplementation( async (_url: string, onFiles: (files: unknown[]) => void) => { for (const batch of batches) onFiles(batch); } ); const base = (name: string) => ({ key: `${name}\u2192${name}`, name, kind: "changed" as const, stat: { insertions: 1, deletions: 0 }, lines: 4 }); const side = (name: string, oid: string) => ({ path: name, oid, isBinary: false, isSubmodule: false, size: 12 }); const file = (name: string): RepoDiffFile => ({ ...base(name), sides: { lhs: side(name, "a"), rhs: side(name, "b") } }); const noteFile = (name: string): RepoDiffFile => ({ ...base(name), note: "binary" }); const diffOf = (...files: RepoDiffFile[]): RepoDiff => ({ files, stat: { insertions: 1, deletions: 0, files_changed: files.length }, contents: { oldRef: "old", newRef: "new" } }); const harness = (diff: RepoDiff) => { let entries: Record = {}; let patchExpected = false; const queue = createDiffQueue({ deps: () => ({}) as RepoDiffDeps, diff: () => diff, entry: (key) => entries[key], put: (parsed) => (entries = { ...entries, ...parsed }), patchExpected: () => patchExpected }); return { queue, get entries() { return entries; }, expectPatch: (expected: boolean) => (patchExpected = expected) }; }; const deferred = () => { let resolve!: (value: T) => void; const promise = new Promise((r) => (resolve = r)); return { promise, resolve }; }; const settle = () => new Promise((resolve) => setTimeout(resolve, 0)); describe("createDiffQueue", () => { beforeEach(() => vi.clearAllMocks()); it("leaves a wanted file to the patch while one is still expected", async () => { fetchSides.mockResolvedValue({ note: "should never be asked for" }); const { queue, entries, expectPatch } = harness(diffOf(file("a.ts"))); expectPatch(true); queue.want("a.ts\u2192a.ts"); await settle(); expect(fetchSides).not.toHaveBeenCalled(); expect(entries).toEqual({}); }); it("fetches what the patch never carried once it settles", async () => { fetchSides.mockResolvedValue({ note: "binary" }); const held = harness(diffOf(file("a.ts"))); held.expectPatch(true); held.queue.want("a.ts\u2192a.ts"); await settle(); held.expectPatch(false); held.queue.drain(); await settle(); expect(fetchSides).toHaveBeenCalledTimes(1); expect(held.entries["a.ts\u2192a.ts"]).toEqual({ note: "binary" }); }); it("parses a file's two sides and keeps the result", async () => { fetchSides.mockResolvedValue({ oldFile: { name: "a.ts", contents: "old" }, newFile: { name: "a.ts", contents: "new" } }); parseDiffInWorker.mockResolvedValue({ name: "a.ts", hunks: [] }); const a = file("a.ts"); const h = harness(diffOf(a)); h.queue.want(a.key); await settle(); expect(h.entries[a.key]).toEqual({ name: "a.ts", hunks: [] }); expect(parseDiffInWorker).toHaveBeenCalledOnce(); }); it("notes a file whose contents cannot be loaded, once the retries are spent", async () => { vi.useFakeTimers(); try { fetchSides.mockRejectedValue(new Error("gone")); const a = file("a.ts"); const h = harness(diffOf(a)); h.queue.want(a.key); await vi.runAllTimersAsync(); expect(h.entries[a.key]).toEqual({ note: "Contents could not be loaded." }); // the first call plus its retries expect(fetchSides).toHaveBeenCalledTimes(4); } finally { vi.useRealTimers(); } }); it("retries a file the mirror briefly refused", async () => { vi.useFakeTimers(); try { fetchSides.mockRejectedValueOnce(new Error("429")).mockResolvedValue({ oldFile: { name: "a.ts", contents: "old" }, newFile: { name: "a.ts", contents: "new" } }); parseDiffInWorker.mockResolvedValue({ name: "a.ts", hunks: [] }); const a = file("a.ts"); const h = harness(diffOf(a)); h.queue.want(a.key); await vi.runAllTimersAsync(); expect(h.entries[a.key]).toEqual({ name: "a.ts", hunks: [] }); } finally { vi.useRealTimers(); } }); it("never queues a file that has no sides to fetch", async () => { const binary = noteFile("logo.png"); const h = harness(diffOf(binary)); h.queue.want(binary.key); await settle(); expect(fetchSides).not.toHaveBeenCalled(); }); it("fills every file the patch covers in one write", async () => { patchYielding([ { name: "a.ts", hunks: [] }, { name: "b.ts", hunks: [] } ]); const a = file("a.ts"); const b = file("b.ts"); const h = harness(diffOf(a, b)); await h.queue.applyPatch("/commit.diff", new AbortController().signal); expect(h.entries[a.key]).toEqual({ name: "a.ts", hunks: [] }); expect(h.entries[b.key]).toEqual({ name: "b.ts", hunks: [] }); }); it("lands each streamed batch as it arrives, not only at the end", async () => { const a = file("a.ts"); const b = file("b.ts"); const seen: number[] = []; streamPatchInWorker.mockImplementation( async (_url: string, onFiles: (files: unknown[]) => void) => { onFiles([{ name: "a.ts", hunks: [] }]); seen.push(Object.keys(h.entries).length); onFiles([{ name: "b.ts", hunks: [] }]); seen.push(Object.keys(h.entries).length); } ); const h = harness(diffOf(a, b)); await h.queue.applyPatch("/commit.patch", new AbortController().signal); // the first batch is readable before the second one is parsed expect(seen).toEqual([1, 2]); expect(h.entries[b.key]).toEqual({ name: "b.ts", hunks: [] }); }); it("leaves a file the patch covers alone while its own fetch is in flight", async () => { const sides = deferred<{ oldFile: unknown; newFile: unknown }>(); fetchSides.mockReturnValue(sides.promise); parseDiffInWorker.mockResolvedValue({ name: "a.ts", hunks: ["full"] }); patchYielding([{ name: "a.ts", hunks: ["partial"] as never[] }]); const a = file("a.ts"); const h = harness(diffOf(a)); h.queue.want(a.key); await h.queue.applyPatch("/commit.diff", new AbortController().signal); expect(h.entries[a.key]).toBeUndefined(); sides.resolve({ oldFile: { name: "a.ts", contents: "old" }, newFile: { name: "a.ts", contents: "new" } }); await settle(); expect(h.entries[a.key]).toEqual({ name: "a.ts", hunks: ["full"] }); }); it("matches a renamed file through the patch's previous name", async () => { patchYielding([{ name: "new.ts", prevName: "old.ts", hunks: [] }]); const renamed: RepoDiffFile = { ...file("new.ts"), oldName: "old.ts" }; const h = harness(diffOf(renamed)); await h.queue.applyPatch("/commit.diff", new AbortController().signal); expect(h.entries[renamed.key]).toMatchObject({ name: "new.ts" }); }); it("drains the file nearest the cursor first", async () => { const order: string[] = []; const gate = deferred(); fetchSides.mockImplementation( async (_deps: unknown, _contents: unknown, f: RepoDiffFile) => { order.push(f.name); await gate.promise; return { oldFile: { name: f.name, contents: "" }, newFile: { name: f.name, contents: "" } }; } ); parseDiffInWorker.mockResolvedValue({ name: "x", hunks: [] }); const files = Array.from({ length: 9 }, (_, index) => file(`f${index}.ts`)); const h = harness(diffOf(...files)); for (const f of files) h.queue.want(f.key); h.queue.focus(files[8].key); for (const f of files) h.queue.want(f.key); gate.resolve(); await settle(); expect(order.slice(0, 6)).toHaveLength(6); expect(order).toContain("f8.ts"); }); });