mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
fix(desktop): retain confirmed aborts across refresh failures
Signed-off-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ type MockBridgeOptions = {
|
||||
code: string;
|
||||
message?: string;
|
||||
} | null>;
|
||||
builderlabDeletionRejectSequence?: boolean[];
|
||||
builderlabDeletionStatusSequence?: string[];
|
||||
builderlabDeletionHttpStatusSequence?: number[];
|
||||
builderlabDeletionBodyStatus?: number;
|
||||
|
||||
Reference in New Issue
Block a user