import { describe, expect, it, vi } from "vitest";
import { createBobbinClient } from "#lib/api/client.ts";
import { gitTarget } from "#lib/api/gitclient.ts";
import {
cappedFetch,
DiffTooLargeError,
isDiffTooLarge,
fetchSides,
loadDiff,
loadDiffStat,
resolveChange
} from "#lib/api/repoDiff.ts";
import { emptyTreeOid, type MirrorFileDiff } from "#lib/api/knotmirror.ts";
import type { Fetch } from "#lib/fetch.ts";
const src = (path: string, extra: Partial<MirrorFileDiff["lhsSrc"]> = {}) => ({
path,
oid: `oid-${path}`,
size: 10,
isBinary: false,
isSubmodule: false,
...extra
});
// knotmirror 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 `<ref>:<path>`, so each side can differ
const harness = (diffs: MirrorFileDiff[], contents: Record<string, string>) => {
const calls: string[] = [];
const fetchMock = vi.fn<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 });
if (url.pathname.endsWith("getEntry")) {
return json({
name: url.searchParams.get("path"),
mode: "100644",
size: content.length
});
}
return new Response(content);
});
const deps = {
ctx: createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock })
};
const git = gitTarget(
{ bobbinUrl: "https://bobbin.test" },
{ 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("getBlob"));
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 knotmirror 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("loadDiffStat", () => {
const config = { bobbinUrl: "https://bobbin.test", ssrDiffMaxBytes: 1 << 20 };
const version = { base: "base", head: "head" };
const diffs: MirrorFileDiff[] = [
{
lhsSrc: src("a.ts"),
rhsSrc: src("a.ts"),
hunks: [{ novelLhs: [0], novelRhs: [0, 1], lines: [{ lhs: 0, rhs: 0 }, { rhs: 1 }] }],
hasSyntacticChanges: false
}
];
it("counts the files and lines of the diff", async () => {
const fetch = vi.fn<Fetch>(async () => json({ diffs }));
await expect(loadDiffStat(config, REPO, version, fetch, 1000)).resolves.toEqual({
insertions: 2,
deletions: 1,
files_changed: 1
});
expect(new URL(String(fetch.mock.calls[0][0])).pathname).toMatch(/temp2\.getDiff$/);
});
it("gives up on a diff over the byte cap", async () => {
const fetch = vi.fn<Fetch>(async () => json({ diffs }));
await expect(
loadDiffStat({ ...config, ssrDiffMaxBytes: 16 }, REPO, version, fetch, 1000)
).resolves.toBeUndefined();
});
it("gives up on a diff slower than its time budget", async () => {
const fetch = vi.fn<Fetch>(
(_, init) =>
new Promise((_, reject) =>
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason))
)
);
await expect(loadDiffStat(config, REPO, version, fetch, 20)).resolves.toBeUndefined();
});
it("gives up when the mirror fails", async () => {
const fetch = vi.fn<Fetch>(async () => new Response("no", { status: 500 }));
await expect(loadDiffStat(config, REPO, version, fetch, 1000)).resolves.toBeUndefined();
});
});
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
);
await expect(capped("https://mirror.test/xrpc/nsid")).rejects.toThrow(DiffTooLargeError);
});
it("is detectable through isDiffTooLarge", async () => {
const capped = cappedFetch(
vi.fn(async () => bigJson(4096)),
1024
);
await expect(capped("https://mirror.test/xrpc/nsid").catch(isDiffTooLarge)).resolves.toBe(
true
);
});
it("aborts the request once the body passes the cap", async () => {
let signal: AbortSignal | null | undefined;
const capped = cappedFetch(
vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
signal = init?.signal;
return bigJson(4096);
}),
1024
);
await capped("https://mirror.test/xrpc/nsid").catch(() => undefined);
expect(signal?.aborted).toBe(true);
});
it("leaves the body unread for the caller", async () => {
const touched: PropertyKey[] = [];
const response = json({ ok: true });
const watched = new Proxy(response, {
get(target, key) {
touched.push(key);
const value = Reflect.get(target, key, target);
return typeof value === "function" ? value.bind(target) : value;
}
});
const capped = cappedFetch(
vi.fn(async () => watched),
1 << 20
);
const res = await capped("https://mirror.test/xrpc/nsid");
expect(touched).not.toContain("body");
await expect(res.json()).resolves.toEqual({ ok: 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);
});
});