Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199import { 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";import type { RecordView, RepoRecord } from "$lib/api/records";
const putRecord = vi.fn(async () => ({ uri: "at://did:plc:owner/sh.tangled.repo/core", cid: "bafynew"}));const deleteRecord = vi.fn(async () => undefined);const createRecord = vi.fn(async () => ({ uri: "at://did:plc:owner/sh.tangled.repo/core", cid: "bafynew"}));const awaitIndexedRecord = vi.fn(async () => undefined);const awaitDeletedRecord = vi.fn(async () => true);const pingIndexNow = vi.fn();
vi.mock("./write", () => ({ createRecord, putRecord, deleteRecord }));vi.mock("./awaitRecord", () => ({ awaitIndexedRecord, awaitDeletedRecord }));vi.mock("$lib/api/indexnow", () => ({ pingIndexNow }));
const call = vi.fn(async (_schema: unknown, _options: unknown) => ({ ok: true, data: undefined }));const post = vi.fn( async (_name: unknown, _options: { as: null; input: Record<string, 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, post } }) 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 view: RecordView<RepoRecord> = { uri: "at://did:plc:owner/sh.tangled.repo/core", cid: "bafyold", value: { $type: "sh.tangled.repo", knot: "knot.test", createdAt: "2026-01-01T00:00:00Z" as never, description: "a forge", website: "https://tangled.org" }};
describe("editedRepoRecord", () => { it("keeps untouched fields and drops the ones an edit clears", () => { expect( api.editedRepoRecord(view.value, { description: undefined, topics: ["svelte"] }) ).toEqual({ $type: "sh.tangled.repo", knot: "knot.test", createdAt: "2026-01-01T00:00:00Z", website: "https://tangled.org", topics: ["svelte"] }); });
it("drops an empty list rather than writing it", () => { expect(api.editedRepoRecord(view.value, { topics: [] })).not.toHaveProperty("topics"); });});
describe("saveRepoRecord", () => { it("swaps on the cid it read and waits for the appview", async () => { await api.saveRepoRecord(agent, bobbin, view, { description: "a forge for git" });
expect(putRecord).toHaveBeenCalledWith( agent, "sh.tangled.repo", "core", expect.objectContaining({ description: "a forge for git" }), "bafyold" ); expect(awaitIndexedRecord).toHaveBeenCalled(); });});
describe("knot procedures", () => { it("names the repo by DID", async () => { await api.setDefaultBranch(agent, "knot.test", "did:plc:repo", "trunk"); expect(call.mock.calls.at(-1)?.[1]).toEqual({ input: { repo: "did:plc:repo", defaultBranch: "trunk" } });
await api.addCollaborator(agent, "knot.test", "did:plc:repo", "did:plc:friend"); expect(call.mock.calls.at(-1)?.[1]).toEqual({ input: { repo: "did:plc:repo", subject: "did:plc:friend" } }); });});
describe("deleteRepo", () => { beforeEach(() => vi.clearAllMocks());
const location = { uri: view.uri, knot: "knot.test", name: "core", ownerHandle: "dawn", ownerDid: "did:plc:owner", repoDid: "did:plc:repo", cid: view.cid }; it("drops the record before the knot repository", async () => { const order: string[] = []; deleteRecord.mockImplementationOnce(async () => { order.push("record"); return undefined; }); post.mockImplementationOnce(async () => { order.push("knot"); return { ok: true, data: undefined }; });
await api.deleteRepo(agent, bobbin, location);
expect(order).toEqual(["record", "knot"]); expect(deleteRecord).toHaveBeenCalledWith(agent, "sh.tangled.repo", "core", "bafyold"); expect(post.mock.calls.at(-1)?.[0]).toBe("sh.tangled.repo.delete"); expect(post.mock.calls.at(-1)?.[1]).toEqual({ as: null, input: { repo: "did:plc:repo", did: "did:plc:owner", name: "core", rkey: "core" } }); expect(awaitDeletedRecord).toHaveBeenCalled(); });
it("names a repository whose record predates repo DIDs", async () => { await api.deleteRepo(agent, bobbin, { ...location, repoDid: undefined });
expect(post.mock.calls.at(-1)?.[1]?.input).toEqual({ did: "did:plc:owner", name: "core", rkey: "core" }); });
it("carries on when the record 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(post).toHaveBeenCalled(); });
it("stops when the record moved under a concurrent edit", async () => { deleteRecord.mockRejectedValueOnce( new ClientResponseError({ status: 400, data: { error: "InvalidSwap", message: "record moved" } }) );
await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow(/InvalidSwap/); expect(post).not.toHaveBeenCalled(); });
it("reports a knot that refuses after the record is gone", async () => { post.mockRejectedValueOnce(new Error("repo already exists: core"));
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/dawn/core"); });
it("reports a deletion the appview has not observed", async () => { awaitDeletedRecord.mockResolvedValueOnce(false);
await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow(/appview/); });});