import { 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";
import type { Fetch } from "#lib/fetch.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<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("fills a page past stars whose repo is gone", async () => {
const ids = Array.from({ length: 70 }, (_, index) => `r${index}`);
const gone = new Set(["r5", "r40"]);
const fetchMock = vi.fn<Fetch>().mockImplementation(async (_, init) => {
const { xrpc, params } = JSON.parse(String(init?.body)) as {
xrpc: string;
params: { offset?: number; limit: number; dids?: string[] };
};
if (xrpc === "org.tangled.repo.getReposByRepoDids") {
const items = (params.dids ?? [])
.filter((did) => !gone.has(did.split(":").at(-1) ?? ""))
.map((did) => ({
uri: `at://${did}/org.tangled.repo.manifest/self`,
value: {
declaration: { owner: "did:plc:bob", name: did },
createdAt: "2026-08-01T00:00:00Z"
}
}));
return Response.json({ output: { items }, data: {} });
}
const offset = params.offset ?? 0;
const items = ids
.slice(offset, offset + params.limit)
.map((id) =>
star(id, { $type: "org.tangled.feed.star#repo", identity: `did:plc:${id}` })
);
return Response.json({ output: { items }, data: {} });
});
const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const first = await fetchStarredPage(ctx, { did: "did:plc:alice", total: 70 });
const second = await fetchStarredPage(ctx, { did: "did:plc:alice", page: 2, total: 70 });
const third = await fetchStarredPage(ctx, { did: "did:plc:alice", page: 3, total: 70 });
const live = ids.filter((id) => !gone.has(id));
const rkeys = (page: typeof first) => page.items.map((item) => item.uri.split("/").at(-1));
expect(rkeys(first)).toEqual(live.slice(0, 30));
expect(rkeys(second)).toEqual(live.slice(30, 60));
expect(rkeys(third)).toEqual(live.slice(60));
expect(third.dropped).toBe(2);
const calls = fetchMock.mock.calls.length;
const far = await fetchStarredPage(ctx, {
did: "did:plc:alice",
page: 50,
total: 70
});
expect(far.items).toEqual([]);
expect(fetchMock.mock.calls.length).toBe(calls);
});
it("only requests minidocs for string URI subjects", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(
Response.json({
output: { items: [] },
data: {}
})
);
const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
await fetchStarredPage(ctx, { did: "did:plc:alice", total: 70 });
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", total: 70 });
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", total: 70 });
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", total: 70 });
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", total: 70 });
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<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", total: 70 });
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<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<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<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<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<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<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<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<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<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<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<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<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 });
});
});