@@ -454,6 +797,56 @@ export function HostedCommunitiesSettingsCard() {
) : null}
+ {statusMessage ? (
+
+ {statusMessage}
+
+ ) : null}
+
+ {pendingDeletion ? (
+
+
+ Deletion acceptance for {pendingDeletion.host} is uncertain. Keep
+ request {pendingDeletion.request_id} pending until its
+ status is resolved. Checking deletion status resends this same
+ request ID and may start the original deletion if it was never
+ received. Do not start a different request.
+
+
+ void checkDeletionStatus(pendingDeletion)}
+ >
+ Check deletion status
+
+
+ {!deletionCapability ? (
+
+ Community deletion is unavailable right now, so this request
+ can't be checked. It stays saved on this device.
+
+ ) : null}
+
+ ) : null}
+
+ {auth && boundHex && deletionCapability && blockingOwnerPubkey ? (
+
+ A deletion request from{" "}
+ {safeNpub(blockingOwnerPubkey) ?? "another Buzz identity"} is still
+ pending on this device. Switch to that Buzz identity and use Check
+ deletion status before starting another deletion here. If you no
+ longer have that identity, contact support.
+
+ ) : null}
+
{loading ? (
Checking sign-in…
@@ -592,14 +985,16 @@ export function HostedCommunitiesSettingsCard() {
Your communities
- {communities.length} of {MAX_COMMUNITIES} used
+ {quota?.used != null && quota.limit != null
+ ? `${quota.used} of ${quota.limit} used`
+ : "Quota unavailable"}
void run("Refreshing…", loadAccount)}
+ onClick={() => void refreshCommunities()}
>
Refresh
@@ -617,10 +1012,20 @@ export function HostedCommunitiesSettingsCard() {
Number(Boolean(b.archived_at)),
)
.map((community, index) => (
- void archiveCommunity(community)}
onUnarchive={() => void unarchiveCommunity(community)}
onTransfer={(npub) => transferCommunity(community, npub)}
+ onDelete={() => startCommunityDeletion(community)}
/>
))}
@@ -667,9 +1073,11 @@ export function HostedCommunitiesSettingsCard() {
{atCommunityLimit ? (
- You've reached the limit of {MAX_COMMUNITIES} hosted
- communities. Transfer one to free up a slot before creating
- another.
+ {quota?.limit === 0
+ ? "You can't create more communities right now."
+ : quota?.limit != null
+ ? `You've reached the limit of ${quota.limit} hosted communities. A deletion frees its slot only after logical cleanup completes.`
+ : "You've reached the hosted-community limit. A deletion frees its slot only after logical cleanup completes."}
) : null}
@@ -712,23 +1120,24 @@ export function HostedCommunitiesSettingsCard() {
That address is available.
) : null}
-
- {action ? (
-
- ) : null}
- {action ?? "Create and connect"}
-
+ {!atCommunityLimit ? (
+
+ {action ? (
+
+ ) : null}
+ {action ?? "Create and connect"}
+
+ ) : null}
>
)}
@@ -777,218 +1186,3 @@ function UnpairIdentityButton({
);
}
-
-function CommunityRow({
- community,
- busy,
- canConnect,
- onConnect,
- onArchive,
- onUnarchive,
- onTransfer,
- showIconPicker,
-}: {
- community: HostedCommunity;
- busy: boolean;
- canConnect: boolean;
- onConnect: () => void;
- onArchive: () => void;
- onUnarchive: () => void;
- onTransfer: (npub: string) => Promise
;
- showIconPicker: boolean;
-}) {
- const [confirmArchive, setConfirmArchive] = React.useState(false);
- const [confirmUnarchive, setConfirmUnarchive] = React.useState(false);
- const [transferOpen, setTransferOpen] = React.useState(false);
- const url = relayUrl(community);
- const archived = Boolean(community.archived_at);
- const displayName = community.name ?? community.slug ?? "Hosted community";
-
- return (
-
-
- {showIconPicker ?
: null}
-
-
{displayName}
-
- {community.normalized_host}
- {archived ? " · Archived" : ""}
-
-
-
-
- {archived ? (
- <>
- setConfirmUnarchive(true)}
- >
- Unarchive
-
-
-
-
- Unarchive {displayName}?
-
- This address becomes connectable again. Connections that
- closed during archival will not reconnect automatically.
-
-
-
- Cancel
-
- Unarchive
-
-
-
-
- >
- ) : (
-
- {url && canConnect ? (
-
- Connect
-
- ) : null}
-
setTransferOpen(true)}
- >
- Transfer
-
-
setConfirmArchive(true)}
- >
- Archive
-
-
-
-
-
- Archive {displayName}?
-
- New and existing connections stop and the address stays
- reserved. Archiving can't be undone from here without
- unarchiving, and the community keeps counting toward your
- quota — it isn't deleted.
-
-
-
- Cancel
-
- Archive
-
-
-
-
-
-
-
- )}
-
- );
-}
-
-function TransferOwnershipDialog({
- open,
- onOpenChange,
- communityName,
- busy,
- onTransfer,
-}: {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- communityName: string;
- busy: boolean;
- onTransfer: (npub: string) => Promise;
-}) {
- const [npub, setNpub] = React.useState("");
- const npubIsValid = npub.startsWith("npub1") && npub.length >= 50;
-
- React.useEffect(() => {
- if (!open) setNpub("");
- }, [open]);
-
- const submit = async () => {
- if (!npubIsValid) return;
- const ok = await onTransfer(npub.trim());
- if (ok) onOpenChange(false);
- };
-
- return (
-
-
-
- Transfer ownership
-
- Transfer {communityName} to another person. You become a regular
- member. The recipient needs a connected Buzz identity first, and
- this can't be undone.
-
-
-
-
setNpub(event.target.value.trim())}
- />
- {npub.length > 0 && !npubIsValid ? (
-
- Enter a valid npub that starts with npub1.
-
- ) : null}
-
-
- onOpenChange(false)}>
- Cancel
-
- void submit()}
- >
- {busy ? : null}
- Transfer ownership
-
-
-
-
- );
-}
diff --git a/desktop/src/features/settings/ui/HostedCommunityDeleteDialog.test.mjs b/desktop/src/features/settings/ui/HostedCommunityDeleteDialog.test.mjs
new file mode 100644
index 00000000000..875e329d3d8
--- /dev/null
+++ b/desktop/src/features/settings/ui/HostedCommunityDeleteDialog.test.mjs
@@ -0,0 +1,12 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { deletionAcknowledgementMatches } from "./HostedCommunityDeleteDialog.tsx";
+
+test("deletion acknowledgement is byte-exact", () => {
+ const host = "Exact-Host.communities.buzz.xyz";
+ assert.equal(deletionAcknowledgementMatches(host, host), true);
+ assert.equal(deletionAcknowledgementMatches(host.toLowerCase(), host), false);
+ assert.equal(deletionAcknowledgementMatches(` ${host}`, host), false);
+ assert.equal(deletionAcknowledgementMatches(`${host} `, host), false);
+});
diff --git a/desktop/src/features/settings/ui/HostedCommunityDeleteDialog.tsx b/desktop/src/features/settings/ui/HostedCommunityDeleteDialog.tsx
new file mode 100644
index 00000000000..9b612e529bf
--- /dev/null
+++ b/desktop/src/features/settings/ui/HostedCommunityDeleteDialog.tsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+
+import type { HostedCommunity } from "@/features/communities/hostedCommunityApi";
+import { Button } from "@/shared/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/ui/dialog";
+import { Input } from "@/shared/ui/input";
+
+export function deletionAcknowledgementMatches(value: string, host: string) {
+ return value === host;
+}
+
+export function HostedCommunityDeleteDialog({
+ community,
+ disabled,
+ onConfirm,
+}: {
+ community: HostedCommunity;
+ disabled: boolean;
+ onConfirm: () => void;
+}) {
+ const [open, setOpen] = React.useState(false);
+ const [hostAcknowledgement, setHostAcknowledgement] = React.useState("");
+ const [finalStep, setFinalStep] = React.useState(false);
+ const host = community.normalized_host ?? "";
+
+ const setDialogOpen = (next: boolean) => {
+ setOpen(next);
+ if (!next) {
+ setHostAcknowledgement("");
+ setFinalStep(false);
+ }
+ };
+
+ return (
+ <>
+ setOpen(true)}
+ >
+ Delete
+
+
+
+
+
+ {finalStep
+ ? "Permanently delete this community?"
+ : `Delete ${host}?`}
+
+
+ This cannot be cancelled by an owner. All community content will
+ eventually be deleted, the host stays permanently reserved, and
+ your quota slot is released only after logical cleanup completes.
+
+
+ {finalStep ? (
+
+ You acknowledged {host} . This final confirmation
+ immediately starts the irreversible deletion workflow.
+
+ ) : (
+
+ Type the exact host to continue: {host}
+ setHostAcknowledgement(event.target.value)}
+ />
+
+ )}
+
+ setDialogOpen(false)}>
+ Cancel
+
+ {finalStep ? (
+ {
+ onConfirm();
+ setDialogOpen(false);
+ }}
+ >
+ Delete community permanently
+
+ ) : (
+ setFinalStep(true)}
+ >
+ Continue
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/desktop/src/features/settings/ui/HostedCommunityRow.tsx b/desktop/src/features/settings/ui/HostedCommunityRow.tsx
new file mode 100644
index 00000000000..723e04c915b
--- /dev/null
+++ b/desktop/src/features/settings/ui/HostedCommunityRow.tsx
@@ -0,0 +1,262 @@
+import * as React from "react";
+import {
+ Archive,
+ ArchiveRestore,
+ ArrowLeftRight,
+ LoaderCircle,
+} from "lucide-react";
+
+import {
+ hostedCommunityRelayUrl as relayUrl,
+ type HostedCommunity,
+} from "@/features/communities/hostedCommunityApi";
+import { CommunityIconSettingsCard } from "@/features/communities/ui/CommunityIconSettingsCard";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/shared/ui/alert-dialog";
+import { Button, buttonVariants } from "@/shared/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/ui/dialog";
+import { Input } from "@/shared/ui/input";
+import { HostedCommunityDeleteDialog } from "./HostedCommunityDeleteDialog";
+
+export function HostedCommunityRow({
+ community,
+ busy,
+ canDelete,
+ canConnect,
+ deletionPending,
+ onConnect,
+ onArchive,
+ onUnarchive,
+ onTransfer,
+ onDelete,
+ showIconPicker,
+}: {
+ community: HostedCommunity;
+ busy: boolean;
+ canDelete: boolean;
+ canConnect: boolean;
+ deletionPending: boolean;
+ onConnect: () => void;
+ onArchive: () => void;
+ onUnarchive: () => void;
+ onTransfer: (npub: string) => Promise;
+ onDelete: () => void;
+ showIconPicker: boolean;
+}) {
+ const [confirmArchive, setConfirmArchive] = React.useState(false);
+ const [confirmUnarchive, setConfirmUnarchive] = React.useState(false);
+ const [transferOpen, setTransferOpen] = React.useState(false);
+ const url = relayUrl(community);
+ const archived = Boolean(community.archived_at);
+ const displayName = community.name ?? community.slug ?? "Hosted community";
+
+ return (
+
+
+ {showIconPicker ?
: null}
+
+
{displayName}
+
+ {community.normalized_host}
+ {archived ? " · Archived" : ""}
+
+
+
+
+ {archived ? (
+
+
setConfirmUnarchive(true)}
+ >
+ Unarchive
+
+
+
+
+ Unarchive {displayName}?
+
+ This address becomes connectable again. Connections that
+ closed during archival will not reconnect automatically.
+
+
+
+ Cancel
+
+ Unarchive
+
+
+
+
+ {canDelete ? (
+
+ ) : null}
+
+ ) : (
+
+ {url && canConnect ? (
+
+ Connect
+
+ ) : null}
+
setTransferOpen(true)}
+ >
+ Transfer
+
+
setConfirmArchive(true)}
+ >
+ Archive
+
+
+
+
+
+ Archive {displayName}?
+
+ New and existing connections stop and the address stays
+ reserved. Archiving can't be undone from here without
+ unarchiving, and the community keeps counting toward your
+ quota — it isn't deleted.
+
+
+
+ Cancel
+
+ Archive
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+function TransferOwnershipDialog({
+ open,
+ onOpenChange,
+ communityName,
+ busy,
+ onTransfer,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ communityName: string;
+ busy: boolean;
+ onTransfer: (npub: string) => Promise;
+}) {
+ const [npub, setNpub] = React.useState("");
+ const npubIsValid = npub.startsWith("npub1") && npub.length >= 50;
+
+ React.useEffect(() => {
+ if (!open) setNpub("");
+ }, [open]);
+
+ const submit = async () => {
+ if (!npubIsValid) return;
+ const ok = await onTransfer(npub.trim());
+ if (ok) onOpenChange(false);
+ };
+
+ return (
+
+
+
+ Transfer ownership
+
+ Transfer {communityName} to another person. You become a regular
+ member. The recipient needs a connected Buzz identity first, and
+ this can't be undone.
+
+
+
+
setNpub(event.target.value.trim())}
+ />
+ {npub.length > 0 && !npubIsValid ? (
+
+ Enter a valid npub that starts with npub1.
+
+ ) : null}
+
+
+ onOpenChange(false)}>
+ Cancel
+
+ void submit()}
+ >
+ {busy ? : null}
+ Transfer ownership
+
+
+
+
+ );
+}
diff --git a/desktop/src/features/settings/ui/acceptedDeletionRefresh.ts b/desktop/src/features/settings/ui/acceptedDeletionRefresh.ts
new file mode 100644
index 00000000000..bc9b8784d4b
--- /dev/null
+++ b/desktop/src/features/settings/ui/acceptedDeletionRefresh.ts
@@ -0,0 +1,87 @@
+import { invoke } from "@tauri-apps/api/core";
+
+import {
+ BUILDERLAB_BACKEND_ORIGIN,
+ deletionResponseDisposition,
+ publicDeletionRequest,
+ type CommunityDeletionTransport,
+ type PendingCommunityDeletion,
+} from "@/features/communities/communityDeletionPending";
+import type {
+ BuilderlabAuth,
+ HostedCommunity,
+} from "@/features/communities/hostedCommunityApi";
+
+type RefreshContext = {
+ accepted: Map;
+ isCurrent: () => boolean;
+ ownerPubkey: () => string | null;
+ identityMismatch: boolean;
+ loadAccount: () => Promise;
+ setAuth: (auth: BuilderlabAuth | null) => void;
+ clearAccount: () => void;
+ restoreListed: (listed: HostedCommunity[]) => void;
+};
+
+/** Recheck accepted requests only during an explicit owner-list refresh. */
+export async function refreshAcceptedCommunityDeletions(
+ context: RefreshContext,
+): Promise {
+ if (!context.isCurrent()) return;
+ const skipReplay = context.identityMismatch;
+ let auth = await invoke("get_builderlab_auth");
+ if (!context.isCurrent()) return;
+ context.setAuth(auth);
+ if (!auth) {
+ context.clearAccount();
+ return;
+ }
+ const listed = await context.loadAccount();
+ if (!context.isCurrent() || skipReplay) return;
+ if (auth.canDeleteBuzzCommunities !== true) return;
+
+ let restored = false;
+ 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("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(
+ "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);
+ }
+}
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 201701952aa..3da04a45ad0 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -231,6 +231,7 @@ type E2eConfig = {
email?: string;
name?: string;
expiresAt: string;
+ canDeleteBuzzCommunities?: boolean;
} | null;
/** Optional policy returned by the native join-policy discovery command. */
joinPolicy?: {
@@ -243,6 +244,11 @@ type E2eConfig = {
builderlabLoginDelayMs?: number;
/** Bound Builderlab Nostr identity. Null/omitted = not linked yet. */
builderlabIdentity?: { npub?: string; pubkey_hex?: string } | null;
+ /** Ordered native identity results; the final result remains sticky. */
+ builderlabIdentityResponseSequence?: Array<
+ | { identity: { npub?: string; pubkey_hex?: string } }
+ | { error: { code: string; setup_needed?: boolean } }
+ >;
/** Structured error returned when onboarding tries to bind the local identity. */
builderlabBindError?: { code?: string; message?: string };
/** Communities owned by the mocked Builderlab account. */
@@ -253,6 +259,33 @@ type E2eConfig = {
normalized_host?: string;
archived_at?: string | null;
}>;
+ builderlabQuota?: { used: number; limit: number; canCreate: boolean };
+ builderlabDeletionError?: { code: string; message?: string };
+ builderlabDeletionErrorSequence?: Array<{
+ code: string;
+ message?: string;
+ } | null>;
+ builderlabDeletionRejectSequence?: boolean[];
+ builderlabDeletionStatusSequence?: string[];
+ builderlabDeletionHttpStatusSequence?: number[];
+ builderlabDeletionBodyStatus?: number;
+ /** Hold deletion responses until the test explicitly releases them. */
+ builderlabDeferDeletion?: boolean | "initial";
+ builderlabAuthSequence?: Array<{
+ email?: string;
+ name?: string;
+ expiresAt: string;
+ canDeleteBuzzCommunities?: boolean;
+ } | null>;
+ builderlabCommunitiesSequence?: Array<
+ Array<{
+ id?: string;
+ name?: string;
+ slug?: string;
+ normalized_host?: string;
+ archived_at?: string | null;
+ }>
+ >;
/** Override the community returned after hosted creation. */
builderlabCreatedCommunity?: {
id?: string;
@@ -1585,6 +1618,8 @@ declare global {
* `syncAgentsToActiveHuddleDelayMs`, letting it resolve without waiting
* out the delay. Returns the number of holds flushed. */
__BUZZ_E2E_RELEASE_HUDDLE_AGENT_SYNCS__?: () => number;
+ /** Release every held hosted-community deletion response. */
+ __BUZZ_E2E_RELEASE_BUILDERLAB_DELETIONS__?: () => number;
/** Hold the next channel read until released. */
__BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__?: () => void;
/** Disarm the latch and release the held channel read, if any. */
@@ -1761,6 +1796,7 @@ let heldManagedAgentStartReleases: Array<() => void> = [];
// must outlast the mid-send mutation a spec injects, then settle on demand
// rather than on a timer, so the publish never races the injection.
let heldHuddleAgentSyncReleases: Array<() => void> = [];
+let heldBuilderlabDeletionReleases: Array<() => void> = [];
let cancelledMediaUploadIds = new Set();
let cancelledMediaFetchIds = new Set();
let mockMediaFetchControllers = new Map();
@@ -11794,6 +11830,12 @@ export function maybeInstallE2eTauriMocks() {
for (const release of held) release();
return held.length;
};
+ heldBuilderlabDeletionReleases = [];
+ window.__BUZZ_E2E_RELEASE_BUILDERLAB_DELETIONS__ = () => {
+ const held = heldBuilderlabDeletionReleases.splice(0);
+ for (const release of held) release();
+ return held.length;
+ };
deferNextChannelsRead = false;
deferredChannelsReadResolve = null;
window.__BUZZ_E2E_CHANNELS_READ_PENDING__ = 0;
@@ -12533,8 +12575,13 @@ export function maybeInstallE2eTauriMocks() {
registry: await handleMockCommand("list_voice_registry", null),
};
}
- case "get_builderlab_auth":
+ case "get_builderlab_auth": {
+ const sequence = activeConfig?.mock?.builderlabAuthSequence;
+ if (sequence?.length) {
+ return sequence.length > 1 ? sequence.shift() : sequence[0];
+ }
return activeConfig?.mock?.builderlabAuth ?? null;
+ }
case "start_builderlab_login": {
const delayMs = activeConfig?.mock?.builderlabLoginDelayMs ?? 0;
if (delayMs > 0)
@@ -12552,6 +12599,10 @@ export function maybeInstallE2eTauriMocks() {
if (activeConfig?.mock) activeConfig.mock.builderlabAuth = null;
return null;
case "get_builderlab_nostr_identity":
+ if (activeConfig?.mock?.builderlabIdentityResponseSequence?.length) {
+ const sequence = activeConfig.mock.builderlabIdentityResponseSequence;
+ return sequence.length > 1 ? sequence.shift() : sequence[0];
+ }
return activeConfig?.mock?.builderlabIdentity
? { identity: activeConfig.mock.builderlabIdentity }
: { error: { code: "missing_mapping", setup_needed: true } };
@@ -12570,10 +12621,26 @@ export function maybeInstallE2eTauriMocks() {
case "delete_builderlab_nostr_identity":
if (activeConfig?.mock) activeConfig.mock.builderlabIdentity = null;
return {};
- case "list_builderlab_communities":
+ case "list_builderlab_communities": {
+ const sequence = activeConfig?.mock?.builderlabCommunitiesSequence;
+ const hostedCommunities =
+ (sequence?.length
+ ? sequence.length > 1
+ ? sequence.shift()
+ : sequence[0]
+ : activeConfig?.mock?.builderlabCommunities) ?? [];
+ const hostedQuota = activeConfig?.mock?.builderlabQuota ?? {
+ used: hostedCommunities.length,
+ limit: 5,
+ canCreate: hostedCommunities.length < 5,
+ };
return {
- communities: activeConfig?.mock?.builderlabCommunities ?? [],
+ communities: hostedCommunities,
+ quota_used: hostedQuota.used,
+ quota_limit: hostedQuota.limit,
+ can_create: hostedQuota.canCreate,
};
+ }
case "check_builderlab_community_name":
return {
available: true,
@@ -12589,6 +12656,86 @@ export function maybeInstallE2eTauriMocks() {
},
};
}
+ case "delete_builderlab_community": {
+ if (
+ activeConfig?.mock?.builderlabDeferDeletion === true ||
+ activeConfig?.mock?.builderlabDeferDeletion === "initial"
+ ) {
+ await new Promise((resolve) => {
+ 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
+ ? sequence.shift()
+ : sequence[0]
+ : activeConfig?.mock?.builderlabDeletionError;
+ if (deletionError) {
+ const statusSequence =
+ activeConfig?.mock?.builderlabDeletionHttpStatusSequence;
+ const configuredStatus = statusSequence?.length
+ ? statusSequence.length > 1
+ ? statusSequence.shift()
+ : statusSequence[0]
+ : undefined;
+ return {
+ http_status:
+ configuredStatus ??
+ (deletionError.code === "not_owner"
+ ? 404
+ : deletionError.code === "deletion_aborted" ||
+ deletionError.code === "must_archive" ||
+ deletionError.code === "protected_target" ||
+ deletionError.code === "deletion_conflict"
+ ? 409
+ : deletionError.code === "acceptance_unknown"
+ ? 503
+ : 400),
+ body: {
+ error: deletionError,
+ correlation_id: "mock-delete-correlation",
+ ...(activeConfig?.mock?.builderlabDeletionBodyStatus === undefined
+ ? {}
+ : {
+ http_status: activeConfig.mock.builderlabDeletionBodyStatus,
+ }),
+ },
+ };
+ }
+ const input = payload as {
+ communityId?: string;
+ host?: string;
+ requestId?: string;
+ acknowledgementVersion?: number;
+ };
+ const statusSequence =
+ activeConfig?.mock?.builderlabDeletionStatusSequence;
+ return {
+ http_status: 202,
+ body: {
+ community_id: input.communityId,
+ host: input.host,
+ request_id: input.requestId,
+ acknowledgement_version: input.acknowledgementVersion,
+ status: statusSequence?.length
+ ? statusSequence.length > 1
+ ? statusSequence.shift()
+ : statusSequence[0]
+ : "submitted",
+ },
+ };
+ }
case "mesh_installed_models":
return mockMeshState.models;
case "mesh_model_catalog":
diff --git a/desktop/tests/e2e/hosted-communities-settings-screenshots.spec.ts b/desktop/tests/e2e/hosted-communities-settings-screenshots.spec.ts
index 40ea5fd3703..9d9ae6f1563 100644
--- a/desktop/tests/e2e/hosted-communities-settings-screenshots.spec.ts
+++ b/desktop/tests/e2e/hosted-communities-settings-screenshots.spec.ts
@@ -9,6 +9,43 @@ const OUTDIR = "test-results/hosted-communities";
const DEFAULT_MOCK_PUBKEY = "deadbeef".repeat(8);
/** A second valid identity key, used only as a contradictory hosted npub. */
const OTHER_HEX = "b".repeat(64);
+const PENDING_KEY = "buzz:hosted-community-delete-pending:v1";
+const OTHER_PENDING = {
+ community_id: "44444444-4444-4444-8444-444444444444",
+ host: "private-other.communities.buzz.xyz",
+ request_id: "55555555-5555-4555-8555-555555555555",
+ acknowledgement_version: 1,
+ bound_owner_pubkey: "a".repeat(64),
+ backend_origin: "https://app.builderlab.xyz",
+};
+const OTHER_PENDING_BYTES = JSON.stringify(OTHER_PENDING);
+const BLOCKED_COPY = `A deletion request from ${npubEncode(OTHER_PENDING.bound_owner_pubkey)} is still pending on this device. Switch to that Buzz identity and use Check deletion status before starting another deletion here. If you no longer have that identity, contact support.`;
+const CAPABILITY_OFF_COPY =
+ "Community deletion is unavailable right now, so this request can't be checked. It stays saved on this device.";
+const DELETION_COMMUNITIES: Array<{
+ id: string;
+ name: string;
+ normalized_host: string;
+ archived_at?: string | null;
+}> = [
+ {
+ id: "11111111-1111-4111-8111-111111111111",
+ name: "Active team",
+ normalized_host: "active.communities.buzz.xyz",
+ },
+ {
+ id: "22222222-2222-4222-8222-222222222222",
+ name: "Archived team",
+ normalized_host: "Exact-Host.communities.buzz.xyz",
+ archived_at: "2026-09-28T00:00:00Z",
+ },
+];
+const SECOND_ARCHIVED = {
+ id: "33333333-3333-4333-8333-333333333333",
+ name: "Second archived team",
+ normalized_host: "second.communities.buzz.xyz",
+ archived_at: "2026-09-28T00:00:00Z",
+};
/**
* Install the default hosted-communities fixture and open its settings
@@ -50,6 +87,1215 @@ test.beforeEach(async ({ page }) => {
await openHostedCommunitiesSettings(page);
});
+async function openDeletionFixture(
+ page: Page,
+ options: {
+ capability?: boolean;
+ mismatch?: boolean;
+ errorCode?: string;
+ errorSequence?: Array<{ code: string; message?: string } | null>;
+ rejectSequence?: boolean[];
+ statusSequence?: string[];
+ capabilitySequence?: boolean[];
+ quota?: { used: number; limit: number; canCreate: boolean };
+ communitiesSequence?: Array;
+ communities?: Array<(typeof DELETION_COMMUNITIES)[number]>;
+ httpStatusSequence?: number[];
+ bodyStatus?: number;
+ identityResponseSequence?: Array<
+ | { identity: { npub?: string; pubkey_hex?: string } }
+ | { error: { code: string; setup_needed?: boolean } }
+ >;
+ deferDeletion?: boolean | "initial";
+ } = {},
+) {
+ await installMockBridge(page, {
+ builderlabAuth: {
+ email: "owner@example.com",
+ expiresAt: "2099-01-01T00:00:00Z",
+ canDeleteBuzzCommunities: options.capability,
+ },
+ builderlabIdentity: {
+ pubkey_hex: options.mismatch ? "f".repeat(64) : DEFAULT_MOCK_PUBKEY,
+ },
+ builderlabCommunities: options.communities ?? DELETION_COMMUNITIES,
+ builderlabCommunitiesSequence: options.communitiesSequence,
+ builderlabQuota: options.quota ?? { used: 2, limit: 5, canCreate: true },
+ builderlabDeletionError: options.errorCode
+ ? { code: options.errorCode, message: "mock deletion error" }
+ : undefined,
+ builderlabDeletionErrorSequence: options.errorSequence,
+ builderlabDeletionRejectSequence: options.rejectSequence,
+ builderlabDeletionStatusSequence: options.statusSequence,
+ builderlabDeletionHttpStatusSequence: options.httpStatusSequence,
+ builderlabDeletionBodyStatus: options.bodyStatus,
+ builderlabIdentityResponseSequence: options.identityResponseSequence,
+ builderlabDeferDeletion: options.deferDeletion,
+ builderlabAuthSequence: options.capabilitySequence?.map((capability) => ({
+ email: "owner@example.com",
+ expiresAt: "2099-01-01T00:00:00Z",
+ canDeleteBuzzCommunities: capability,
+ })),
+ });
+ await page.goto("/");
+ await openSettings(page, "hosted-communities");
+}
+
+async function startArchivedDeletion(page: Page) {
+ const exactHost = "Exact-Host.communities.buzz.xyz";
+ await page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" })
+ .filter({ hasNotText: "Second archived team" })
+ .getByRole("button", { name: "Delete", exact: true })
+ .click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .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(
+ "buzz:hosted-community-delete-pending:v1",
+ );
+ return raw ? JSON.parse(raw).request_id : null;
+ });
+}
+
+async function storedDeletionBytes(page: Page) {
+ return page.evaluate(() =>
+ window.localStorage.getItem("buzz:hosted-community-delete-pending:v1"),
+ );
+}
+
+async function deletionPayloads(page: Page) {
+ return page.evaluate(() =>
+ window.__BUZZ_E2E_COMMAND_PAYLOADS__
+ ?.filter(({ command }) => command === "delete_builderlab_community")
+ .map(({ payload }) => payload),
+ );
+}
+
+async function seedOtherOwnerPending(page: Page) {
+ await page.evaluate(
+ ({ key, bytes }) => window.localStorage.setItem(key, bytes),
+ { key: PENDING_KEY, bytes: OTHER_PENDING_BYTES },
+ );
+}
+
+test("another Buzz identity's pending slot disables archived Delete without revealing its target", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, { capability: true });
+ await seedOtherOwnerPending(page);
+ await page.getByRole("button", { name: "Refresh" }).click();
+
+ const notice = page.getByText(BLOCKED_COPY, { exact: true });
+ await expect(notice).toBeVisible();
+ await expect(notice).not.toContainText(OTHER_PENDING.host);
+ await expect(notice).not.toContainText(OTHER_PENDING.community_id);
+ await expect(notice).not.toContainText(OTHER_PENDING.request_id);
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" })
+ .getByRole("button", { name: "Delete", exact: true }),
+ ).toBeDisabled();
+ expect(await storedDeletionBytes(page)).toBe(OTHER_PENDING_BYTES);
+ expect(await deletionPayloads(page)).toHaveLength(0);
+});
+
+test("an occupied slot discovered during confirmation never sends or overwrites", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, { capability: true });
+ const archived = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" });
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(
+ "Type the exact host to continue: Exact-Host.communities.buzz.xyz",
+ )
+ .fill("Exact-Host.communities.buzz.xyz");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await seedOtherOwnerPending(page);
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+
+ await expect(page.getByText(BLOCKED_COPY, { exact: true })).toBeVisible();
+ await expect(
+ page.getByText(/Could not safely save the pending deletion request/),
+ ).toHaveCount(0);
+ expect(await storedDeletionBytes(page)).toBe(OTHER_PENDING_BYTES);
+ expect(await deletionPayloads(page)).toHaveLength(0);
+});
+
+test("another identity's blocked notice is hidden when deletion capability is off", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, { capability: false });
+ await seedOtherOwnerPending(page);
+ await page.getByRole("button", { name: "Refresh" }).click();
+
+ await expect(page.getByText(BLOCKED_COPY, { exact: true })).toHaveCount(0);
+ await expect(page.getByText(/pending deletion on this device/i)).toHaveCount(
+ 0,
+ );
+ expect(await storedDeletionBytes(page)).toBe(OTHER_PENDING_BYTES);
+ expect(await deletionPayloads(page)).toHaveLength(0);
+});
+
+test("own pending request explains why Check is disabled when deletion capability is off", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, { capability: false });
+ const ownedBytes = JSON.stringify({
+ ...OTHER_PENDING,
+ bound_owner_pubkey: DEFAULT_MOCK_PUBKEY,
+ });
+ await page.evaluate(
+ ({ key, bytes }) => window.localStorage.setItem(key, bytes),
+ { key: PENDING_KEY, bytes: ownedBytes },
+ );
+ await page.getByRole("button", { name: "Refresh" }).click();
+
+ await expect(
+ page.getByText(CAPABILITY_OFF_COPY, { exact: true }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toBeDisabled();
+ expect(await storedDeletionBytes(page)).toBe(ownedBytes);
+ expect(await deletionPayloads(page)).toHaveLength(0);
+});
+
+test("reopen sends nothing and Check resends the same saved request once", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }, null],
+ });
+ await startArchivedDeletion(page);
+ await expect(page.getByText(/Deletion acceptance for/)).toBeVisible();
+ const firstId = await storedDeletionRequestId(page);
+ expect(firstId).not.toBeNull();
+ await page.keyboard.press("Escape");
+ await expect(page.getByTestId("settings-view")).toHaveCount(0);
+ await openSettings(page, "hosted-communities");
+ await expect(page.getByText(firstId, { exact: true })).toBeVisible();
+ await expect.poll(() => storedDeletionRequestId(page)).toBe(firstId);
+ expect(
+ await page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length,
+ ),
+ ).toBe(1);
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length,
+ ),
+ )
+ .toBe(2);
+ await expect.poll(() => storedDeletionRequestId(page)).toBeNull();
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+});
+
+test("deletion is default-off and identity mismatch preserves the gate", async ({
+ page,
+}) => {
+ await openDeletionFixture(page);
+ await expect(
+ page.getByRole("button", { name: "Delete", exact: true }),
+ ).toHaveCount(0);
+
+ await openDeletionFixture(page, { capability: true, mismatch: true });
+ await expect(
+ page.getByRole("button", { name: "Delete", exact: true }),
+ ).toHaveCount(0);
+});
+
+test("explicit quota false hides Create and shows the limit copy", async ({
+ page,
+}) => {
+ await openDeletionFixture(page);
+ await page.evaluate(() => {
+ if (window.__BUZZ_E2E__?.mock)
+ window.__BUZZ_E2E__.mock.builderlabQuota = {
+ used: 5,
+ limit: 5,
+ canCreate: false,
+ };
+ });
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(
+ page.getByText(/reached the limit of 5 hosted communities/),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Create and connect" }),
+ ).toHaveCount(0);
+});
+
+test("zero community quota does not promise a deletion will free a slot", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ quota: { used: 2, limit: 0, canCreate: false },
+ });
+ await expect(
+ page.getByText("You can't create more communities right now.", {
+ exact: true,
+ }),
+ ).toBeVisible();
+ await expect(page.getByText(/deletion frees its slot/i)).toHaveCount(0);
+});
+
+test("archived deletion requires exact host and two confirmations, then removes the row", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, { capability: true });
+ const active = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Active team" });
+ const archived = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" });
+ await expect(
+ active.getByRole("button", { name: "Delete", exact: true }),
+ ).toHaveCount(0);
+
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ const exactHost = "Exact-Host.communities.buzz.xyz";
+ const input = page.getByLabel(
+ `Type the exact host to continue: ${exactHost}`,
+ );
+ await input.fill(exactHost.toLowerCase());
+ await expect(page.getByRole("button", { name: "Continue" })).toBeDisabled();
+ await input.fill(` ${exactHost}`);
+ await expect(page.getByRole("button", { name: "Continue" })).toBeDisabled();
+ await page.getByRole("button", { name: "Cancel" }).click();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length ?? 0,
+ ),
+ )
+ .toBe(0);
+
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await expect(
+ page.getByRole("button", { name: "Delete community permanently" }),
+ ).toBeVisible();
+ await page.getByRole("button", { name: "Cancel" }).click();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length ?? 0,
+ ),
+ )
+ .toBe(0);
+
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .dblclick();
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ await expect(archived).toHaveCount(0);
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length ?? 0,
+ ),
+ )
+ .toBe(1);
+});
+
+test("ambiguous deletion keeps the same pending request and exposes manual same-UUID check", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorCode: "acceptance_unknown",
+ });
+ const archived = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" });
+ const exactHost = "Exact-Host.communities.buzz.xyz";
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toBeVisible();
+ await expect(
+ page.getByText(/Correlation ID: mock-delete-correlation/),
+ ).toBeVisible();
+ await expect(archived).toBeVisible();
+});
+
+test("transient identity loss hides but retains an ambiguous envelope and restores it for the same owner", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorCode: "acceptance_unknown",
+ identityResponseSequence: [
+ { identity: { pubkey_hex: DEFAULT_MOCK_PUBKEY } },
+ { error: { code: "unauthorized" } },
+ { identity: { pubkey_hex: DEFAULT_MOCK_PUBKEY } },
+ ],
+ });
+ await startArchivedDeletion(page);
+ const requestId = await storedDeletionRequestId(page);
+ expect(requestId).not.toBeNull();
+
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(page.getByText(requestId, { exact: true })).toHaveCount(0);
+ await expect.poll(() => storedDeletionRequestId(page)).toBe(requestId);
+
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(page.getByText(requestId, { exact: true })).toBeVisible();
+ await expect.poll(() => storedDeletionRequestId(page)).toBe(requestId);
+});
+
+test("A-B-A owner switch retains exact bytes and replays only under A", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }, null],
+ identityResponseSequence: [
+ { identity: { pubkey_hex: DEFAULT_MOCK_PUBKEY } },
+ { identity: { pubkey_hex: OTHER_HEX } },
+ { identity: { pubkey_hex: DEFAULT_MOCK_PUBKEY } },
+ ],
+ });
+ await startArchivedDeletion(page);
+ const requestId = await storedDeletionRequestId(page);
+ const bytes = await storedDeletionBytes(page);
+ const firstCalls = await deletionPayloads(page);
+ expect(firstCalls).toHaveLength(1);
+
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(page.getByText(requestId, { exact: true })).toHaveCount(0);
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toHaveCount(0);
+ await expect(
+ page.getByText(
+ /A deletion request from npub1.* is still pending on this device/,
+ ),
+ ).toBeVisible();
+ await expect.poll(() => storedDeletionBytes(page)).toBe(bytes);
+ expect(await deletionPayloads(page)).toHaveLength(1);
+
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(page.getByText(requestId, { exact: true })).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toBeVisible();
+ await expect.poll(() => storedDeletionBytes(page)).toBe(bytes);
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect
+ .poll(() => deletionPayloads(page))
+ .toEqual([firstCalls?.[0], firstCalls?.[0]]);
+});
+
+test("sign out hides but retains an ambiguous envelope for same-owner reauthentication", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorCode: "acceptance_unknown",
+ });
+ await startArchivedDeletion(page);
+ const requestId = await storedDeletionRequestId(page);
+
+ await page.getByRole("button", { name: "Sign out" }).click();
+ await expect(page.getByText(requestId, { exact: true })).toHaveCount(0);
+ await expect.poll(() => storedDeletionRequestId(page)).toBe(requestId);
+
+ await page.getByRole("button", { name: "Sign in" }).click();
+ await expect(page.getByText(requestId, { exact: true })).toBeVisible();
+ await expect.poll(() => storedDeletionRequestId(page)).toBe(requestId);
+});
+
+test("late A response cannot settle after a valid A-B-A owner transition", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ deferDeletion: true,
+ });
+ await startArchivedDeletion(page);
+ await expect.poll(() => storedDeletionRequestId(page)).not.toBeNull();
+ const bytes = await storedDeletionBytes(page);
+
+ await page.keyboard.press("Escape");
+ await expect(page.getByTestId("settings-view")).toHaveCount(0);
+ await page.evaluate((pubkey) => {
+ if (window.__BUZZ_E2E__?.mock) {
+ window.__BUZZ_E2E__.mock.builderlabIdentity = { pubkey_hex: pubkey };
+ }
+ }, OTHER_HEX);
+ await openSettings(page, "hosted-communities");
+ await expect.poll(() => storedDeletionBytes(page)).toBe(bytes);
+ await expect(
+ page.getByText("This account is connected to a different Buzz identity"),
+ ).toBeVisible();
+ await page.keyboard.press("Escape");
+ await expect(page.getByTestId("settings-view")).toHaveCount(0);
+ await page.evaluate((pubkey) => {
+ if (window.__BUZZ_E2E__?.mock) {
+ window.__BUZZ_E2E__.mock.builderlabIdentity = { pubkey_hex: pubkey };
+ }
+ }, DEFAULT_MOCK_PUBKEY);
+ await openSettings(page, "hosted-communities");
+ await expect(
+ page.getByText("This account is connected to a different Buzz identity"),
+ ).toHaveCount(0);
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toBeVisible();
+ await expect
+ .poll(() =>
+ page.evaluate(() => window.__BUZZ_E2E_RELEASE_BUILDERLAB_DELETIONS__?.()),
+ )
+ .toBe(1);
+ await expect(page.getByText("Deletion started", { exact: true })).toHaveCount(
+ 0,
+ );
+ await expect.poll(() => storedDeletionBytes(page)).toBe(bytes);
+});
+
+test("a changed persisted request fences Check before dispatch in one mounted card", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }, null],
+ });
+ await startArchivedDeletion(page);
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toBeVisible();
+ const replacement = await page.evaluate(() => {
+ const key = "buzz:hosted-community-delete-pending:v1";
+ const raw = window.localStorage.getItem(key);
+ if (!raw) throw new Error("missing original envelope");
+ const before =
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "get_builderlab_auth",
+ ).length ?? 0;
+ const button = [...document.querySelectorAll("button")].find((candidate) =>
+ candidate.textContent?.includes("Check deletion status"),
+ );
+ if (!(button instanceof HTMLButtonElement))
+ throw new Error("missing Check button");
+ button.click();
+ const after =
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "get_builderlab_auth",
+ ).length ?? 0;
+ if (after !== before + 1)
+ throw new Error("Check did not enter the auth preflight");
+ const next = {
+ ...JSON.parse(raw),
+ request_id: "44444444-4444-4444-8444-444444444444",
+ };
+ const bytes = JSON.stringify(next);
+ window.localStorage.setItem(key, bytes);
+ return bytes;
+ });
+ await expect.poll(() => storedDeletionBytes(page)).toBe(replacement);
+ await expect.poll(async () => (await deletionPayloads(page))?.length).toBe(1);
+ await expect(page.getByText("Deletion started", { exact: true })).toHaveCount(
+ 0,
+ );
+});
+
+test("a pre-remount acceptance cannot erase a later uncertain request", async ({
+ page,
+}) => {
+ const secondArchived = {
+ id: "33333333-3333-4333-8333-333333333333",
+ name: "Second archived team",
+ normalized_host: "second.communities.buzz.xyz",
+ archived_at: "2026-09-28T00:00:00Z",
+ };
+ await openDeletionFixture(page, {
+ capability: true,
+ communities: [...DELETION_COMMUNITIES, secondArchived],
+ deferDeletion: "initial",
+ errorSequence: [null, { code: "acceptance_unknown" }, null],
+ });
+ await startArchivedDeletion(page);
+ const firstId = await storedDeletionRequestId(page);
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length ?? 0,
+ ),
+ )
+ .toBe(1);
+ await page.evaluate(() => {
+ if (window.__BUZZ_E2E__?.mock)
+ window.__BUZZ_E2E__.mock.builderlabDeferDeletion = false;
+ });
+
+ expect(firstId).not.toBeNull();
+
+ await page.keyboard.press("Escape");
+ await expect(page.getByTestId("settings-view")).toHaveCount(0);
+ await openSettings(page, "hosted-communities");
+ await expect.poll(() => storedDeletionRequestId(page)).toBe(firstId);
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect.poll(() => storedDeletionRequestId(page)).toBeNull();
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+
+ const second = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Second archived team" });
+ await second.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel("Type the exact host to continue: second.communities.buzz.xyz")
+ .fill("second.communities.buzz.xyz");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+ const pendingKey = "buzz:hosted-community-delete-pending:v1";
+ const secondBytes = await page.evaluate(
+ (key) => window.localStorage.getItem(key),
+ pendingKey,
+ );
+ expect(secondBytes).not.toBeNull();
+ expect(JSON.parse(secondBytes as string).request_id).not.toBe(firstId);
+ await expect(
+ page.getByText(/Deletion acceptance is uncertain/),
+ ).toBeVisible();
+
+ expect(
+ await page.evaluate(() =>
+ window.__BUZZ_E2E_RELEASE_BUILDERLAB_DELETIONS__?.(),
+ ),
+ ).toBe(1);
+ await expect
+ .poll(() =>
+ page.evaluate((key) => window.localStorage.getItem(key), pendingKey),
+ )
+ .toBe(secondBytes);
+ await expect(
+ page.getByText(JSON.parse(secondBytes as string).request_id, {
+ exact: true,
+ }),
+ ).toBeVisible();
+});
+
+test("one pending envelope blocks a second mounted deletion without overwriting or dispatching", async ({
+ page,
+}) => {
+ const secondArchived = {
+ id: "33333333-3333-4333-8333-333333333333",
+ name: "Second archived team",
+ normalized_host: "second.communities.buzz.xyz",
+ archived_at: "2026-09-28T00:00:00Z",
+ };
+ await openDeletionFixture(page, {
+ capability: true,
+ errorCode: "acceptance_unknown",
+ communities: [...DELETION_COMMUNITIES, secondArchived],
+ });
+ const first = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" })
+ .filter({ hasNotText: "Second archived team" });
+ await first.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(
+ "Type the exact host to continue: Exact-Host.communities.buzz.xyz",
+ )
+ .fill("Exact-Host.communities.buzz.xyz");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+
+ const firstRequestId = await page.evaluate(() => {
+ const raw = window.localStorage.getItem(
+ "buzz:hosted-community-delete-pending:v1",
+ );
+ return raw ? JSON.parse(raw).request_id : null;
+ });
+ expect(firstRequestId).not.toBeNull();
+
+ const secondDelete = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Second archived team" })
+ .getByRole("button", { name: "Delete", exact: true });
+ await secondDelete.evaluate((button: HTMLButtonElement) => button.click());
+ const secondHostInput = page.getByLabel(
+ "Type the exact host to continue: second.communities.buzz.xyz",
+ );
+ if (await secondHostInput.isVisible().catch(() => false)) {
+ await secondHostInput.fill("second.communities.buzz.xyz");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+ }
+
+ await expect(secondDelete).toBeDisabled();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length ?? 0,
+ ),
+ )
+ .toBe(1);
+ await expect
+ .poll(() =>
+ page.evaluate(() => {
+ const raw = window.localStorage.getItem(
+ "buzz:hosted-community-delete-pending:v1",
+ );
+ return raw ? JSON.parse(raw).request_id : null;
+ }),
+ )
+ .toBe(firstRequestId);
+});
+
+test("native HTTP status wins over a contradictory body claim in the mounted flow", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorCode: "not_owner",
+ httpStatusSequence: [503],
+ bodyStatus: 404,
+ });
+ const archived = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" });
+ const exactHost = "Exact-Host.communities.buzz.xyz";
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+
+ const requestId = await page.evaluate(() => {
+ const raw = window.localStorage.getItem(
+ "buzz:hosted-community-delete-pending:v1",
+ );
+ return raw ? JSON.parse(raw).request_id : null;
+ });
+ expect(requestId).not.toBeNull();
+ await expect(page.getByText(requestId, { exact: true })).toBeVisible();
+ await expect(archived).toBeVisible();
+});
+
+test("ambiguous check not_owner settles the same request UUID", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }, { code: "not_owner" }],
+ capabilitySequence: [true, true, true],
+ communitiesSequence: [DELETION_COMMUNITIES, []],
+ });
+ const archived = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" });
+ const exactHost = "Exact-Host.communities.buzz.xyz";
+ await archived.getByRole("button", { name: "Delete", exact: true }).click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+ const requestId = await page.evaluate(() => {
+ const raw = window.localStorage.getItem(
+ "buzz:hosted-community-delete-pending:v1",
+ );
+ return raw ? JSON.parse(raw).request_id : null;
+ });
+ await expect(page.getByText(requestId, { exact: true })).toBeVisible();
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect(page.getByText(/Only the community owner/)).toBeVisible();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMAND_PAYLOADS__?.filter(
+ ({ command }) => command === "delete_builderlab_community",
+ ).length,
+ ),
+ )
+ .toBe(2);
+ await expect(page.getByText(requestId, { exact: true })).toHaveCount(0);
+ await expect
+ .poll(() =>
+ page.evaluate(() => {
+ const raw = window.localStorage.getItem(
+ "buzz:hosted-community-delete-pending:v1",
+ );
+ return raw ? JSON.parse(raw).request_id : null;
+ }),
+ )
+ .toBeNull();
+});
+
+test("check rechecks capability after the fresh owner list", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }],
+ capabilitySequence: [true, true, false],
+ communitiesSequence: [DELETION_COMMUNITIES, DELETION_COMMUNITIES],
+ });
+ const exactHost = "Exact-Host.communities.buzz.xyz";
+ await page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" })
+ .getByRole("button", { name: "Delete", exact: true })
+ .click();
+ await page
+ .getByLabel(`Type the exact host to continue: ${exactHost}`)
+ .fill(exactHost);
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page
+ .getByRole("button", { name: "Delete community permanently" })
+ .click();
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect(
+ page.getByText(/Community deletion is no longer enabled/),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: "Check deletion status" }),
+ ).toBeDisabled();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMANDS__?.filter(
+ (command) => command === "delete_builderlab_community",
+ ).length ?? 0,
+ ),
+ )
+ .toBe(1);
+});
+
+test("check resends the saved UUID even when the fresh owner list omits it", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }, null],
+ capabilitySequence: [true, true, true],
+ communitiesSequence: [DELETION_COMMUNITIES, []],
+ });
+ await startArchivedDeletion(page);
+ const requestId = await storedDeletionRequestId(page);
+ expect(requestId).not.toBeNull();
+ const firstCalls = await page.evaluate(() =>
+ window.__BUZZ_E2E_COMMAND_PAYLOADS__
+ ?.filter(({ command }) => command === "delete_builderlab_community")
+ .map(({ payload }) => payload),
+ );
+ expect(firstCalls).toEqual([
+ {
+ communityId: DELETION_COMMUNITIES[1].id,
+ host: DELETION_COMMUNITIES[1].normalized_host,
+ requestId,
+ acknowledgementVersion: 1,
+ },
+ ]);
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect
+ .poll(() =>
+ page.evaluate(() =>
+ window.__BUZZ_E2E_COMMAND_PAYLOADS__
+ ?.filter(({ command }) => command === "delete_builderlab_community")
+ .map(({ payload }) => payload),
+ ),
+ )
+ .toEqual([firstCalls?.[0], firstCalls?.[0]]);
+ await expect.poll(() => storedDeletionRequestId(page)).toBeNull();
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+});
+
+test("Refresh replays an accepted request and restores a tuple-bound aborted row", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ statusSequence: ["submitted", "aborted"],
+ });
+ await startArchivedDeletion(page);
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ const firstCalls = await deletionPayloads(page);
+ expect(firstCalls).toHaveLength(1);
+ expect(await storedDeletionBytes(page)).toBeNull();
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" }),
+ ).toHaveCount(0);
+
+ await page.getByRole("button", { name: "Refresh" }).click();
+ const archived = page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" });
+ await expect(archived).toBeVisible();
+ await expect(
+ archived.getByRole("button", { name: "Delete", exact: true }),
+ ).toBeEnabled();
+ await expect(page.getByText("2 of 5 used", { exact: false })).toBeVisible();
+ await expect(page.getByTestId("hosted-community-row")).toHaveCount(2);
+ await expect(
+ page.getByText("Deletion stopped. This community is not being deleted.", {
+ exact: true,
+ }),
+ ).toBeVisible();
+ await expect(page.getByText("Deletion started", { exact: true })).toHaveCount(
+ 0,
+ );
+ await expect
+ .poll(() => deletionPayloads(page))
+ .toEqual([firstCalls?.[0], firstCalls?.[0]]);
+ expect(await storedDeletionBytes(page)).toBeNull();
+});
+
+test("Refresh restores only the aborted row while another accepted deletion remains", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ communities: [...DELETION_COMMUNITIES, SECOND_ARCHIVED],
+ quota: { used: 3, limit: 5, canCreate: true },
+ statusSequence: ["submitted", "submitted", "aborted", "approved"],
+ });
+ await startArchivedDeletion(page);
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ await startSecondArchivedDeletion(page);
+ await expect.poll(() => deletionPayloads(page)).toHaveLength(2);
+ const acceptedCalls = await deletionPayloads(page);
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Second archived team" }),
+ ).toHaveCount(0);
+
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" }),
+ ).toBeVisible();
+ 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 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 ({
+ page,
+ }) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ statusSequence: scenario.statusSequence,
+ errorSequence: scenario.errorSequence,
+ });
+ await startArchivedDeletion(page);
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ const firstCalls = await deletionPayloads(page);
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect
+ .poll(() => deletionPayloads(page))
+ .toEqual([firstCalls?.[0], firstCalls?.[0]]);
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" }),
+ ).toHaveCount(0);
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ await expect(
+ page.getByText("Deletion stopped. This community is not being deleted.", {
+ 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);
+ }
+ });
+}
+
+test("Refresh with deletion capability off keeps an accepted row hidden without replay", async ({
+ page,
+}) => {
+ await openDeletionFixture(page, {
+ capability: true,
+ capabilitySequence: [true, false],
+ communities: [...DELETION_COMMUNITIES, SECOND_ARCHIVED],
+ });
+ await startArchivedDeletion(page);
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ const firstCalls = await deletionPayloads(page);
+ await page.getByRole("button", { name: "Refresh" }).click();
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" })
+ .filter({ hasNotText: "Second archived team" }),
+ ).toHaveCount(0);
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Second archived team" })
+ .getByRole("button", { name: "Delete", exact: true }),
+ ).toHaveCount(0);
+ await expect(
+ page.getByText("Deletion started", { exact: true }),
+ ).toBeVisible();
+ expect(await deletionPayloads(page)).toEqual(firstCalls);
+});
+
+test("check settles must_archive and offers Archive after the owner row is unarchived", async ({
+ page,
+}) => {
+ const unarchived = DELETION_COMMUNITIES.map((community) =>
+ community.id === DELETION_COMMUNITIES[1].id
+ ? { ...community, archived_at: null }
+ : community,
+ );
+ await openDeletionFixture(page, {
+ capability: true,
+ errorSequence: [{ code: "acceptance_unknown" }, { code: "must_archive" }],
+ capabilitySequence: [true, true, true],
+ communitiesSequence: [DELETION_COMMUNITIES, unarchived],
+ });
+ await startArchivedDeletion(page);
+ const requestId = await storedDeletionRequestId(page);
+ expect(requestId).not.toBeNull();
+ await page.getByRole("button", { name: "Check deletion status" }).click();
+ await expect(
+ page.getByText(/Archive this community before deleting it/),
+ ).toBeVisible();
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ window.__BUZZ_E2E_COMMAND_PAYLOADS__?.filter(
+ ({ command }) => command === "delete_builderlab_community",
+ ).length,
+ ),
+ )
+ .toBe(2);
+ await expect.poll(() => storedDeletionRequestId(page)).toBeNull();
+ await expect(
+ page
+ .getByTestId("hosted-community-row")
+ .filter({ hasText: "Archived team" })
+ .getByRole("button", { name: "Archive", exact: true }),
+ ).toBeVisible();
+});
+
test("identity: mismatch rows follow pubkey_hex, never the hosted npub or raw hex", async ({
page,
}) => {
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index e945508c085..29a24e609a4 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -162,7 +162,12 @@ type MockBridgeOptions = {
/** Native-like huddle state seeded from authoritative role-bearing membership. */
huddle?: MockHuddleSeed;
/** Builderlab account returned by hosted-community onboarding. Null/omitted = signed out. */
- builderlabAuth?: { email?: string; name?: string; expiresAt: string } | null;
+ builderlabAuth?: {
+ email?: string;
+ name?: string;
+ expiresAt: string;
+ canDeleteBuzzCommunities?: boolean;
+ } | null;
/** Optional policy returned by the native join-policy discovery command. */
joinPolicy?: {
terms_markdown?: string;
@@ -172,6 +177,11 @@ type MockBridgeOptions = {
} | null;
/** Bound Builderlab Nostr identity. Null/omitted = not linked yet. */
builderlabIdentity?: { npub?: string; pubkey_hex?: string } | null;
+ /** Ordered native identity results; the final result remains sticky. */
+ builderlabIdentityResponseSequence?: Array<
+ | { identity: { npub?: string; pubkey_hex?: string } }
+ | { error: { code: string; setup_needed?: boolean } }
+ >;
/** Communities owned by the mocked Builderlab account. */
builderlabCommunities?: Array<{
id?: string;
@@ -180,6 +190,33 @@ type MockBridgeOptions = {
normalized_host?: string;
archived_at?: string | null;
}>;
+ builderlabQuota?: { used: number; limit: number; canCreate: boolean };
+ builderlabDeletionError?: { code: string; message?: string };
+ builderlabDeletionErrorSequence?: Array<{
+ code: string;
+ message?: string;
+ } | null>;
+ builderlabDeletionRejectSequence?: boolean[];
+ builderlabDeletionStatusSequence?: string[];
+ builderlabDeletionHttpStatusSequence?: number[];
+ builderlabDeletionBodyStatus?: number;
+ /** Hold deletion responses until the test explicitly releases them. */
+ builderlabDeferDeletion?: boolean | "initial";
+ builderlabAuthSequence?: Array<{
+ email?: string;
+ name?: string;
+ expiresAt: string;
+ canDeleteBuzzCommunities?: boolean;
+ } | null>;
+ builderlabCommunitiesSequence?: Array<
+ Array<{
+ id?: string;
+ name?: string;
+ slug?: string;
+ normalized_host?: string;
+ archived_at?: string | null;
+ }>
+ >;
acpRuntimesCatalog?: Record[];
/** Catalog returned after a successful mocked install. */
acpRuntimesCatalogAfterInstall?: Record[];