import { describe, expect, it, vi } from "vitest"; import { ClientResponseError } from "$lib/api/client"; import { classifyRepoAvailability, withAuthorHandles, withImportAvailability, type RepoAvailability } from "$lib/api/repoIndex"; import type { ImportState } from "$lib/api/repo"; import type { CommitSummary } from "$lib/components/repo/types"; const unsupported = () => new ClientResponseError({ status: 404, data: { error: "XRPCNotSupported" } }); describe("classifyRepoAvailability", () => { it("recognizes a legacy knot that does not expose repository endpoints", () => { const result = classifyRepoAvailability( [unsupported(), unsupported(), unsupported()].map((cause) => ({ value: null, error: cause })) ); expect(result).toBe("needs-upgrade"); }); it("keeps transport failures as unreachable", () => { const result = classifyRepoAvailability( ["tree", "log", "branches"].map(() => ({ value: null, error: new TypeError("offline") })) ); expect(result).toBe("unreachable"); }); it("does not mistake a missing ref for an unavailable knot", () => { const result = classifyRepoAvailability([ { value: null, error: unsupported() }, { value: null, error: unsupported() }, { value: { branches: [] }, error: null } ]); expect(result).toBe("ok"); }); }); describe("withImportAvailability", () => { it.each([ ["ok", { content: "fetching" as const }, "ok"], ["ok", { content: "partial" as const }, "ok"], ["ok", { content: "failed" as const, reason: "err" }, "ok"], ["needs-upgrade", { content: "fetching" as const }, "needs-upgrade"], ["needs-upgrade", { content: "partial" as const }, "needs-upgrade"], ["needs-upgrade", { content: "failed" as const }, "needs-upgrade"], ["empty", null, "empty"], ["unreachable", null, "unreachable"], ["empty", { content: "failed" as const, reason: "clone error" }, "import-failed"], ["unreachable", { content: "failed" as const, reason: "timeout" }, "import-failed"], ["empty", { content: "fetching" as const }, "importing"], ["empty", { content: "partial" as const }, "importing"], ["unreachable", { content: "fetching" as const }, "importing"], ["unreachable", { content: "partial" as const }, "importing"] ] satisfies [RepoAvailability, ImportState | null, RepoAvailability][])( "maps (%s, %j) -> %s", (availability, status, expected) => { expect(withImportAvailability(availability, status)).toBe(expected); } ); }); const commit = (c: Partial): CommitSummary => ({ hash: "h", shortHash: "h", subject: "s", body: "", authorName: "", authorEmail: "", when: "2026-01-01T00:00:00Z", ...c }); // dispatches the two upstreams withAuthorHandles touches: the internal kv // route (relative) and bobbin's resolveMiniDoc. const fetchMock = vi.fn(async (input: string | URL, _init?: RequestInit) => { const url = String(input); if (url.startsWith("/_internal/email-did")) { return new Response( JSON.stringify({ "alice@example.com": "did:plc:alice", "bob@example.com": "did:plc:bob", "MIXED@Example.com": "did:plc:mixed", "ghost@example.com": null }) ); } if (url.includes("blue.microcosm.identity.resolveMiniDoc")) { const did = new URL(url).searchParams.get("identifier") ?? ""; return new Response(JSON.stringify({ did, handle: `${did.replace("did:plc:", "")}.test` })); } return new Response("unexpected", { status: 404 }); }); describe("withAuthorHandles", () => { it("resolves verified emails to a did before hydrating handles", async () => { const commits = [ commit({ authorName: "Alice", authorEmail: "alice@example.com" }), commit({ authorName: "Signed", authorEmail: "did:plc:signed", authorDid: "did:plc:signed" }) ]; const out = await withAuthorHandles( commits, "https://bobbin.test", fetchMock as typeof globalThis.fetch ); expect(out[0]).toMatchObject({ authorDid: "did:plc:alice", authorHandle: "alice.test" }); expect(out[1]).toMatchObject({ authorDid: "did:plc:signed", authorHandle: "signed.test" }); }); it("keeps the requested email case when looking up the kv result", async () => { const out = await withAuthorHandles( [commit({ authorEmail: "MIXED@Example.com" })], "https://bobbin.test", fetchMock as typeof globalThis.fetch ); expect(out[0]).toMatchObject({ authorDid: "did:plc:mixed", authorHandle: "mixed.test" }); }); it("leaves unresolved emails as plain authors", async () => { const out = await withAuthorHandles( [commit({ authorEmail: "ghost@example.com" })], "https://bobbin.test", fetchMock as typeof globalThis.fetch ); expect(out[0].authorDid).toBeUndefined(); expect(out[0].authorHandle).toBeUndefined(); }); it("fails open when the kv route errors", async () => { const broken = vi.fn(async () => new Response("nope", { status: 500 })); const out = await withAuthorHandles( [commit({ authorEmail: "alice@example.com" })], "https://bobbin.test", broken as typeof globalThis.fetch ); expect(out[0].authorDid).toBeUndefined(); }); it("keeps a did from the email pass when handle resolution fails", async () => { const brokenBobbin = vi.fn(async (input: string | URL) => { if (String(input).startsWith("/_internal/email-did")) { return new Response(JSON.stringify({ "alice@example.com": "did:plc:alice" })); } return new Response("nope", { status: 500 }); }); const out = await withAuthorHandles( [commit({ authorEmail: "alice@example.com" })], "https://bobbin.test", brokenBobbin as typeof globalThis.fetch ); expect(out[0]).toMatchObject({ authorDid: "did:plc:alice" }); expect(out[0].authorHandle).toBeUndefined(); }); });