Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434import { describe, expect, it, vi } from "vitest";import { createBobbinClient } from "$lib/api/client";import { gitTarget } from "$lib/api/gitclient";import { cappedFetch, DiffTooLargeError, isDiffTooLarge, fetchSides, loadDiff, resolveChange} from "$lib/api/repoDiff";import { emptyTreeOid, type MirrorFileDiff } from "$lib/api/gitmirror";
const src = (path: string, extra: Partial<MirrorFileDiff["lhsSrc"]> = {}) => ({ path, oid: `oid-${path}`, size: 10, isBinary: false, isSubmodule: false, ...extra});
// gitmirror names both sides of an add or delete after the same path and marks the// side that doesn't exist with a null oidconst absent = (path: string) => src(path, { oid: "0".repeat(40), size: 0 });
const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
const REPO = "did:plc:repo" as never;
const diffSpec = { kind: "diff", repo: REPO, version: { base: "base", head: "head" } } as const;
// contents keyed by `<ref>:<path>`, so each side can differconst harness = (diffs: MirrorFileDiff[], contents: Record<string, string>) => { const calls: string[] = []; const fetchMock = vi.fn<typeof globalThis.fetch>().mockImplementation(async (input) => { const url = new URL(String(input)); calls.push(url.pathname); if (url.pathname.endsWith("temp2.getDiff") || url.pathname.endsWith("temp2.getInterdiff")) { return json({ diffs }); } const key = `${url.searchParams.get("ref")}:${url.searchParams.get("path")}`; const content = contents[key]; if (content === undefined) return new Response("nope", { status: 404 }); return json({ path: url.searchParams.get("path"), content, encoding: "utf-8", size: 1 }); });
const deps = { ctx: createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }) }; const git = gitTarget( { bobbinUrl: "https://bobbin.test", knotMirrorUrl: "" } as never, { uri: "at://did:plc:alice/sh.tangled.repo/core", repoDid: "did:plc:repo" }, fetchMock ); return { deps: { ...deps, git }, calls };};
const interdiffSpec = { kind: "interdiff", repo: REPO, oldVersion: { base: "v1base", head: "v1head" }, version: { base: "v2base", head: "v2head" }} as const;
const blobCalls = (calls: string[]) => calls.filter((path) => path.endsWith("repo.blob"));
describe("loadDiff", () => { it("describes every file without reading a single blob", async () => { const { deps, calls } = harness( [ { lhsSrc: src("a.ts"), rhsSrc: src("a.ts"), hunks: [ { novelLhs: [0], novelRhs: [0, 1], lines: [{ lhs: 0, rhs: 0 }, { rhs: 1 }] } ], hasSyntacticChanges: false }, { lhsSrc: absent("new.ts"), rhsSrc: src("new.ts"), hunks: [{ novelLhs: [], novelRhs: [0], lines: [{ rhs: 0 }] }], hasSyntacticChanges: false }, { lhsSrc: src("gone.ts"), rhsSrc: absent("gone.ts"), hunks: [{ novelLhs: [0], novelRhs: [], lines: [{ lhs: 0 }] }], hasSyntacticChanges: false }, { lhsSrc: src("old.ts"), rhsSrc: src("moved.ts"), hunks: [], hasSyntacticChanges: false } ], {} );
const diff = await loadDiff(deps, diffSpec);
expect(diff.stat).toEqual({ insertions: 3, deletions: 2, files_changed: 4 }); expect(diff.contents).toEqual({ oldRef: "base", newRef: "head" }); expect(diff.files.map((file) => file.kind)).toEqual([ "changed", "new", "deleted", "renamed" ]); expect(diff.files[3].oldName).toBe("old.ts"); // every renderable file carries the two sides a later fetch needs expect(diff.files.every((file) => file.sides !== undefined)).toBe(true); // the whole point: metadata costs one request, no blob reads at all expect(blobCalls(calls)).toHaveLength(0); expect(calls).toHaveLength(1); });
it("reserves height from the hunk line count", async () => { const { deps } = harness( [ { lhsSrc: src("a.ts"), rhsSrc: src("a.ts"), hunks: [ { novelLhs: [], novelRhs: [], lines: [{ lhs: 0, rhs: 0 }, { rhs: 1 }] }, { novelLhs: [], novelRhs: [], lines: [{ lhs: 5, rhs: 6 }] } ], hasSyntacticChanges: false } ], {} );
const diff = await loadDiff(deps, diffSpec);
// three line pairs plus two lines of per-hunk chrome expect(diff.files[0].lines).toBe(7); });
it("notes the files it will never fetch, and leaves them no sides", async () => { const { deps } = harness( [ { lhsSrc: src("logo.png", { isBinary: true }), rhsSrc: src("logo.png", { isBinary: true }), hunks: [], hasSyntacticChanges: false }, { lhsSrc: src("vendor", { isSubmodule: true }), rhsSrc: src("vendor", { isSubmodule: true }), hunks: [], hasSyntacticChanges: false }, // the payload carries each side's size, so an oversize file is noted // without ever paying a request the blob endpoint would refuse { lhsSrc: src("huge.ts"), rhsSrc: src("huge.ts", { size: (1 << 20) + 1 }), hunks: [], hasSyntacticChanges: false } ], {} );
const diff = await loadDiff(deps, diffSpec);
expect(diff.files.map((file) => file.note)).toEqual([ "This is a binary file and will not be displayed.", "Submodule pointer, not shown.", "This file is too large to display." ]); expect(diff.files.every((file) => file.sides === undefined)).toBe(true); });
it("marks an interdiff's left side as arriving inline", async () => { const { deps, calls } = harness([], {});
const diff = await loadDiff(deps, interdiffSpec);
expect(diff.contents).toEqual({ inlineOld: true, newRef: "v2head" }); expect(blobCalls(calls)).toHaveLength(0); });});
describe("fetchSides", () => { it("reads both sides of a plain diff, and only the ones that exist", async () => { const { deps, calls } = harness( [ { lhsSrc: src("a.ts"), rhsSrc: src("a.ts"), hunks: [{ novelLhs: [0], novelRhs: [0], lines: [{ lhs: 0, rhs: 0 }] }], hasSyntacticChanges: false }, { lhsSrc: absent("new.ts"), rhsSrc: src("new.ts"), hunks: [{ novelLhs: [], novelRhs: [0], lines: [{ rhs: 0 }] }], hasSyntacticChanges: false } ], { "base:a.ts": "one\n", "head:a.ts": "two\nthree\n", "head:new.ts": "hello\n" } );
const diff = await loadDiff(deps, diffSpec); const changed = await fetchSides(deps, diff.contents, diff.files[0]); const added = await fetchSides(deps, diff.contents, diff.files[1]);
expect(changed).toEqual({ oldFile: { name: "a.ts", contents: "one\n" }, newFile: { name: "a.ts", contents: "two\nthree\n" } }); // a file with no left side gets an empty left side, not a failed fetch expect(added).toEqual({ oldFile: { name: "new.ts", contents: "" }, newFile: { name: "new.ts", contents: "hello\n" } }); // two for the changed file, one for the added file's right side: the absent // side is never fetched, it would 404 expect(blobCalls(calls)).toHaveLength(3); });
it("diffs a root commit against the empty tree and reads only the new side", async () => { const head = "a".repeat(40); const { deps, calls } = harness( [ { lhsSrc: absent("new.ts"), rhsSrc: src("new.ts"), hunks: [{ novelLhs: [], novelRhs: [0], lines: [{ rhs: 0 }] }], hasSyntacticChanges: false } ], { [`${head}:new.ts`]: "hello\n" } ); const spec = { kind: "diff", repo: REPO, version: { base: emptyTreeOid(head), head } } as const;
const diff = await loadDiff(deps, spec); const added = await fetchSides(deps, diff.contents, diff.files[0]);
expect(diff.contents).toEqual({ oldRef: emptyTreeOid(head), newRef: head }); expect(diff.files.map((file) => file.kind)).toEqual(["new"]); expect(added).toEqual({ oldFile: { name: "new.ts", contents: "" }, newFile: { name: "new.ts", contents: "hello\n" } }); expect(blobCalls(calls)).toHaveLength(1); });
it("takes an interdiff's left side inline and fetches only the right", async () => { const { deps, calls } = harness( [ { lhsSrc: src("a.ts", { content: "one\n" }), rhsSrc: src("a.ts"), hunks: [{ novelLhs: [0], novelRhs: [0], lines: [{ lhs: 0, rhs: 0 }] }], hasSyntacticChanges: false }, // the rebased tree only exists inside one gitmirror request, so a left // side with no inline text can never be recovered { lhsSrc: src("b.ts"), rhsSrc: src("b.ts"), hunks: [], hasSyntacticChanges: false } ], { "v2head:a.ts": "two\n", "v2head:b.ts": "two\n" } );
const diff = await loadDiff(deps, interdiffSpec);
expect(await fetchSides(deps, diff.contents, diff.files[0])).toEqual({ oldFile: { name: "a.ts", contents: "one\n" }, newFile: { name: "a.ts", contents: "two\n" } }); expect(await fetchSides(deps, diff.contents, diff.files[1])).toEqual({ note: "Contents could not be loaded." }); // one blob per file, the right side only expect(blobCalls(calls)).toHaveLength(2); });
it("notes a side the blob endpoint cannot serve", async () => { const { deps } = harness( [ { lhsSrc: src("a.ts"), rhsSrc: src("a.ts"), hunks: [], hasSyntacticChanges: false } ], {} );
const diff = await loadDiff(deps, diffSpec);
expect(await fetchSides(deps, diff.contents, diff.files[0])).toEqual({ note: "Contents could not be loaded." }); });
it("hands back a noted file's note rather than fetching it", async () => { const { deps, calls } = harness( [ { lhsSrc: src("logo.png", { isBinary: true }), rhsSrc: src("logo.png", { isBinary: true }), hunks: [], hasSyntacticChanges: false } ], {} );
const diff = await loadDiff(deps, diffSpec);
expect(await fetchSides(deps, diff.contents, diff.files[0])).toEqual({ note: "This is a binary file and will not be displayed." }); expect(blobCalls(calls)).toHaveLength(0); });});
describe("resolveChange", () => { const bases = { oldVersion: { base: "v1base", head: "v1head" }, version: { base: "v2base", head: "v2head" } }; const older = [{ hash: "aaa", parent: "aaaparent", changeId: "kmzwvxqo" }] as never[]; const newer = [ { hash: "bbb", parent: "bbbparent", changeId: "kmzwvxqo" }, { hash: "ccc", parent: "cccparent", changeId: "nvtsyrpq" } ] as never[];
it("interdiffs the two commits carrying the change", () => { expect(resolveChange("kmzwvxqo", older, newer, bases)).toEqual({ kind: "interdiff", oldVersion: { base: "aaaparent", head: "aaa" }, version: { base: "bbbparent", head: "bbb" } }); });
it("plain-diffs a change that only exists in the newer version", () => { expect(resolveChange("nvtsyrpq", older, newer, bases)).toEqual({ kind: "diff", version: { base: "cccparent", head: "ccc" } }); });
it("reports a change the newer version does not have", () => { expect(resolveChange("zzzzzzzz", older, newer, bases)).toEqual({ kind: "missing" }); // a repo without jj change ids can never match expect(resolveChange("kmzwvxqo", older, [{ hash: "bbb" }] as never[], bases)).toEqual({ kind: "missing" }); });
it("falls back to the version base for a root commit", () => { const rootless = [{ hash: "aaa", changeId: "kmzwvxqo" }] as never[]; expect(resolveChange("kmzwvxqo", rootless, newer, bases)).toEqual({ kind: "interdiff", oldVersion: { base: "v1base", head: "aaa" }, version: { base: "bbbparent", head: "bbb" } }); });});
describe("emptyTreeOid", () => { it("picks the sentinel matching the repo's hash algorithm", () => { expect(emptyTreeOid("a".repeat(40))).toBe("4b825dc642cb6eb9a060e54bf8d69288fbee4904"); expect(emptyTreeOid("a".repeat(64))).toBe( "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321" ); });});
describe("cappedFetch", () => { const bigJson = (bytes: number) => new Response("x".repeat(bytes), { status: 200, headers: { "content-type": "application/json" } });
it("passes through bodies under the cap", async () => { const capped = cappedFetch( vi.fn(async () => json({ ok: true })), 1 << 20 ); const res = await capped("https://mirror.test/xrpc/nsid"); await expect(res.json()).resolves.toEqual({ ok: true }); });
it("fails fast once the body passes the cap", async () => { const capped = cappedFetch( vi.fn(async () => bigJson(4096)), 1024 ); const res = await capped("https://mirror.test/xrpc/nsid"); await expect(res.json()).rejects.toThrow(DiffTooLargeError); });
it("is detectable through isDiffTooLarge", async () => { const capped = cappedFetch( vi.fn(async () => bigJson(4096)), 1024 ); const res = await capped("https://mirror.test/xrpc/nsid"); await expect(res.json().catch(isDiffTooLarge)).resolves.toBe(true); });
it("does not touch non-ok responses", async () => { const capped = cappedFetch( vi.fn(async () => new Response("no", { status: 500 })), 8 ); const res = await capped("https://mirror.test/xrpc/nsid"); expect(res.status).toBe(500); });});