From 586e75e50fc20a917092885ec059b8fc513a3464 Mon Sep 17 00:00:00 2001 From: dawn Date: Tue, 15 Sep 2026 05:47:28 +0300 Subject: [PATCH] web: delete the repo record before asking the knot Signed-off-by: dawn --- web/src/lib/api/repoSettings.test.ts | 53 +++++++++++++++++++++------- web/src/lib/api/repoSettings.ts | 34 +++++++++++------- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/web/src/lib/api/repoSettings.test.ts b/web/src/lib/api/repoSettings.test.ts index c0d83bf22..77909d96d 100644 --- a/web/src/lib/api/repoSettings.test.ts +++ b/web/src/lib/api/repoSettings.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { BobbinContext } from "$lib/api/client"; import type { RecordView, RepoRecord } from "$lib/api/records"; @@ -92,28 +92,55 @@ describe("knot procedures", () => { }); describe("deleteRepo", () => { - it("drops the knot repository before the record", async () => { + beforeEach(() => vi.clearAllMocks()); + + const location = { + uri: view.uri, + knot: "knot.test", + ownerDid: "did:plc:owner", + repoDid: "did:plc:repo" + }; + + it("drops the record before the knot repository", async () => { const order: string[] = []; - call.mockImplementationOnce(async () => { - order.push("knot"); - return { ok: true, data: undefined }; - }); deleteRecord.mockImplementationOnce(async () => { order.push("record"); return undefined; }); - - await api.deleteRepo(agent, bobbin, { - uri: view.uri, - knot: "knot.test", - ownerDid: "did:plc:owner", - repoDid: "did:plc:repo" + call.mockImplementationOnce(async () => { + order.push("knot"); + return { ok: true, data: undefined }; }); - expect(order).toEqual(["knot", "record"]); + await api.deleteRepo(agent, bobbin, location); + + expect(order).toEqual(["record", "knot"]); + expect(call.mock.calls.at(-1)?.[1]).toEqual({ + input: { + repo: "did:plc:repo", + did: "did:plc:owner", + name: "core", + rkey: "core" + } + }); expect(awaitDeletedRecord).toHaveBeenCalled(); }); + it("reports a knot that refuses after the record is gone", async () => { + call.mockRejectedValueOnce(new Error("repo already exists: core")); + + await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow( + /did not delete the repository/ + ); + expect(awaitDeletedRecord).not.toHaveBeenCalled(); + }); + + it("reports a deletion the appview has not observed", async () => { + awaitDeletedRecord.mockResolvedValueOnce(false); + + await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow(/appview/); + }); + it("refuses a repository that has no DID on its knot", async () => { await expect( api.deleteRepo(agent, bobbin, { diff --git a/web/src/lib/api/repoSettings.ts b/web/src/lib/api/repoSettings.ts index 85309dbf5..2f794ddb4 100644 --- a/web/src/lib/api/repoSettings.ts +++ b/web/src/lib/api/repoSettings.ts @@ -97,7 +97,6 @@ export interface RepoLocation { repoDid?: string; } -// drop knot git first, orphaned knot state is reapable but a broken record is not export const deleteRepo = async ( agent: OAuthUserAgent, bobbinUrl: string, @@ -107,19 +106,30 @@ export const deleteRepo = async ( throw new Error("This repository predates repo DIDs and cannot be deleted here."); } const rkey = rkeyFromUri(repo.uri); - await ok( - serviceClient(agent, repo.knot).xrpc.call(deleteRepoSchema, { - input: { - repo: didOf(repo.repoDid), - did: didOf(repo.ownerDid), - name: rkey, - rkey: recordKeyOf(rkey) - } - }) - ); await deleteRecord(agent, REPO_COLLECTION, rkey); - await awaitDeletedRecord( + try { + await ok( + serviceClient(agent, repo.knot).xrpc.call(deleteRepoSchema, { + input: { + repo: didOf(repo.repoDid), + did: didOf(repo.ownerDid), + name: rkey, + rkey: recordKeyOf(rkey) + } + }) + ); + } catch (cause) { + const detail = cause instanceof Error ? cause.message : String(cause); + throw new Error( + `The record is gone, but ${repo.knot} did not delete the repository: ${detail}`, + { cause } + ); + } + const confirmed = await awaitDeletedRecord( createBobbinClient({ serviceUrl: bobbinUrl }), repo.uri as ResourceUri ); + if (!confirmed) { + throw new Error("Deleted, but the appview has not caught up yet."); + } }; -- 2.51.2