import type { Did } from "@atcute/lexicons/syntax";
import { describe, expect, it, vi } from "vitest";
import { blameFor, hydrateBlameAuthors } from "#lib/api/blame.ts";
import { createBobbinClient } from "#lib/api/client.ts";
import type { Fetch } from "#lib/fetch.ts";
const REPO_DID = "did:plc:repo" as Did;
const jsonResponse = (body: unknown): Response =>
new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" }
});
const errorResponse = (status: number, error: string): Response =>
new Response(JSON.stringify({ error }), {
status,
headers: { "content-type": "application/json" }
});
const mirrorResponse = (body: {
commits: Array<Record<string, unknown>>;
runs: Array<Record<string, unknown>>;
}): Response => jsonResponse({ commit: "abc123", ...body });
const makeCtx = (fetchMock: Fetch) =>
createBobbinClient({ serviceUrl: "https://bobbin.example", fetch: fetchMock });
const blameCall = (ctx: ReturnType<typeof makeCtx>) =>
blameFor(ctx, REPO_DID, "main", "src/main.rs");
describe("blameFor", () => {
it("requests knotmirror getBlame and maps its one-based runs", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(
mirrorResponse({
commits: [
{
oid: "abc123",
message: "add main\n\nlonger explanation",
author: {
name: "alice",
email: "did:plc:alice",
when: "2026-06-01T12:30:00Z"
}
}
],
runs: [{ start: 1, count: 3, commit: "abc123" }]
})
);
const ctx = makeCtx(fetchMock);
await expect(blameCall(ctx)).resolves.toEqual([
{
start: 1,
count: 3,
commit: {
sha: "abc123",
subject: "add main",
when: "2026-06-01T12:30:00Z",
author: { name: "alice", email: "did:plc:alice", did: "did:plc:alice" }
}
}
]);
const url = new URL(String(fetchMock.mock.calls[0][0]));
expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp2.getBlame");
expect(url.searchParams.get("repo")).toBe(REPO_DID);
expect(url.searchParams.get("ref")).toBe("main");
expect(url.searchParams.get("path")).toBe("src/main.rs");
});
it("keeps only the subject of the mirror's full commit message", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(
mirrorResponse({
commits: [
{
oid: "abc123",
message: "subject line\n\nbody",
author: {
name: "alice",
email: "alice@tangled.org",
when: "2026-06-01T12:30:00Z"
}
}
],
runs: [{ start: 1, count: 1, commit: "abc123" }]
})
);
const ctx = makeCtx(fetchMock);
const runs = await blameCall(ctx);
expect(runs[0].commit.subject).toBe("subject line");
expect(runs[0].commit.author.did).toBeUndefined();
});
it("propagates mirror errors that are not a mirror miss", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(errorResponse(500, "InternalError"));
const ctx = makeCtx(fetchMock);
await expect(blameCall(ctx)).rejects.toThrow("InternalError");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("rejects duplicate commit metadata", async () => {
const commit = {
oid: "abc123",
message: "add main",
author: { name: "alice", email: "alice@tangled.org", when: "2026-06-01T12:30:00Z" }
};
const fetchMock = vi.fn<Fetch>().mockResolvedValue(
mirrorResponse({
commits: [commit, commit],
runs: [{ start: 1, count: 1, commit: "abc123" }]
})
);
const ctx = makeCtx(fetchMock);
await expect(blameCall(ctx)).rejects.toThrow("duplicate blame commit: abc123");
});
it("rejects runs that reference missing commits", async () => {
const fetchMock = vi
.fn<Fetch>()
.mockResolvedValue(
mirrorResponse({ commits: [], runs: [{ start: 1, count: 1, commit: "abc123" }] })
);
const ctx = makeCtx(fetchMock);
await expect(blameCall(ctx)).rejects.toThrow("unknown blame commit: abc123");
});
it("rejects non-contiguous run tables", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(
mirrorResponse({
commits: [
{
oid: "abc123",
message: "add main",
author: {
name: "alice",
email: "alice@tangled.org",
when: "2026-06-01T12:30:00Z"
}
}
],
runs: [{ start: 2, count: 1, commit: "abc123" }]
})
);
const ctx = makeCtx(fetchMock);
await expect(blameCall(ctx)).rejects.toThrow("invalid blame run at line 2");
});
});
describe("hydrateBlameAuthors", () => {
it("bounds identity lookups for large blame results", async () => {
const blame = Array.from({ length: 300 }, (_, index) => ({
start: index + 1,
count: 1,
commit: {
sha: `sha-${index}`,
subject: `commit ${index}`,
when: "2026-06-01T12:30:00Z",
author: {
name: `author ${index}`,
email: `did:plc:blameauthor${index}`,
did: `did:plc:blameauthor${index}`
}
}
}));
const fetchMock = vi.fn<Fetch>(async (input) => {
const url = new URL(String(input));
const did = url.searchParams.get("identifier")!;
return jsonResponse({ did, handle: `${did.slice("did:plc:".length)}.example` });
});
const ctx = createBobbinClient({ serviceUrl: "https://bobbin.example", fetch: fetchMock });
const hydrated = await hydrateBlameAuthors(blame, ctx);
expect(fetchMock).toHaveBeenCalledTimes(250);
expect(hydrated[249].commit.author.handle).toBe("blameauthor249.example");
expect(hydrated[250].commit.author.handle).toBeUndefined();
});
});