Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317import { beforeEach, describe, expect, it, vi } from "vitest";import { ClientResponseError } from "@atcute/client";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import type { BobbinContext } from "#lib/api/client.ts";import type { RepoRecord } from "#lib/api/records.ts";
const manifest: RepoRecord = { $type: "org.tangled.repo.manifest", declaration: { owner: "did:plc:owner", name: "core" }, createdAt: "2026-01-01T00:00:00Z" as never, description: "a forge", website: "https://tangled.org"};const manifestUri = "at://did:plc:repo/org.tangled.repo.manifest/self";const written = { uri: manifestUri, cid: "bafyreihkszkjxz4fuml3wol5lujacnrlfhwutc2o7yj2hdsc3kr6usccyu"};
const updateCob = vi.fn( async ( _agent: unknown, _knot: unknown, _uri: unknown, mutate: (m: RepoRecord) => RepoRecord ) => ({ ...written, value: mutate(manifest) }));const createGrant = vi.fn(async () => undefined);const eraseGrant = vi.fn(async () => undefined);const refsWritten = { uri: "at://did:plc:repo/org.tangled.git.refs/self", cid: "bafyreib2rxk3rh6kzwq3u5ft6aqvq2tqkr4nwm7ud3ebp6m4uttaxyi6nm"};const knotProcedure = vi.fn(async () => refsWritten);vi.mock("./cob", () => ({ updateCob, createGrant, eraseGrant, knotProcedure }));
const declaration = { value: { $type: "org.tangled.repo.declaration", repo: "did:plc:repo", createdAt: "2026-01-01T00:00:00Z" }, written: { uri: "at://did:plc:owner/org.tangled.repo.declaration/core", cid: "bafyold" }};const readRecord = vi.fn(async (_agent: unknown, _collection: unknown, rkey: string) => rkey === "core" ? declaration : null);const createRecord = vi.fn(async () => ({ uri: "at://did:plc:owner/org.tangled.repo.declaration/forge", cid: "bafynew"}));const deleteRecord = vi.fn(async () => undefined);vi.mock("./write", () => ({ createRecord, deleteRecord, readRecord }));
const awaitIndexedRecord = vi.fn(async () => undefined);const awaitDeletedRecord = vi.fn(async () => true);vi.mock("./awaitRecord", () => ({ awaitIndexedRecord, awaitDeletedRecord }));const pingIndexNow = vi.fn();vi.mock("#lib/api/indexnow.ts", () => ({ pingIndexNow }));
const call = vi.fn(async (_schema: unknown, _options: unknown) => ({ ok: true, data: undefined }));vi.mock("./client", async (importOriginal) => ({ ...(await importOriginal<typeof import("./client")>()), createBobbinClient: () => ({ xrpc: { call } }) as unknown as BobbinContext}));vi.mock("./_request", async (importOriginal) => ({ ...(await importOriginal<typeof import("./_request")>()), serviceClient: () => ({ xrpc: { call } }) as unknown as BobbinContext}));
const api = await import("./repoSettings");
const agent = { sub: "did:plc:owner" } as unknown as OAuthUserAgent;const bobbin = "https://bobbin.test";const repo = { uri: manifestUri, knot: "knot.test", rkey: "core" };
beforeEach(() => vi.clearAllMocks());
describe("editedManifest", () => { it("keeps untouched fields and drops the ones an edit clears", () => { expect( api.editedManifest(manifest, { description: undefined, topics: ["svelte"] }) ).toEqual({ $type: "org.tangled.repo.manifest", declaration: { owner: "did:plc:owner", name: "core" }, createdAt: "2026-01-01T00:00:00Z", website: "https://tangled.org", topics: ["svelte"] }); });
it("drops an empty list rather than writing it", () => { expect(api.editedManifest(manifest, { topics: [] })).not.toHaveProperty("topics"); });
it("drops a defaultBranch left over from older manifests", () => { const onMain = { ...manifest, defaultBranch: "main" } as RepoRecord; expect(api.editedManifest(onMain, {})).not.toHaveProperty("defaultBranch"); });});
describe("editManifest", () => { it("edits the manifest on the knot and waits for the appview", async () => { await api.editManifest(agent, bobbin, repo, { description: "a new forge" });
expect(updateCob).toHaveBeenCalledWith( agent, "knot.test", manifestUri, expect.any(Function) ); expect((await updateCob.mock.results[0].value).value).toMatchObject({ description: "a new forge" }); expect(awaitIndexedRecord).toHaveBeenCalledWith( expect.anything(), expect.objectContaining(written) ); });});
describe("setDefaultBranch", () => { it("asks the knot to move the default branch and waits for the refs record", async () => { await api.setDefaultBranch(agent, bobbin, "knot.test", "did:plc:repo", "trunk");
expect(knotProcedure).toHaveBeenCalledWith( agent, expect.anything(), "did:plc:repo", "org.tangled.git.setDefaultBranch", { branch: "trunk" } ); expect(awaitIndexedRecord).toHaveBeenCalledWith(expect.anything(), refsWritten); });});
describe("renameRepo", () => { it("only edits the manifest when the record key stays the same", async () => { await api.renameRepo(agent, bobbin, repo, "Core");
expect((await updateCob.mock.results[0].value).value.declaration.name).toBe("Core"); expect(createRecord).not.toHaveBeenCalled(); expect(deleteRecord).not.toHaveBeenCalled(); });
it("moves the declaration around the manifest edit", async () => { const order: string[] = []; createRecord.mockImplementationOnce(async () => { order.push("new declaration"); return { uri: "at://did:plc:owner/org.tangled.repo.declaration/forge", cid: "bafynew" }; }); updateCob.mockImplementationOnce(async (_a, _k, _u, mutate) => { order.push("manifest"); return { ...written, value: mutate(manifest) }; }); deleteRecord.mockImplementationOnce(async () => { order.push("old declaration"); });
await api.renameRepo(agent, bobbin, repo, "Forge");
expect(order).toEqual(["new declaration", "manifest", "old declaration"]); expect(awaitIndexedRecord).toHaveBeenCalledWith(expect.anything(), { uri: "at://did:plc:owner/org.tangled.repo.declaration/forge", cid: "bafynew" }); expect(createRecord).toHaveBeenCalledWith( agent, "org.tangled.repo.declaration", declaration.value, "forge" ); expect(deleteRecord).toHaveBeenCalledWith( agent, "org.tangled.repo.declaration", "core", "bafyold" ); });
it("takes the new declaration back down when the knot refuses the rename", async () => { updateCob.mockRejectedValueOnce( new Error("the record key forge already belongs to another repository") );
await expect(api.renameRepo(agent, bobbin, repo, "Forge")).rejects.toThrow( /another repository/ );
expect(deleteRecord).toHaveBeenCalledTimes(1); expect(deleteRecord).toHaveBeenCalledWith( agent, "org.tangled.repo.declaration", "forge", "bafynew" ); });
it("refuses a name the account already has without writing anything", async () => { readRecord.mockImplementation(async () => declaration);
await expect(api.renameRepo(agent, bobbin, repo, "Taken")).rejects.toThrow( /already exists/ );
expect(createRecord).not.toHaveBeenCalled(); expect(updateCob).not.toHaveBeenCalled(); readRecord.mockImplementation(async (_a, _c, rkey: string) => rkey === "core" ? declaration : null ); });});
describe("collaborators", () => { it("are grant records on the repo, keyed by the collaborator", async () => { await api.addCollaborator(agent, "knot.test", "did:plc:repo", "did:plc:friend"); expect(createGrant).toHaveBeenCalledWith( agent, "knot.test", "did:plc:repo", "org.tangled.graph.collaborator", "did:plc:friend" );
await api.removeCollaborator(agent, "knot.test", "did:plc:repo", "did:plc:friend"); expect(eraseGrant).toHaveBeenCalledWith( agent, "knot.test", "did:plc:repo", "org.tangled.graph.collaborator", "did:plc:friend" ); });});
describe("deleteRepo", () => { const location = { rkey: "core", ownerDid: "did:plc:owner", knot: "knot.test", name: "core", ownerHandle: "alice", repoDid: "did:plc:repo" };
it("drops the declaration before asking the knot", async () => { const order: string[] = []; deleteRecord.mockImplementationOnce(async () => { order.push("declaration"); }); call.mockImplementationOnce(async () => { order.push("knot"); return { ok: true, data: undefined }; });
await api.deleteRepo(agent, bobbin, location);
expect(order).toEqual(["declaration", "knot"]); expect(deleteRecord).toHaveBeenCalledWith(agent, "org.tangled.repo.declaration", "core"); expect(call.mock.calls[0][1]).toEqual({ input: { repo: "did:plc:repo" } }); expect(awaitDeletedRecord).toHaveBeenCalledWith( expect.anything(), "at://did:plc:owner/org.tangled.repo.declaration/core" ); });
it("carries on when the declaration is already gone", async () => { deleteRecord.mockRejectedValueOnce( new ClientResponseError({ status: 400, data: { error: "RecordNotFound", message: "could not locate record" } }) );
await api.deleteRepo(agent, bobbin, location);
expect(call).toHaveBeenCalled(); });
it("stops when the pds refuses for another reason", async () => { deleteRecord.mockRejectedValueOnce( new ClientResponseError({ status: 500, data: { error: "InternalServerError" } }) );
await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow( /InternalServerError/ ); expect(call).not.toHaveBeenCalled(); });
it("reports a knot that refuses after the declaration is gone", async () => { call.mockRejectedValueOnce( new Error("only repository owner or a knot admin may delete it") );
await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow( /did not delete core/ ); expect(awaitDeletedRecord).not.toHaveBeenCalled(); });
it("tells indexnow the repo url went away", async () => { await api.deleteRepo(agent, bobbin, location);
expect(pingIndexNow).toHaveBeenCalledWith("https://tangled.org/alice/core"); });
it("reports a deletion the appview has not observed", async () => { awaitDeletedRecord.mockResolvedValueOnce(false);
await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow(/appview/); });});