fix(desktop): retain confirmed aborts across refresh failures

Signed-off-by: Codex <noreply@openai.com>
This commit is contained in:
Codex
2026-09-30 22:02:45 +00:00
parent e5f56aa67b
commit e2d23ca8cf
4 changed files with 166 additions and 48 deletions

View File

@@ -27,7 +27,7 @@ type RefreshContext = {
export async function refreshAcceptedCommunityDeletions(
context: RefreshContext,
): Promise<void> {
if (!context.isCurrent()) return;
if (!context.isCurrent() || context.identityMismatch) return;
let auth = await invoke<BuilderlabAuth | null>("get_builderlab_auth");
if (!context.isCurrent()) return;
context.setAuth(auth);
@@ -40,39 +40,47 @@ export async function refreshAcceptedCommunityDeletions(
if (auth.canDeleteBuzzCommunities !== true) return;
let restored = false;
for (const [communityId, envelope] of [...context.accepted.entries()]) {
if (!listed.some((community) => community.id === communityId)) continue;
const stillEligible = () =>
context.isCurrent() &&
context.ownerPubkey() === envelope.bound_owner_pubkey &&
envelope.backend_origin === BUILDERLAB_BACKEND_ORIGIN &&
context.accepted.get(communityId) === envelope &&
!context.identityMismatch;
if (!stillEligible()) return;
auth = await invoke<BuilderlabAuth | null>("get_builderlab_auth");
if (!stillEligible()) return;
context.setAuth(auth);
if (!auth) {
context.clearAccount();
return;
}
if (auth.canDeleteBuzzCommunities !== true) return;
const request = publicDeletionRequest(envelope);
if (!stillEligible()) return;
const response = await invoke<CommunityDeletionTransport>(
"delete_builderlab_community",
{
communityId: request.community_id,
host: request.host,
requestId: request.request_id,
acknowledgementVersion: request.acknowledgement_version,
},
);
if (!stillEligible()) return;
if (deletionResponseDisposition(response, envelope, "check") === "abort") {
context.accepted.delete(communityId);
restored = true;
try {
for (const [communityId, envelope] of [...context.accepted.entries()]) {
if (!listed.some((community) => community.id === communityId)) continue;
const stillEligible = () =>
context.isCurrent() &&
context.ownerPubkey() === envelope.bound_owner_pubkey &&
envelope.backend_origin === BUILDERLAB_BACKEND_ORIGIN &&
context.accepted.get(communityId) === envelope;
if (!stillEligible()) return;
auth = await invoke<BuilderlabAuth | null>("get_builderlab_auth");
if (!stillEligible()) return;
context.setAuth(auth);
if (!auth) {
context.clearAccount();
return;
}
if (auth.canDeleteBuzzCommunities !== true) return;
const request = publicDeletionRequest(envelope);
const response = await invoke<CommunityDeletionTransport>(
"delete_builderlab_community",
{
communityId: request.community_id,
host: request.host,
requestId: request.request_id,
acknowledgementVersion: request.acknowledgement_version,
},
);
if (!stillEligible()) return;
const disposition = deletionResponseDisposition(
response,
envelope,
"check",
);
if (disposition === "abort") {
context.accepted.delete(communityId);
restored = true;
} else if (disposition !== "accept") {
throw new Error("Couldn't check deletion status.");
}
}
} finally {
if (restored && context.isCurrent()) context.restoreListed(listed);
}
if (restored && context.isCurrent()) context.restoreListed(listed);
}

View File

@@ -265,6 +265,7 @@ type E2eConfig = {
code: string;
message?: string;
} | null>;
builderlabDeletionRejectSequence?: boolean[];
builderlabDeletionStatusSequence?: string[];
builderlabDeletionHttpStatusSequence?: number[];
builderlabDeletionBodyStatus?: number;
@@ -12664,6 +12665,16 @@ export function maybeInstallE2eTauriMocks() {
heldBuilderlabDeletionReleases.push(resolve);
});
}
const rejectSequence =
activeConfig?.mock?.builderlabDeletionRejectSequence;
if (
rejectSequence?.length &&
(rejectSequence.length > 1
? rejectSequence.shift()
: rejectSequence[0])
) {
throw new Error("Mock deletion transport failure");
}
const sequence = activeConfig?.mock?.builderlabDeletionErrorSequence;
const deletionError = sequence?.length
? sequence.length > 1

View File

@@ -94,6 +94,7 @@ async function openDeletionFixture(
mismatch?: boolean;
errorCode?: string;
errorSequence?: Array<{ code: string; message?: string } | null>;
rejectSequence?: boolean[];
statusSequence?: string[];
capabilitySequence?: boolean[];
quota?: { used: number; limit: number; canCreate: boolean };
@@ -124,6 +125,7 @@ async function openDeletionFixture(
? { code: options.errorCode, message: "mock deletion error" }
: undefined,
builderlabDeletionErrorSequence: options.errorSequence,
builderlabDeletionRejectSequence: options.rejectSequence,
builderlabDeletionStatusSequence: options.statusSequence,
builderlabDeletionHttpStatusSequence: options.httpStatusSequence,
builderlabDeletionBodyStatus: options.bodyStatus,
@@ -156,6 +158,23 @@ async function startArchivedDeletion(page: Page) {
.click();
}
async function startSecondArchivedDeletion(page: Page) {
await page
.getByTestId("hosted-community-row")
.filter({ hasText: "Second archived team" })
.getByRole("button", { name: "Delete", exact: true })
.click();
await page
.getByLabel(
`Type the exact host to continue: ${SECOND_ARCHIVED.normalized_host}`,
)
.fill(SECOND_ARCHIVED.normalized_host);
await page.getByRole("button", { name: "Continue" }).click();
await page
.getByRole("button", { name: "Delete community permanently" })
.click();
}
async function storedDeletionRequestId(page: Page) {
return page.evaluate(() => {
const raw = window.localStorage.getItem(
@@ -1036,20 +1055,7 @@ test("Refresh restores only the aborted row while another accepted deletion rema
await expect(
page.getByText("Deletion started", { exact: true }),
).toBeVisible();
await page
.getByTestId("hosted-community-row")
.filter({ hasText: "Second archived team" })
.getByRole("button", { name: "Delete", exact: true })
.click();
await page
.getByLabel(
`Type the exact host to continue: ${SECOND_ARCHIVED.normalized_host}`,
)
.fill(SECOND_ARCHIVED.normalized_host);
await page.getByRole("button", { name: "Continue" }).click();
await page
.getByRole("button", { name: "Delete community permanently" })
.click();
await startSecondArchivedDeletion(page);
await expect.poll(() => deletionPayloads(page)).toHaveLength(2);
const acceptedCalls = await deletionPayloads(page);
await expect(
@@ -1082,15 +1088,99 @@ test("Refresh restores only the aborted row while another accepted deletion rema
]);
});
test("Refresh restores an earlier abort when a later replay rejects", async ({
page,
}) => {
await openDeletionFixture(page, {
capability: true,
communities: [...DELETION_COMMUNITIES, SECOND_ARCHIVED],
quota: { used: 3, limit: 5, canCreate: true },
statusSequence: ["submitted", "submitted", "aborted"],
rejectSequence: [false, false, false, true],
});
await startArchivedDeletion(page);
await startSecondArchivedDeletion(page);
await expect.poll(() => deletionPayloads(page)).toHaveLength(2);
const acceptedCalls = await deletionPayloads(page);
await page.getByRole("button", { name: "Refresh" }).click();
await expect(
page.getByText("Mock deletion transport failure", { exact: true }),
).toBeVisible();
const firstRow = page
.getByTestId("hosted-community-row")
.filter({ hasText: "Archived team" })
.filter({ hasNotText: "Second archived team" });
await expect(firstRow).toBeVisible();
await expect(
firstRow.getByRole("button", { name: "Delete", exact: true }),
).toBeEnabled();
await expect(
page
.getByTestId("hosted-community-row")
.filter({ hasText: "Second archived team" }),
).toHaveCount(0);
await expect(
page.getByText("Deletion started", { exact: true }),
).toBeVisible();
await expect
.poll(() => deletionPayloads(page))
.toEqual([
acceptedCalls?.[0],
acceptedCalls?.[1],
acceptedCalls?.[0],
acceptedCalls?.[1],
]);
});
test("Refresh restores an earlier abort when a later capability check fails", async ({
page,
}) => {
await openDeletionFixture(page, {
capability: true,
capabilitySequence: [true, true, true, false],
communities: [...DELETION_COMMUNITIES, SECOND_ARCHIVED],
quota: { used: 3, limit: 5, canCreate: true },
statusSequence: ["submitted", "submitted", "aborted"],
});
await startArchivedDeletion(page);
await startSecondArchivedDeletion(page);
await expect.poll(() => deletionPayloads(page)).toHaveLength(2);
const acceptedCalls = await deletionPayloads(page);
await page.getByRole("button", { name: "Refresh" }).click();
await expect(page.getByRole("button", { name: "Refresh" })).toBeEnabled();
const firstRow = page
.getByTestId("hosted-community-row")
.filter({ hasText: "Archived team" })
.filter({ hasNotText: "Second archived team" });
await expect(firstRow).toBeVisible();
await expect(
page
.getByTestId("hosted-community-row")
.filter({ hasText: "Second archived team" }),
).toHaveCount(0);
await expect(
page.getByText("Deletion started", { exact: true }),
).toBeVisible();
expect(await deletionPayloads(page)).toEqual([
acceptedCalls?.[0],
acceptedCalls?.[1],
acceptedCalls?.[0],
]);
});
for (const scenario of [
{ name: "non-aborted stage", statusSequence: ["submitted", "approved"] },
{
name: "uncertain 503",
errorSequence: [null, { code: "acceptance_unknown" }],
expectsCheckError: true,
},
{
name: "error-only 409",
errorSequence: [null, { code: "deletion_aborted" }],
expectsCheckError: true,
},
]) {
test(`Refresh keeps an accepted row hidden after ${scenario.name}`, async ({
@@ -1123,6 +1213,14 @@ for (const scenario of [
exact: true,
}),
).toHaveCount(0);
const checkError = page.getByText("Couldn't check deletion status.", {
exact: true,
});
if (scenario.expectsCheckError) {
await expect(checkError).toBeVisible();
} else {
await expect(checkError).toHaveCount(0);
}
});
}

View File

@@ -196,6 +196,7 @@ type MockBridgeOptions = {
code: string;
message?: string;
} | null>;
builderlabDeletionRejectSequence?: boolean[];
builderlabDeletionStatusSequence?: string[];
builderlabDeletionHttpStatusSequence?: number[];
builderlabDeletionBodyStatus?: number;