Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733import { describe, expect, it, vi } from "vitest";import { createBobbinClient } from "#lib/api/client.ts";import { TYPE_MINIDOC, TYPE_REPO_VIEW } from "#lib/api/enrich.ts";import { fetchPeoplePage, fetchForksPage, fetchPinned, fetchProfileActivity, fetchReposPage, fetchStarredPage} from "#lib/components/profile/pages.ts";const enriched = () => Response.json({ output: { items: [], hits: [] }, data: {} });
const bobDoc = { did: "did:plc:bob", handle: "bob.test" };
const star = (id: string, subject: unknown) => ({ uri: `at://did:plc:alice/org.tangled.feed.star/${id}`, value: { subject, createdAt: "2026-08-03T00:00:00Z" }});
const stringSubject = (uri: string) => ({ $type: "org.tangled.feed.star#record", record: { uri }});
const stringRecord = (name: string, description: string, size: number, delay = 0) => ({ delay, value: { snippet: { name, content: { $type: "blob", ref: { $link: "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku" }, size, mimeType: "text/plain" } }, description, createdAt: "2026-08-02T00:00:00Z" }});
interface StarredFixture { stars: unknown[]; // a string uri without an entry answers 404 strings?: Record<string, ReturnType<typeof stringRecord>>; repos?: unknown[]; data?: Record<string, unknown>;}
const starredContext = ({ stars, strings = {}, repos = [], data = {} }: StarredFixture) => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockImplementation(async (input, init) => { const url = new URL(String(input instanceof Request ? input.url : input)); if (url.pathname === "/xrpc/org.tangled.string.getString") { const uri = url.searchParams.get("uri") ?? ""; const found = strings[uri]; if (!found) return new Response("gone", { status: 404 }); await new Promise((resolve) => setTimeout(resolve, found.delay)); return Response.json({ uri, value: found.value }); } const { xrpc } = JSON.parse(String(init?.body)) as { xrpc: string }; if (xrpc === "org.tangled.repo.getReposByRepoDids") return Response.json({ output: { items: repos }, data: {} }); return Response.json({ output: { items: stars }, data }); }); return createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });};
const requestBody = (fetchMock: ReturnType<typeof vi.fn>, index = 0) => { const init = fetchMock.mock.calls[index][1] as RequestInit; return JSON.parse(String(init.body)) as { xrpc: string; enrich: { source: string; type: string; targets?: string[] }[]; };};
describe("fetchStarredPage", () => { it("only requests minidocs for string URI subjects", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ output: { items: [] }, data: {} }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
await fetchStarredPage(ctx, { did: "did:plc:alice" });
const body = requestBody(fetchMock); expect(body.enrich[0].targets).toEqual(["items[].value.subject.record.uri"]); });
it("hydrates starred strings into string cards", async () => { const subject = "at://did:plc:bob/org.tangled.snippet.string/3kstr"; const ctx = starredContext({ stars: [star("one", stringSubject(subject))], strings: { [subject]: stringRecord("hello.rs", "says hi", 2048) }, data: { "did:plc:bob": { "org.tangled.feed.star:subject": { [TYPE_MINIDOC]: bobDoc } } } });
const page = await fetchStarredPage(ctx, { did: "did:plc:alice" });
expect(page.items).toMatchObject([ { kind: "string", uri: "at://did:plc:alice/org.tangled.feed.star/one", string: { rkey: "3kstr", ownerHandle: "bob.test", filename: "hello.rs", description: "says hi", createdAt: "2026-08-02T00:00:00Z" } } ]); expect(page.items[0]).toHaveProperty("string.sizeLabel", "2.0 kB"); });
it("drops a starred string that can't be fetched and keeps its neighbours in order", async () => { const before = "at://did:plc:bob/org.tangled.snippet.string/3kbefore"; const gone = "at://did:plc:bob/org.tangled.snippet.string/3kgone"; const after = "at://did:plc:bob/org.tangled.snippet.string/3kafter"; const ctx = starredContext({ stars: [ star("a", stringSubject(before)), star("b", stringSubject(gone)), star("c", { $type: "org.tangled.feed.star#repo", identity: "did:plc:repo" }), star("d", stringSubject(after)) ], strings: { [before]: stringRecord("before.rs", "kept", 10), [after]: stringRecord("after.rs", "kept too", 10) }, repos: [ { uri: "at://did:plc:repo/org.tangled.repo.manifest/self", value: { declaration: { owner: "did:plc:bob", name: "reef" }, createdAt: "2026-08-01T00:00:00Z" } } ] });
const page = await fetchStarredPage(ctx, { did: "did:plc:alice" });
expect(page.items.map((item) => item.uri.split("/").at(-1))).toEqual(["a", "c", "d"]); expect(page.items.map((item) => item.kind)).toEqual(["string", "repo", "string"]); });
it("links a starred string to its owner's did when bobbin has no handle for them", async () => { const subject = "at://did:plc:bob/org.tangled.snippet.string/3kstr"; const ctx = starredContext({ stars: [star("one", stringSubject(subject))], strings: { [subject]: stringRecord("hello.rs", "says hi", 2048) }, data: { "did:plc:bob": { "org.tangled.feed.star:subject": { [TYPE_MINIDOC]: { did: "did:plc:bob", handle: "handle.invalid" } } } } });
const page = await fetchStarredPage(ctx, { did: "did:plc:alice" });
expect(page.items).toMatchObject([{ string: { ownerHandle: "did:plc:bob" } }]); });
it("keeps repo and string stars in star order, each string with its own record", async () => { const first = "at://did:plc:bob/org.tangled.snippet.string/3kfirst"; const second = "at://did:plc:bob/org.tangled.snippet.string/3ksecond"; const ctx = starredContext({ stars: [ star("a", stringSubject(first)), star("b", { $type: "org.tangled.feed.star#repo", identity: "did:plc:repo" }), star("c", stringSubject(second)) ], // the first string answers last, so a response matched by arrival would swap them strings: { [first]: stringRecord("first.rs", "the first", 10, 30), [second]: stringRecord("second.rs", "the second", 10) }, repos: [ { uri: "at://did:plc:repo/org.tangled.repo.manifest/self", value: { declaration: { owner: "did:plc:bob", name: "reef" }, createdAt: "2026-08-01T00:00:00Z" } } ], data: { "did:plc:bob": { "org.tangled.feed.star:subject": { [TYPE_MINIDOC]: bobDoc } } } });
const page = await fetchStarredPage(ctx, { did: "did:plc:alice" });
expect(page.items.map((item) => item.kind)).toEqual(["string", "repo", "string"]); expect(page.items).toMatchObject([ { string: { rkey: "3kfirst", filename: "first.rs", description: "the first" } }, { repo: { name: "reef", repoDid: "did:plc:repo" } }, { string: { rkey: "3ksecond", filename: "second.rs", description: "the second" } } ]); });
it("targets repo views and owner minidocs independently", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValueOnce( Response.json({ output: { items: [ { uri: "at://did:plc:alice/org.tangled.feed.star/one", value: { subject: { $type: "org.tangled.feed.star#repo", identity: "did:plc:repo" }, createdAt: "2026-08-01T00:00:00Z" } } ] }, data: {} }) ) .mockResolvedValueOnce(Response.json({ output: { items: [] }, data: {} })); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
await fetchStarredPage(ctx, { did: "did:plc:alice" });
const body = requestBody(fetchMock, 1); expect(body.enrich).toEqual([ { source: "org.tangled.repo.manifest:.", type: TYPE_REPO_VIEW, targets: ["items[].uri"] }, { source: "org.tangled.repo.manifest:.repo", type: "blue.microcosm.identity.resolveMiniDoc", targets: ["items[].value.declaration.owner"] } ]); });
it.each([ [undefined, "items[].uri"], ["needle", "hits[].uri"] ])("targets repo views for list/search results", async (q, expectedTarget) => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(enriched()); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
await fetchReposPage(ctx, { did: "did:plc:alice", handle: "alice.test", viewerDid: "did:plc:viewer", q });
const body = requestBody(fetchMock); expect(body.enrich).toEqual([ { source: "org.tangled.repo.manifest:.", type: TYPE_REPO_VIEW, targets: [expectedTarget] } ]); });
it.each([ [{ star: "at://did:plc:viewer/org.tangled.feed.star/abc" }, "abc"], [{}, null], [undefined, undefined] ])("fills repo cards from hydrated views", async (viewer, viewerStarRkey) => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ output: { items: [ { uri: "at://did:plc:reef/org.tangled.repo.manifest/self", value: { declaration: { owner: "did:plc:alice", name: "reef" }, createdAt: "2026-08-01T00:00:00Z" } } ] }, data: { "at://did:plc:reef/org.tangled.repo.manifest/self": { "org.tangled.repo.manifest:.": { [TYPE_REPO_VIEW]: { did: "did:plc:reef", slug: "reef", owner: { did: "did:plc:alice", handle: "handle.invalid" }, createdAt: "2026-08-01T00:00:00Z", starCount: 4, forkCount: 2, openIssueCount: 3, openPullCount: 1, primaryLanguage: "Rust", viewer } } } } }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchReposPage(ctx, { did: "did:plc:alice", handle: "alice.test" });
expect(page.items[0]).toMatchObject({ name: "reef", ownerHandle: "alice.test", stars: 4, forks: 2, issues: 3, pulls: 1, language: "Rust" }); expect(page.items[0].viewerStarRkey).toBe(viewerStarRkey); });
it.each([ ["followers" as const, "items[].uri"], ["following" as const, "items[].subject"] ])("targets %s identities and stats", async (direction, expectedTarget) => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(enriched()); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
await fetchPeoplePage(ctx, { did: "did:plc:alice", viewerDid: "did:plc:viewer", direction });
const body = requestBody(fetchMock); expect(body.enrich).toHaveLength(4); expect(body.enrich.every((descriptor) => descriptor.targets?.[0] === expectedTarget)).toBe( true ); });
it("does not retry missing enriched identities", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ output: { items: [ { uri: "at://did:plc:bob/org.tangled.graph.follow/did:plc:alice", subject: "did:plc:alice", value: { createdAt: "2026-08-01T00:00:00Z" } } ] }, data: {} }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchPeoplePage(ctx, { did: "did:plc:alice", direction: "followers" });
expect(fetchMock).toHaveBeenCalledTimes(2); expect(page.items[0].handle).toBe("handle.invalid"); });
it("tells people with an avatar from people without one", async () => { const follows = ["alice", "bob"].map((name) => ({ uri: `at://did:plc:${name}/org.tangled.graph.follow/did:plc:me`, subject: "did:plc:me", value: { createdAt: "2026-08-01T00:00:00Z" } })); const picture = { $type: "blob", ref: { $link: "bafkreigh2akiscaildc7gnvtklbsfhdgwz72eolmpckbqr5ej26byp3uli" }, mimeType: "image/png", size: 10 }; const fetchMock = vi.fn<typeof globalThis.fetch>().mockImplementation(async (input) => { const url = new URL(String(input instanceof Request ? input.url : input)); if (url.pathname !== "/xrpc/org.tangled.actor.getProfiles") { return Response.json({ output: { items: follows }, data: {} }); } return Response.json({ items: [ { uri: "at://did:plc:alice/org.tangled.actor.profile/self", value: { avatar: picture } }, { uri: "at://did:plc:bob/org.tangled.actor.profile/self", value: {} } ] }); }); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchPeoplePage(ctx, { did: "did:plc:me", direction: "followers" });
expect(page.items.map((person) => person.avatarCid)).toEqual([picture.ref.$link, null]); });
it("leaves the avatar unknown when the profile records could not be read", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockImplementation(async (input) => { const url = new URL(String(input instanceof Request ? input.url : input)); if (url.pathname === "/xrpc/org.tangled.actor.getProfiles") { return new Response("down", { status: 500 }); } return Response.json({ output: { items: [ { uri: "at://did:plc:alice/org.tangled.graph.follow/did:plc:me", value: { createdAt: "2026-08-01T00:00:00Z" } } ] }, data: {} }); }); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchPeoplePage(ctx, { did: "did:plc:me", direction: "followers" });
expect(page.items[0].avatarCid).toBeUndefined(); });
it("targets repo dids for both pinned repo queries", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockImplementation(async () => enriched()); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
await fetchPinned(ctx, { keys: ["did:plc:repo", "at://did:plc:alice/sh.tangled.repo/example"], did: "did:plc:alice", handle: "alice.test", viewerDid: "did:plc:viewer" });
const bodies = fetchMock.mock.calls.map((_, index) => requestBody(fetchMock, index)); expect(bodies.map((body) => body.xrpc).sort()).toEqual([ "org.tangled.repo.getRepos", "org.tangled.repo.getReposByRepoDids" ]); expect( bodies.every((body) => body.enrich.every((descriptor) => descriptor.type === TYPE_MINIDOC ? descriptor.targets?.[0] === "items[].value.declaration.owner" : descriptor.targets?.[0] === "items[].uri" ) ) ).toBe(true); });
it("deduplicates repos returned by fetchReposPage by repoKey", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ output: { items: [ { uri: "at://did:plc:repo-one/org.tangled.repo.manifest/self", value: { declaration: { owner: "did:plc:alice", name: "repo-one" }, createdAt: "2026-08-01T00:00:00Z" } }, { uri: "at://did:plc:repo-one/org.tangled.repo.manifest/self", value: { declaration: { owner: "did:plc:alice", name: "repo-one-dup" }, createdAt: "2026-08-02T00:00:00Z" } }, { uri: "at://did:plc:repo-two/org.tangled.repo.manifest/self", value: { declaration: { owner: "did:plc:alice", name: "repo-two" }, createdAt: "2026-08-03T00:00:00Z" } } ] }, data: {} }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const result = await fetchReposPage(ctx, { did: "did:plc:alice", handle: "alice.test", viewerDid: "did:plc:viewer" });
expect(result.items).toHaveLength(2); expect(result.items[0].repoDid).toBe("did:plc:repo-one"); expect(result.items[0].name).toBe("repo-one-dup"); expect(result.items[1].repoDid).toBe("did:plc:repo-two"); });});
describe("fetchProfileActivity", () => { it("maps one hydrated Bobbin response into the profile view model", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ months: [ { start: "2026-09-01T00:00:00Z", commits: 3, repos: [ { kind: "created", repo: { uri: "at://did:plc:alice/sh.tangled.repo/current", did: "did:plc:current", ownerDid: "did:plc:alice", ownerHandle: "handle.invalid", name: "current" } } ], issues: [ { uri: "at://did:plc:alice/sh.tangled.repo.issue/open", title: "Open issue", state: "open", repo: { uri: "at://did:plc:bob/sh.tangled.repo/target", did: "did:plc:target", ownerDid: "did:plc:bob", ownerHandle: "handle.invalid", name: "target" } } ], pulls: [ { uri: "at://did:plc:alice/sh.tangled.repo.pull/merged", title: "Merged pull", state: "merged", repo: { uri: "at://did:plc:bob/sh.tangled.repo/target", did: "did:plc:target", ownerDid: "did:plc:bob", ownerHandle: "bob.test", name: "target" } } ] }, { start: "2026-08-01T00:00:00Z", commits: 0, repos: [ { kind: "forked", repo: { uri: "at://did:plc:alice/sh.tangled.repo/fork", did: "did:plc:fork", ownerDid: "did:plc:alice", ownerHandle: "alice.test", name: "fork" } } ], issues: [], pulls: [] }, { start: "2026-03-01T00:00:00Z", commits: 0, repos: [], issues: [ { uri: "at://did:plc:alice/sh.tangled.repo.issue/closed", title: "Closed issue", state: "closed", repo: { uri: "at://did:plc:bob/sh.tangled.repo/target", did: "did:plc:target", ownerDid: "did:plc:bob", ownerHandle: "bob.test", name: "target" } } ], pulls: [] } ], truncated: true }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const activity = await fetchProfileActivity(ctx, { did: "did:plc:alice", handle: "alice.test", now: new Date("2026-09-01T12:00:00Z") });
expect(fetchMock).toHaveBeenCalledTimes(1); const request = fetchMock.mock.calls[0][0]; const requestUrl = new URL(request instanceof Request ? request.url : String(request)); expect(requestUrl.pathname).toBe("/xrpc/org.tangled.actor.getProfileActivity"); expect(requestUrl.searchParams.get("actor")).toBe("did:plc:alice"); expect(requestUrl.searchParams.get("months")).toBe("7"); expect(activity.months.map((month) => month.monthsAgo)).toEqual([0, 1, 6]); expect(activity.truncated).toBe(true); expect(activity.months[0].commits).toBe(3); expect(activity.months[0].repos).toMatchObject([ { name: "current", ownerHandle: "alice.test", forked: false } ]); expect(activity.months[1].repos).toMatchObject([{ name: "fork", forked: true }]); expect(activity.months[0].issues).toMatchObject([ { rkey: "open", state: "open", repo: { repoDid: "did:plc:target", ownerHandle: "did:plc:bob", name: "target" } } ]); expect(activity.months[0].pulls).toMatchObject([{ rkey: "merged", state: "merged" }]); expect(activity.months[2].issues).toMatchObject([{ rkey: "closed", state: "closed" }]); });});
describe("fetchForksPage", () => { const forkUri = "at://did:plc:fork/org.tangled.repo.manifest/self";
it("lists forks of a repo did with the fork owner's handle and counts", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ output: { items: [ { uri: forkUri, value: { declaration: { owner: "did:plc:bob", name: "reef" }, createdAt: "2026-08-01T00:00:00Z" } } ], cursor: "next", total: 41 }, data: { "did:plc:bob": { "org.tangled.repo.manifest:.repo": { [TYPE_MINIDOC]: { did: "did:plc:bob", handle: "bob.test" } } }, [forkUri]: { "org.tangled.repo.manifest:.": { [TYPE_REPO_VIEW]: { did: "did:plc:fork", slug: "reef", owner: { did: "did:plc:bob", handle: "bob.test" }, createdAt: "2026-08-01T00:00:00Z", starCount: 2, forkCount: 0, openIssueCount: 0, openPullCount: 0 } } } } }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchForksPage(ctx, { repoDid: "did:plc:source", offset: 30 });
const body = requestBody(fetchMock) as ReturnType<typeof requestBody> & { params: Record<string, unknown>; }; expect(body.xrpc).toBe("org.tangled.repo.listForks"); expect(body.params).toMatchObject({ subject: "did:plc:source", offset: 30 }); expect(body.enrich.map((descriptor) => descriptor.targets?.[0])).toEqual([ "items[].uri", "items[].value.declaration.owner" ]); expect(page.total).toBe(41); expect(page.cursor).toBe("next"); expect(page.items).toMatchObject([ { name: "reef", repoDid: "did:plc:fork", ownerHandle: "bob.test", stars: 2 } ]); });
it("names a fork's owner by did when bobbin has no handle for them", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( Response.json({ output: { items: [ { uri: forkUri, value: { declaration: { owner: "did:plc:bob", name: "reef" }, createdAt: "2026-08-01T00:00:00Z" } } ] }, data: { "did:plc:bob": { "org.tangled.repo.manifest:.repo": { [TYPE_MINIDOC]: { did: "did:plc:bob", handle: "handle.invalid" } } } } }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchForksPage(ctx, { repoDid: "did:plc:source" });
expect(page.items).toMatchObject([{ ownerHandle: "did:plc:bob" }]); });
it("is empty for a repo nobody forked", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(Response.json({ output: { items: [], total: 0 }, data: {} })); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const page = await fetchForksPage(ctx, { repoDid: "did:plc:source" });
expect(page).toEqual({ items: [], cursor: undefined, total: 0 }); });});