import { describe, expect, it, vi } from "vitest"; import { createBobbinClient } from "./client"; import { gitTarget } from "./gitclient"; import { fetchSides, loadDiff, resolveChange } from "./pullDiff"; import type { MirrorFileDiff } from "./gitmirror"; const src = (path: string, extra: Partial = {}) => ({ 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 oid const 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 `:`, so each side can differ const harness = (diffs: MirrorFileDiff[], contents: Record) => { const calls: string[] = []; const fetchMock = vi.fn().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.truncated).toBe(0); 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("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" } }); }); });