import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RepoDiff, RepoDiffDeps, RepoDiffFile } from "#lib/api/repoDiff.ts";
import { createDiffQueue, type DiffEntry } from "#lib/components/repo/diffQueue.ts";
const { fetchSides, parseDiffInWorker, streamPatchInWorker } = vi.hoisted(() => ({
fetchSides: vi.fn(),
parseDiffInWorker: vi.fn(),
streamPatchInWorker: vi.fn()
}));
vi.mock("#lib/api/repoDiff.ts", () => ({ fetchSides }));
vi.mock("#lib/components/repo/diffRpc.ts", () => ({ 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<string, DiffEntry> = {};
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 = <T>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((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("keeps the note on a file the patch also carries", async () => {
patchYielding([{ name: "Cargo.nix", hunks: [] }]);
const noted = noteFile("Cargo.nix");
const h = harness(diffOf(noted));
await h.queue.applyPatch("/commit.diff", new AbortController().signal);
h.queue.adopt({ [noted.key]: { name: "Cargo.nix", hunks: [] } as never });
expect(h.entries[noted.key]).toBeUndefined();
});
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<void>();
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");
});
});