Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267import { 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 itconst 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("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"); });});