import { describe, expect, it, vi } from "vitest";
import type { Did } from "@atcute/lexicons/syntax";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
const agent = {
sub: "did:plc:alice"
} as unknown as OAuthUserAgent;
interface HarnessOptions {
createError?: Error;
recordError?: Error;
putError?: Error;
readError?: Error;
existing?: { value: Record<string, unknown>; cid?: string | null } | null;
mirrorMisses?: number;
emptyAnswers?: number;
upstreamBranches?: string[];
}
// mocks have to be installed before the module is loaded
const load = async ({
createError,
recordError,
putError,
readError,
existing = null,
mirrorMisses = 0,
emptyAnswers = 0,
upstreamBranches = ["main"]
}: HarnessOptions = {}) => {
vi.resetModules();
const events: string[] = [];
const records: unknown[] = [];
const knotInputs: Record<string, unknown>[] = [];
const knotAgents: OAuthUserAgent[] = [];
const createRecord = vi.fn(async (_agent, _collection, record) => {
events.push("record:create");
if (recordError) throw recordError;
records.push(record);
return { uri: "at://did:plc:alice/sh.tangled.repo/demo", cid: "initial-cid" };
});
const runCalls: Record<string, unknown>[] = [];
const putRecord = vi.fn(async (_agent, _collection, _rkey, record, swapRecord) => {
events.push("record:put");
if (putError) throw putError;
records.push(record);
return {
uri: "at://did:plc:alice/sh.tangled.repo/demo",
cid: "committed-cid",
swapRecord
};
});
const deleteRecord = vi.fn(async () => {
events.push("record:delete");
});
const readRecord = vi.fn(async () => {
if (readError) throw readError;
return existing === null
? null
: {
value: existing.value,
written:
existing.cid === null
? null
: {
uri: "at://did:plc:alice/sh.tangled.repo/demo",
cid: existing.cid ?? "existing-cid"
}
};
});
let misses = mirrorMisses;
let empties = emptyAnswers;
const listBranches = vi.fn(async (_ctx, { repo }: { repo: string }) => {
events.push("mirror:list");
if (repo === "did:plc:upstream")
return { branches: upstreamBranches.map((name) => ({ name })) };
if (misses-- > 0) throw new Error("RepoNotFound");
return { branches: empties-- > 0 ? [] : [{ name: "main" }] };
});
vi.doMock("./knotmirror", () => ({ branches: listBranches }));
vi.doMock("./write", () => ({ createRecord, putRecord, deleteRecord, readRecord }));
vi.doMock("./client", () => ({
createBobbinClient: ({
serviceUrl,
agent: caller
}: {
serviceUrl: string;
agent?: OAuthUserAgent;
}) => ({
xrpc: {
call: async (_schema: unknown, options: { input?: Record<string, unknown> }) => {
if (serviceUrl === "https://bobbin.test") {
events.push("bobbin:await");
return { ok: true, data: { status: "indexed" } };
}
if (caller) knotAgents.push(caller);
if (options.input) {
knotInputs.push(options.input);
runCalls.push(options.input);
}
if (options.input && "repo" in options.input) {
events.push("knot:delete");
return { ok: true, data: null };
}
events.push("knot:create");
if (createError) throw createError;
return { ok: true, data: { did: "did:plc:repo" } };
}
}
})
}));
return {
api: await import("./repoCreate"),
events,
records,
runCalls,
knotInputs,
knotAgents,
createRecord,
putRecord,
deleteRecord,
readRecord,
listBranches
};
};
const input = {
ownerDid: "did:plc:alice" as Did,
ownerHandle: "alice.test",
name: "Demo.git",
description: "a repo",
defaultBranch: "main",
knot: "knot.test",
spindle: "spindle.test"
};
describe("repository creation", () => {
it("uses the organization agent for the PDS record and knot creation", async () => {
const { api, createRecord, knotAgents } = await load();
const org = { sub: "did:plc:org" } as unknown as OAuthUserAgent;
const result = await api.createRepo(org, "https://bobbin.test", {
...input,
ownerDid: "did:plc:org",
ownerHandle: "org.test"
});
expect(createRecord.mock.calls[0][0]).toBe(org);
expect(knotAgents).toEqual([org]);
expect(result.ownerHandle).toBe("org.test");
});
it("rejects an organization owner paired with the controller's session", async () => {
const { api, createRecord } = await load();
await expect(
api.createRepo(agent, "https://bobbin.test", {
...input,
ownerDid: "did:plc:org",
ownerHandle: "org.test"
})
).rejects.toThrow("active account changed");
expect(createRecord).not.toHaveBeenCalled();
});
it("creates the knot repo, then declares its DID under the lowercased name", async () => {
const { api, events, records, knotInputs, createRecord } = await load();
const creation = await api.createRepo(agent, "https://bobbin.test", input);
expect(creation.name).toBe("Demo");
expect(events).toEqual(["knot:create", "record:create", "bobbin:await", "mirror:list"]);
expect(knotInputs[0]).toMatchObject({ name: "Demo", spindle: "did:web:spindle.test" });
expect(records[0]).toMatchObject({
$type: "org.tangled.repo.declaration",
repo: "did:plc:repo"
});
expect(createRecord).toHaveBeenCalledWith(
agent,
"org.tangled.repo.declaration",
expect.anything(),
"demo"
);
});
it("waits for the mirror to know the repo before handing it back", async () => {
const { api, listBranches } = await load({ mirrorMisses: 2 });
const creation = await api.createRepo(agent, "https://bobbin.test", input);
expect(listBranches).toHaveBeenCalledTimes(3);
expect(listBranches.mock.calls[0]).toEqual([
expect.anything(),
{ repo: "did:plc:repo", limit: 1 }
]);
expect(creation.repoDid).toBe("did:plc:repo");
});
it("waits for a fork to have the upstream's branches, not just to exist", async () => {
const { api, listBranches } = await load({ emptyAnswers: 2 });
await api.createRepo(agent, "https://bobbin.test", {
...input,
forkOf: "did:plc:upstream"
});
const forkReads = listBranches.mock.calls.filter(([, { repo }]) => repo === "did:plc:repo");
expect(forkReads).toHaveLength(3);
});
it("doesn't wait for branches on a fork of an empty repo", async () => {
const { api, listBranches } = await load({ emptyAnswers: Infinity, upstreamBranches: [] });
await api.createRepo(agent, "https://bobbin.test", {
...input,
forkOf: "did:plc:upstream"
});
const forkReads = listBranches.mock.calls.filter(([, { repo }]) => repo === "did:plc:repo");
expect(forkReads).toHaveLength(1);
});
it("hands the repo back anyway when the mirror never catches up", async () => {
vi.useFakeTimers();
try {
const { api, events, listBranches } = await load({ mirrorMisses: Infinity });
const creating = api.createRepo(agent, "https://bobbin.test", input);
await vi.advanceTimersByTimeAsync(60_000);
expect((await creating).repoDid).toBe("did:plc:repo");
expect(listBranches.mock.calls.length).toBeGreaterThan(3);
expect(events).not.toContain("knot:delete");
expect(events).not.toContain("record:delete");
} finally {
vi.useRealTimers();
}
});
it("cleans up the knot when the declaration cannot be written", async () => {
const { api, events } = await load({ recordError: new Error("pds refused") });
await expect(api.createRepo(agent, "https://bobbin.test", input)).rejects.toThrow(
"pds refused"
);
expect(events.slice(-1)).toEqual(["knot:delete"]);
});
it("leaves the pds untouched when the knot rejects creation", async () => {
const { api, events } = await load({ createError: new Error("no capacity") });
await expect(api.createRepo(agent, "https://bobbin.test", input)).rejects.toThrow(
"no capacity"
);
expect(events).not.toContain("record:create");
expect(events).not.toContain("knot:delete");
});
it("refuses a name the account already has instead of failing inside the pds", async () => {
const { api, events, readRecord } = await load({
existing: {
value: { $type: "org.tangled.repo.declaration", repo: "did:plc:theirs" }
}
});
await expect(api.createRepo(agent, "https://bobbin.test", input)).rejects.toThrow(
'A repository named "Demo" already exists in your account. Choose another name.'
);
expect(readRecord).toHaveBeenCalledWith(agent, "org.tangled.repo.declaration", "demo");
expect(events).toEqual([]);
});
it("fails immediately when readRecord throws rather than proceeding with blind create", async () => {
const { api } = await load({ readError: new Error("PDS connection dropped") });
await expect(api.createRepo(agent, "https://bobbin.test", input)).rejects.toThrow(
"PDS connection dropped"
);
});
it("passes the fork upstream as a tangled source", async () => {
const { api, knotInputs } = await load();
await api.createRepo(agent, "https://bobbin.test", {
...input,
name: "fork",
forkOf: "did:plc:upstream"
});
expect(knotInputs[0]).toMatchObject({
source: { $type: "org.tangled.repo.defs#sourceTangled", did: "did:plc:upstream" }
});
});
});
describe("repository names", () => {
it.each(["", ".hidden", "trail.", "a/b", "a..b", "self", "bad name"])(
"rejects %j",
async (name) => {
const { api } = await load();
expect(() => api.validateRepoName(name)).toThrow();
}
);
});
describe("default branch names", () => {
it.each(["main", "release/2026.10", "feat/qa-fixes", "v1"])("accepts %j", async (branch) => {
const { api } = await load();
expect(api.branchNameProblem(branch)).toBeUndefined();
});
it.each([
"",
"a".repeat(245),
"é".repeat(123),
"has space",
"a..b",
"-lead",
"trail/",
"a//b",
"end.",
"x.lock",
".hidden",
"a@{b",
"@",
"what?"
])("rejects %j", async (branch) => {
const { api } = await load();
expect(api.branchNameProblem(branch)).toBeTypeOf("string");
});
});