Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316import { 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; putError?: Error; readError?: Error; existing?: { value: Record<string, unknown>; cid?: string | null } | null;}
// mocks have to be installed before the module is loadedconst load = async ({ createError, putError, readError, existing = null }: 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"); 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" } }; });
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: { repoDid: "did:plc:repo" } }; } } }) }));
return { api: await import("./repoCreate"), events, records, runCalls, knotInputs, knotAgents, createRecord, putRecord, deleteRecord, readRecord };};
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, putRecord, 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(putRecord.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("announces the record before creating the knot repo, then commits the repo DID", async () => { const { api, events, records, putRecord } = await load();
const creation = await api.createRepo(agent, "https://bobbin.test", input);
expect(creation.name).toBe("Demo"); expect(events).toEqual([ "record:create", "bobbin:await", "knot:create", "record:put", "bobbin:await" ]); expect(records[0]).toMatchObject({ $type: "sh.tangled.repo", knot: "knot.test", name: "Demo", spindle: "spindle.test" }); expect(records[1]).toMatchObject({ repoDid: "did:plc:repo" }); expect(putRecord).toHaveBeenCalledWith( agent, "sh.tangled.repo", "demo", expect.anything(), "initial-cid" ); });
it("removes the pds record before cleaning up the knot when commit fails", async () => { const { api, events, deleteRecord } = await load({ putError: new Error("pds refused") });
await expect(api.createRepo(agent, "https://bobbin.test", input)).rejects.toThrow( "pds refused" );
expect(events.slice(-2)).toEqual(["record:delete", "knot:delete"]); expect(deleteRecord).toHaveBeenCalledWith(agent, "sh.tangled.repo", "demo", "initial-cid"); });
it("removes the announced record 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).toContain("record:delete"); 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: "sh.tangled.repo", knot: "knot.test", repoDid: "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, "sh.tangled.repo", "demo"); expect(events).toEqual([]); });
it("resumes a creation that never reached the knot instead of stranding the name", async () => { const { api, events, runCalls, createRecord, putRecord } = await load({ existing: { value: { $type: "sh.tangled.repo", knot: "knot.test" } } });
const creation = await api.createRepo(agent, "https://bobbin.test", input);
expect(creation.repoDid).toBe("did:plc:repo"); expect(createRecord).not.toHaveBeenCalled(); expect(events).toEqual([ "record:put", "bobbin:await", "knot:create", "record:put", "bobbin:await" ]); expect(putRecord.mock.calls.map((call) => call[4])).toEqual([ "existing-cid", "committed-cid" ]); expect(runCalls.at(-1)).toMatchObject({ rkey: "demo", defaultBranch: "main" }); });
it("refuses an interrupted record left on a different knot", async () => { const { api, events } = await load({ existing: { value: { $type: "sh.tangled.repo", knot: "other.knot.test" } } });
await expect(api.createRepo(agent, "https://bobbin.test", input)).rejects.toThrow( 'A repository named "Demo" already exists in your account. Choose another name.' ); 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 fork identity and clone source to both halves", async () => { const { api, records, knotInputs } = await load(); await api.createRepo(agent, "https://bobbin.test", { ...input, name: "fork", source: { url: "https://knot.source/did:plc:upstream", kind: "fork", record: "did:plc:upstream" } });
expect(records[0]).toMatchObject({ source: "did:plc:upstream" }); expect(knotInputs[0]).toMatchObject({ source: { url: "https://knot.source/did:plc:upstream", kind: "fork" } }); });
it("creates optimistic record without repoDid and awaits bobbin indexing", async () => { const { api, records, events } = await load(); const result = await api.createOptimisticRepoRecord(agent, "https://bobbin.test", input);
expect(result.rkey).toBe("demo"); expect(result.name).toBe("Demo"); expect(records[0]).toMatchObject({ $type: "sh.tangled.repo", knot: "knot.test" }); expect((records[0] as Record<string, unknown>).repoDid).toBeUndefined(); expect(events).toEqual(["record:create", "bobbin:await"]); });
it("normalizes knot did:web to hostname in the created record", async () => { const { api, records } = await load(); await api.createOptimisticRepoRecord(agent, "https://bobbin.test", { ...input, knot: "did:web:knot2.tngl.boltless.dev" });
expect(records[0]).toMatchObject({ $type: "sh.tangled.repo", knot: "knot2.tngl.boltless.dev" }); });
it("normalizes knot did:web with encoded port to hostname in the created repo", async () => { const { api, records } = await load(); await api.createRepo(agent, "https://bobbin.test", { ...input, knot: "did:web:knot.oyster.cafe%3A8443" });
expect(records[0]).toMatchObject({ $type: "sh.tangled.repo", knot: "knot.oyster.cafe:8443" }); });});
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(); } );});