Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386import { describe, expect, it, vi } from "vitest";import { coAuthorsFrom, importState, repoNameOf, resolveForkRepoLabels, resolveRepoByName, resolveRepoByRepoDid, sortTreeEntries, toBranchSummary, toTagSummary, toTreeEntrySummary, treeEntryKind, type TreeEntrySummary} from "$lib/api/repo";import { ClientResponseError, createBobbinClient, type BobbinContext } from "$lib/api/client";import type { RecordView, RepoRecord } from "$lib/api/records";
const jsonResponse = (body: unknown): Response => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
describe("repoNameOf", () => { const view = (uri: string, name?: string): RecordView<RepoRecord> => ({ uri: uri as RecordView<RepoRecord>["uri"], value: { $type: "sh.tangled.repo", createdAt: "", knot: "knot.test", name } });
it("prefers the record's cosmetic name over a tid rkey", () => { expect(repoNameOf(view("at://did:plc:o/sh.tangled.repo/3lzg6enurmo22", "infra"))).toBe( "infra" ); });
it("falls back to the rkey for unnamed records", () => { expect(repoNameOf(view("at://did:plc:o/sh.tangled.repo/infra"))).toBe("infra"); });});
describe("treeEntryKind", () => { it("maps git file modes to entry kinds", () => { expect(treeEntryKind("0040000")).toBe("directory"); expect(treeEntryKind("040000")).toBe("directory"); expect(treeEntryKind("0100644")).toBe("file"); expect(treeEntryKind("0100755")).toBe("file"); expect(treeEntryKind("0120000")).toBe("symlink"); expect(treeEntryKind("0160000")).toBe("submodule"); });});
describe("sortTreeEntries", () => { it("puts directories and submodules before files, then sorts by name", () => { const entry = (name: string, kind: TreeEntrySummary["kind"]): TreeEntrySummary => ({ name, kind, size: 0 }); const sorted = sortTreeEntries([ entry("readme.md", "file"), entry("src", "directory"), entry(".gitignore", "file"), entry("vendor", "submodule") ]); expect(sorted.map((item) => item.name)).toEqual([ "src", "vendor", ".gitignore", "readme.md" ]); });});
describe("coAuthorsFrom", () => { it("stamps every co-author with the passed When, like go's Commit.CoAuthors", () => { const message = "subject\n\nCo-authored-by: Ada Lovelace <ada@example.com>\nCo-authored-by: Alan Turing <alan@example.com>\n"; expect(coAuthorsFrom(message, "2026-07-02T10:00:00Z")).toEqual([ { Name: "Ada Lovelace", Email: "ada@example.com", When: "2026-07-02T10:00:00Z" }, { Name: "Alan Turing", Email: "alan@example.com", When: "2026-07-02T10:00:00Z" } ]); });
it("dedupes by email and matches the trailer case-insensitively", () => { const message = "subject\n\nco-authored-by: Ada <ada@example.com>\nCo-Authored-By: Ada Again <ada@example.com>\n"; expect(coAuthorsFrom(message, "w")).toEqual([ { Name: "Ada", Email: "ada@example.com", When: "w" } ]); });});
describe("importState", () => { const repo = { uri: "at://did:plc:o/sh.tangled.repo/abc", repoDid: "did:plc:repo" }; const describeResponse = (body: Record<string, string>) => jsonResponse({ ownerDid: "did:plc:o", repoDid: repo.repoDid, rkey: "abc", ...body });
it("sends the at-uri for bobbin's proxy and repoDid for the knot", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(describeResponse({})); await importState(makeCtx(fetchMock), repo); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/sh.tangled.repo.describeRepo"); expect(url.searchParams.get("repo")).toBe(repo.uri); expect(url.searchParams.get("repoDid")).toBe(repo.repoDid); });
it("keeps in-flight states and the failure reason", async () => { const state = (content: string, reason?: string) => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(describeResponse({ content, reason: reason ?? "" })); return importState(makeCtx(fetchMock), repo); }; expect(await state("fetching")).toEqual({ content: "fetching" }); expect(await state("partial")).toEqual({ content: "partial" }); expect(await state("failed", "clone died")).toEqual({ content: "failed", reason: "clone died" }); });
it("reports nothing for present content or a record with no repoDid", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(describeResponse({ content: "present" })); expect(await importState(makeCtx(fetchMock), repo)).toBeNull(); expect(await importState(makeCtx(fetchMock), { uri: repo.uri })).toBeNull(); });});
describe("toBranchSummary", () => { it("reads the tip commit and marks the default branch", () => { const branch = { name: "master", oid: "ff3a3678", message: "the tip commit", author: { name: "Ada", email: "a@b.c", when: "2026-07-01T10:00:00Z" as const }, committer: { name: "Ada", email: "a@b.c", when: "2026-07-02T10:00:00Z" as const } }; expect(toBranchSummary(branch, "master")).toEqual({ name: "master", hash: "ff3a3678", when: "2026-07-02T10:00:00Z", isDefault: true, message: "the tip commit" }); });
it("treats a missing defaultBranch as not default", () => { const branch = { name: "topic", oid: "abc", message: "", author: { name: "Ada", email: "a@b.c", when: "2026-07-02T10:00:00Z" as const }, committer: { name: "Ada", email: "a@b.c", when: "2026-07-02T10:00:00Z" as const } }; expect(toBranchSummary(branch).isDefault).toBe(false); });});
describe("toTagSummary", () => { const author = { name: "Ada", email: "a@b.c", when: "2026-06-30T10:00:00Z" as const };
it("reads the tagger and message from the annotation", () => { expect( toTagSummary({ name: "v1.0.0", oid: "4b4efe25", message: "tip commit", author, annotation: { message: "release", tagger: { name: "Ada", email: "a@b.c", when: "2026-07-01T10:00:00Z" } } }) ).toEqual({ name: "v1.0.0", commitHash: "4b4efe25", when: "2026-07-01T10:00:00Z", message: "release", taggerName: "Ada" }); });
it("a lightweight tag is its own commit", () => { const summary = toTagSummary({ name: "v1.0.0", oid: "eebb477b", message: "", author }); expect(summary.commitHash).toBe("eebb477b"); });});
describe("toTreeEntrySummary", () => { it("carries the last commit over from snake_case", () => { expect( toTreeEntrySummary({ name: "flake.nix", mode: "0100644", size: 213, last_commit: { hash: "f0b11b85", message: "init", when: "2026-07-01T10:00:00Z" } }) ).toEqual({ name: "flake.nix", kind: "file", size: 213, lastCommitHash: "f0b11b85", lastCommitWhen: "2026-07-01T10:00:00Z", lastCommitMessage: "init" }); });});
describe("resolveRepoByName", () => { const owner = "did:plc:owner";
it("asks bobbin for the owner and name", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( jsonResponse({ uri: `at://${owner}/sh.tangled.repo/3lzg6enurmo22`, value: { $type: "sh.tangled.repo", knot: "knot.test", name: "infra" } }) ); const view = await resolveRepoByName(makeCtx(fetchMock), owner, "infra"); expect(view?.uri).toBe(`at://${owner}/sh.tangled.repo/3lzg6enurmo22`); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/sh.tangled.repo.getRepoByName"); expect(url.searchParams.get("owner")).toBe(owner); expect(url.searchParams.get("name")).toBe("infra"); });
it("returns null when the owner has no such repo", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ error: "RecordNotFound" }), { status: 404, headers: { "content-type": "application/json" } }) ); expect(await resolveRepoByName(makeCtx(fetchMock), owner, "missing")).toBeNull(); });
it("propagates anything that is not a miss", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ error: "UpstreamFailed" }), { status: 502, headers: { "content-type": "application/json" } }) ); await expect(resolveRepoByName(makeCtx(fetchMock), owner, "infra")).rejects.toBeInstanceOf( ClientResponseError ); });});
describe("resolveForkRepoLabels", () => { const enrichReply = jsonResponse({ output: { items: [ { uri: "at://did:plc:forkowner/sh.tangled.repo/core", value: { $type: "sh.tangled.repo", knot: "knot.test", name: "core", repoDid: "did:plc:forkrepo" } } ] }, data: { "did:plc:forkowner": { "sh.tangled.repo:.repo": { "blue.microcosm.identity.resolveMiniDoc": { handle: "fork.example" } } } } });
it("labels each fork did as owner/name from the enrich sidecar", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(enrichReply); const labels = await resolveForkRepoLabels(makeCtx(fetchMock), ["did:plc:forkrepo"]); expect(labels).toEqual({ "did:plc:forkrepo": "fork.example/core" });
const [url, request] = fetchMock.mock.calls[0]; expect(String(url)).toContain("/xrpc/sh.tangled.query.enrichResponse"); expect(JSON.parse(String(request?.body))).toMatchObject({ xrpc: "sh.tangled.repo.getReposByRepoDids", params: { dids: ["did:plc:forkrepo"] } }); });
it("skips dids bobbin has no repo for", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(jsonResponse({ output: { items: [] }, data: {} })); expect(await resolveForkRepoLabels(makeCtx(fetchMock), ["did:plc:gone"])).toEqual({}); });
it("does not ask when there is nothing to label", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>(); expect(await resolveForkRepoLabels(makeCtx(fetchMock), [])).toEqual({}); expect(fetchMock).not.toHaveBeenCalled(); });});
describe("resolveRepoByRepoDid", () => { const enrichReply = jsonResponse({ output: { uri: "at://did:plc:owner/sh.tangled.repo/core", value: { $type: "sh.tangled.repo", knot: "knot.test", name: "core", repoDid: "did:plc:j5hmlfdrwkvtxm7cjmu7j2is" } }, data: { "did:plc:owner": { "sh.tangled.repo:.repo": { "blue.microcosm.identity.resolveMiniDoc": { did: "did:plc:owner", handle: "tangled.org" } } } } });
it("resolves owner handle and repo name from repo did", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(enrichReply); const result = await resolveRepoByRepoDid( makeCtx(fetchMock), "did:plc:j5hmlfdrwkvtxm7cjmu7j2is" ); expect(result).toEqual({ owner: "tangled.org", name: "core" });
const [url, request] = fetchMock.mock.calls[0]; expect(String(url)).toContain("/xrpc/sh.tangled.query.enrichResponse"); expect(JSON.parse(String(request?.body))).toMatchObject({ xrpc: "sh.tangled.repo.getRepoByRepoDid", params: { repoDid: "did:plc:j5hmlfdrwkvtxm7cjmu7j2is" } }); });
it("falls back to owner did when handle is missing or .invalid", async () => { const enrichReplyNoHandle = jsonResponse({ output: { uri: "at://did:plc:owner/sh.tangled.repo/core", value: { $type: "sh.tangled.repo", knot: "knot.test", name: "core", repoDid: "did:plc:j5hmlfdrwkvtxm7cjmu7j2is" } }, data: { "did:plc:owner": { "sh.tangled.repo:.repo": { "blue.microcosm.identity.resolveMiniDoc": { did: "did:plc:owner", handle: "handle.invalid" } } } } }); const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(enrichReplyNoHandle); const result = await resolveRepoByRepoDid( makeCtx(fetchMock), "did:plc:j5hmlfdrwkvtxm7cjmu7j2is" ); expect(result).toEqual({ owner: "did:plc:owner", name: "core" }); });
it("returns null when bobbin has no such repo", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ error: "RecordNotFound" }), { status: 404, headers: { "content-type": "application/json" } }) ); expect(await resolveRepoByRepoDid(makeCtx(fetchMock), "did:plc:missing")).toBeNull(); });});