import { describe, expect, it, vi } from "vitest";
import { ClientResponseError } from "#lib/api/client.ts";
import {
classifyRepoAvailability,
withAuthorHandles,
withImportAvailability,
type RepoAvailability
} from "#lib/api/repoIndex.ts";
import type { ImportState } from "#lib/api/repo.ts";
import { sshFingerprint } from "#lib/api/settings.ts";
import type { CommitSummary } from "#lib/components/repo/types.ts";
import type { Fetch } from "#lib/fetch.ts";
const notSynced = () => new ClientResponseError({ status: 404, data: { error: "RepoNotFound" } });
describe("classifyRepoAvailability", () => {
it("recognizes a repo the mirror has not synced yet", () => {
const result = classifyRepoAvailability(
[notSynced(), notSynced(), notSynced()].map((cause) => ({
value: null,
error: cause
}))
);
expect(result).toBe("not-synced");
});
it("does not treat other 404s as not synced", () => {
const result = classifyRepoAvailability(
["tree", "log", "branches"].map(() => ({
value: null,
error: new ClientResponseError({ status: 404, data: { error: "XRPCNotSupported" } })
}))
);
expect(result).toBe("error");
});
it("keeps transport failures as errors", () => {
const result = classifyRepoAvailability(
["tree", "log", "branches"].map(() => ({
value: null,
error: new TypeError("offline")
}))
);
expect(result).toBe("error");
});
it("does not mistake a missing ref for an unsynced repo", () => {
const result = classifyRepoAvailability([
{ value: null, error: notSynced() },
{ value: null, error: notSynced() },
{ 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 }, "ok"],
["not-synced", null, "not-synced"],
["not-synced", { content: "fetching" as const }, "importing"],
["not-synced", { content: "partial" as const }, "importing"],
["not-synced", { content: "failed" as const }, "import-failed"],
["empty", null, "empty"],
["error", null, "error"],
["empty", { content: "failed" as const }, "import-failed"],
["error", { content: "failed" as const }, "import-failed"],
["empty", { content: "fetching" as const }, "importing"],
["empty", { content: "partial" as const }, "importing"],
["error", { content: "fetching" as const }, "importing"],
["error", { 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>): 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 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 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 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 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 Fetch
);
expect(out[0]).toMatchObject({ authorDid: "did:plc:alice" });
expect(out[0].authorHandle).toBeUndefined();
});
});
describe("withAuthorHandles signatures", () => {
const signKey =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEisipfUi+zRyW1reo72Rt4+EUbM5mKZrI9/PkyHMpR";
const authKey =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKTt0Ub2mIVQbZG0DL1fB6vk1PB8iI5bmNTe7IfBnMO";
const keysFetch = vi.fn(async (input: string | URL, init?: RequestInit) => {
if (!String(input).includes("org.tangled.publicKey.listKeys"))
return fetchMock(input, init);
const subject = new URL(String(input)).searchParams.get("subject");
const items =
subject === "did:plc:alice"
? [
{ value: { publicKey: signKey } },
{ value: { publicKey: authKey, usages: ["auth"] } }
].map((item, i) => ({
uri: `at://${subject}/org.tangled.key.sshKey/${i}`,
...item
}))
: [];
return new Response(JSON.stringify({ items }));
});
const verifiedFor = async (
committerEmail: string,
key: string | null,
authorEmail = "bob@example.com"
) => {
const [out] = await withAuthorHandles(
[commit({ authorEmail, committerEmail, signatureFingerprint: key ?? undefined })],
"https://bobbin.test",
keysFetch as Fetch
);
return out;
};
it("verifies a signature made with one of the committer's signing keys", async () => {
const out = await verifiedFor("alice@example.com", await sshFingerprint(signKey));
expect(out).toMatchObject({
authorDid: "did:plc:bob",
committerDid: "did:plc:alice",
committerHandle: "alice.test",
signature: "verified"
});
});
it("still maps the author when the signature does not verify", async () => {
for (const key of ["SHA256:someoneelse", await sshFingerprint(authKey)]) {
const out = await verifiedFor("alice@example.com", key);
expect(out).toMatchObject({
authorDid: "did:plc:bob",
committerDid: "did:plc:alice",
signature: "unverified"
});
}
const notTheirs = await verifiedFor("bob@example.com", await sshFingerprint(signKey));
expect(notTheirs.signature).toBe("unverified");
});
it("leaves unsigned commits without a signature state", async () => {
const out = await verifiedFor("alice@example.com", null);
expect(out).toMatchObject({
authorDid: "did:plc:bob",
committerDid: "did:plc:alice",
committerHandle: "alice.test"
});
expect(out.signature).toBeUndefined();
});
});