import type { Did } from "@atcute/lexicons/syntax"; import { describe, expect, it, vi } from "vitest"; import { blameFor, hydrateBlameAuthors } from "$lib/api/blame"; import { createBobbinClient } from "$lib/api/client"; 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>; runs: Array>; }): Response => jsonResponse({ commit: "abc123", ...body }); const makeCtx = (fetchMock: typeof globalThis.fetch) => createBobbinClient({ serviceUrl: "https://bobbin.example", fetch: fetchMock }); const blameCall = (ctx: ReturnType) => blameFor(ctx, REPO_DID, "main", "src/main.rs"); describe("blameFor", () => { it("requests gitmirror getBlame and maps its one-based runs", async () => { const fetchMock = vi.fn().mockResolvedValue( mirrorResponse({ commits: [ { oid: "abc123", message: "add main\n\nlonger explanation", author: { name: "dawn", email: "did:plc:dawn", 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: "dawn", email: "did:plc:dawn", did: "did:plc:dawn" } } } ]); 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().mockResolvedValue( mirrorResponse({ commits: [ { oid: "abc123", message: "subject line\n\nbody", author: { name: "dawn", email: "dawn@example.com", 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() .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: "dawn", email: "dawn@example.com", when: "2026-06-01T12:30:00Z" } }; const fetchMock = vi.fn().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() .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().mockResolvedValue( mirrorResponse({ commits: [ { oid: "abc123", message: "add main", author: { name: "dawn", email: "dawn@example.com", 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(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(); }); });