diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx
index 75c378ad23..01dda1c1cb 100644
--- a/desktop/src/app/App.tsx
+++ b/desktop/src/app/App.tsx
@@ -90,6 +90,24 @@ const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200;
const BOOT_SPLASH_FADE_MS = 200;
const INITIAL_RENDER_READY_EVENT = "initial-render-ready";
+function incrementE2eAppReadyMounts() {
+ const e2eWindow = window as Window & {
+ __BUZZ_E2E__?: unknown;
+ __BUZZ_E2E_APP_READY_MOUNTS__?: number;
+ };
+ if (!e2eWindow.__BUZZ_E2E__) return;
+ e2eWindow.__BUZZ_E2E_APP_READY_MOUNTS__ =
+ (e2eWindow.__BUZZ_E2E_APP_READY_MOUNTS__ ?? 0) + 1;
+}
+
+function E2eAppReadyMountTelemetry() {
+ useEffect(() => {
+ incrementE2eAppReadyMounts();
+ }, []);
+
+ return null;
+}
+
type BootSplashPhase = "holding" | "fading" | "done";
type CommunityOnboardingWorkOwner = {
@@ -335,17 +353,34 @@ function AppReady({
isCommunitySwitch: boolean;
}) {
const onboarding = useAppOnboardingState(isSharedIdentity);
+ const isE2eBuild = import.meta.env.MODE === "e2e";
+ const e2eTelemetry = isE2eBuild ? : null;
if (onboarding.stage === "reset-failed") {
- return ;
+ return (
+ <>
+ {e2eTelemetry}
+
+ >
+ );
}
if (onboarding.stage === "keyring-locked") {
- return ;
+ return (
+ <>
+ {e2eTelemetry}
+
+ >
+ );
}
if (onboarding.stage === "relaunch-required") {
- return ;
+ return (
+ <>
+ {e2eTelemetry}
+
+ >
+ );
}
if (
@@ -353,35 +388,46 @@ function AppReady({
(continueOnboarding && onboarding.stage === "blocking")
) {
return (
-
+ <>
+ {e2eTelemetry}
+
+ >
);
}
if (onboarding.stage === "blocking") {
- return isCommunitySwitch ? : ;
+ return (
+ <>
+ {e2eTelemetry}
+ {isCommunitySwitch ? : }
+ >
+ );
}
return (
-
- void router.navigate({
- to: "/settings",
- search: { section: "profile" },
- })
- }
- >
-
-
-
-
+ <>
+ {e2eTelemetry}
+
+ void router.navigate({
+ to: "/settings",
+ search: { section: "profile" },
+ })
+ }
+ >
+
+
+
+
+ >
);
}
@@ -457,9 +503,28 @@ function CommunityApp({
const isContinuingOnboarding = continueOnboarding && !isCommunitySwitch;
const transaction = communityOnboarding.transaction;
+ // The onboarding transaction is temporary control-plane state. Retiring it
+ // must not change the generation of an init that already applied this
+ // community, or useCommunityInit will tear down and apply the same
+ // workspace again. Update this during render so the clear() render already
+ // has the retired transaction's generation; keep the community id with it so
+ // another community can never inherit the value.
+ const lastCommunityInitAttemptRef = useRef<{
+ communityId: string;
+ initAttempt: number;
+ } | null>(null);
+ const transactionOwnsActiveCommunity =
+ transaction !== null && transaction.communityId === activeCommunity?.id;
+ if (transactionOwnsActiveCommunity && activeCommunity) {
+ lastCommunityInitAttemptRef.current = {
+ communityId: activeCommunity.id,
+ initAttempt: transaction.initAttempt ?? 0,
+ };
+ }
const communityInitAttempt =
- transaction && transaction.communityId === activeCommunity?.id
- ? (transaction.initAttempt ?? 0)
+ activeCommunity &&
+ lastCommunityInitAttemptRef.current?.communityId === activeCommunity.id
+ ? lastCommunityInitAttemptRef.current.initAttempt
: -1;
const community = useCommunityInit(
activeCommunity,
diff --git a/desktop/src/features/communities/useCommunityInit.ts b/desktop/src/features/communities/useCommunityInit.ts
index ffb9df2bac..ed0666e0cb 100644
--- a/desktop/src/features/communities/useCommunityInit.ts
+++ b/desktop/src/features/communities/useCommunityInit.ts
@@ -71,6 +71,13 @@ async function resetCommunityState({
}: {
resetAvatarState: boolean;
}): Promise {
+ if (import.meta.env.MODE === "e2e") {
+ const e2eWindow = window as Window & {
+ __BUZZ_E2E_COMMUNITY_RESETS__?: number;
+ };
+ e2eWindow.__BUZZ_E2E_COMMUNITY_RESETS__ =
+ (e2eWindow.__BUZZ_E2E_COMMUNITY_RESETS__ ?? 0) + 1;
+ }
relayClient.disconnect();
await resetNavigationDeepLinkDrain();
resetRateLimitGate();
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 882a41349a..1a88d54093 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -403,6 +403,8 @@ type E2eConfig = {
/** Delay (ms) for `apply_workspace` so e2e tests can observe the
* community-switch gate. 0/undefined = instant. */
applyCommunityDelayMs?: number;
+ /** Hold `apply_workspace` until the E2E release seam is called. */
+ deferApplyCommunity?: boolean;
/** Reject `clear_pending_navigation_deep_links` with this message. */
clearPendingNavigationDeepLinksError?: string;
openDmDelayMs?: number;
@@ -1275,6 +1277,14 @@ declare global {
/** Last payload written through the native clipboard command. */
__BUZZ_E2E_LAST_CLIPBOARD__?: { html: string | null; text: string };
__BUZZ_E2E_COMMANDS__?: string[];
+ /** Number of community-scoped reset passes through useCommunityInit. */
+ __BUZZ_E2E_COMMUNITY_RESETS__?: number;
+ /** Number of AppReady component mounts across the community boundary. */
+ __BUZZ_E2E_APP_READY_MOUNTS__?: number;
+ /** Release `apply_workspace` calls held by `deferApplyCommunity`. */
+ __BUZZ_E2E_RELEASE_APPLY_WORKSPACES__?: () => number;
+ /** Number of held `apply_workspace` calls. */
+ __BUZZ_E2E_APPLY_WORKSPACES_PENDING__?: () => number;
__BUZZ_E2E_COMMAND_PAYLOADS__?: Array<{
command: string;
payload: unknown;
@@ -1797,6 +1807,7 @@ type DeferredProfileUpdate = {
let deferredProfileReadQueue: DeferredProfileRead[] = [];
let nextProfileReadOrdinal = 1;
let profileReadsReleased = false;
+let deferredApplyCommunityQueue: Array<() => void> = [];
let deferredProfileUpdateQueue: DeferredProfileUpdate[] = [];
let nextProfileUpdateOrdinal = 1;
let profileUpdatesReleased = false;
@@ -11526,6 +11537,7 @@ export function maybeInstallE2eTauriMocks() {
deferredProfileReadQueue = [];
nextProfileReadOrdinal = 1;
profileReadsReleased = false;
+ deferredApplyCommunityQueue = [];
deferredProfileUpdateQueue = [];
nextProfileUpdateOrdinal = 1;
profileUpdatesReleased = false;
@@ -11557,6 +11569,13 @@ export function maybeInstallE2eTauriMocks() {
};
window.__BUZZ_E2E_THREAD_REPLIES_PENDING__ = () =>
deferredThreadRepliesQueue.length;
+ window.__BUZZ_E2E_RELEASE_APPLY_WORKSPACES__ = () => {
+ const queued = deferredApplyCommunityQueue.splice(0);
+ for (const release of queued) release();
+ return queued.length;
+ };
+ window.__BUZZ_E2E_APPLY_WORKSPACES_PENDING__ = () =>
+ deferredApplyCommunityQueue.length;
const releaseProfileRead = () => {
const deferred = deferredProfileReadQueue.shift();
if (!deferred) return 0;
@@ -11663,6 +11682,8 @@ export function maybeInstallE2eTauriMocks() {
}
mockWindows(config.mock?.windowLabel ?? "main");
window.__BUZZ_E2E_COMMANDS__ = [];
+ window.__BUZZ_E2E_COMMUNITY_RESETS__ = 0;
+ window.__BUZZ_E2E_APP_READY_MOUNTS__ = 0;
window.__BUZZ_E2E_COMMAND_PAYLOADS__ = [];
window.__BUZZ_E2E_COMMAND_LOG__ = [];
window.__BUZZ_E2E_OBSERVER_CONTROLS__ = [];
@@ -13088,6 +13109,17 @@ export function maybeInstallE2eTauriMocks() {
return activeConfig?.mock?.linkPreviewMetadata ?? null;
}
case "apply_workspace": {
+ if (activeConfig?.mock?.deferApplyCommunity) {
+ return new Promise((resolve) => {
+ deferredApplyCommunityQueue.push(() => {
+ window.__BUZZ_E2E_COMMAND_LOG__?.push({
+ command: "apply_workspace:released",
+ payload: null,
+ });
+ resolve();
+ });
+ });
+ }
const applyDelayMs = activeConfig?.mock?.applyCommunityDelayMs ?? 0;
if (applyDelayMs > 0) {
return new Promise((resolve) =>
diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts
index ad2b6ea2b2..b49936be0d 100644
--- a/desktop/tests/e2e/onboarding.spec.ts
+++ b/desktop/tests/e2e/onboarding.spec.ts
@@ -74,6 +74,11 @@ async function setRelayConnectionState(
const HOME_SEEN_STORAGE_KEY_PREFIX = "buzz-home-feed-seen.v1:";
const COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY =
"buzz-community-onboarding-transaction.v1";
+const WELCOME_SURFACE_READY_EVENT = "buzz:onboarding-welcome-surface-ready";
+const ENTERING_CURTAIN_FADE_MS = 500;
+const ENTERING_CURTAIN_SAFETY_MS = 8_000;
+const ENTERING_CLOCK_STEP_MS = 100;
+const ENTERING_CLOCK_MAX_ADVANCE_MS = 7_500;
const ONE_PIXEL_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
const DEFAULT_MOCK_PUBKEY = "deadbeef".repeat(8);
@@ -142,12 +147,14 @@ async function seedCommunityProfileStage(page: Page, id: string) {
async function seedConnectingCorporateProfileTransaction(
page: Page,
{
+ addedCommunity = true,
communityId = "e2e-enterprise-community",
communityName = "Enterprise",
pubkey = BLANK_TYLER_IDENTITY.pubkey,
relayUrl,
transactionId,
}: {
+ addedCommunity?: boolean;
communityId?: string;
communityName?: string;
pubkey?: string;
@@ -157,7 +164,14 @@ async function seedConnectingCorporateProfileTransaction(
) {
await seedActiveIdentity(page, { ...BLANK_TYLER_IDENTITY, pubkey });
await page.addInitScript(
- ({ communityId, communityName, pubkey, relayUrl, transactionId }) => {
+ ({
+ addedCommunity,
+ communityId,
+ communityName,
+ pubkey,
+ relayUrl,
+ transactionId,
+ }) => {
window.localStorage.setItem(
`buzz-machine-onboarding-complete.v2:${pubkey}`,
"true",
@@ -185,13 +199,20 @@ async function seedConnectingCorporateProfileTransaction(
relayUrl,
communityName,
communityId,
- addedCommunity: true,
+ addedCommunity,
createdAt: timestamp,
updatedAt: timestamp,
}),
);
},
- { communityId, communityName, pubkey, relayUrl, transactionId },
+ {
+ addedCommunity,
+ communityId,
+ communityName,
+ pubkey,
+ relayUrl,
+ transactionId,
+ },
);
}
@@ -210,6 +231,64 @@ async function readCommunityOnboardingTransaction(page: Page) {
}, COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY);
}
+async function readCommunityInitMetrics(page: Page) {
+ return page.evaluate(() => ({
+ applyWorkspaceCalls: (window.__BUZZ_E2E_COMMANDS__ ?? []).filter(
+ (command) => command === "apply_workspace",
+ ).length,
+ communityResets: window.__BUZZ_E2E_COMMUNITY_RESETS__ ?? 0,
+ appReadyMounts: window.__BUZZ_E2E_APP_READY_MOUNTS__ ?? 0,
+ }));
+}
+
+async function readCommunityRetirementState(page: Page) {
+ return page.evaluate(() => {
+ const testWindow = window as Window & {
+ __BUZZ_E2E_WELCOME_SURFACE_READY_AT__?: number | null;
+ };
+ const rawTransaction = window.localStorage.getItem(
+ "buzz-community-onboarding-transaction.v1",
+ );
+ const transaction = rawTransaction
+ ? (JSON.parse(rawTransaction) as { stage?: string })
+ : null;
+ const welcomeContent = document.querySelector(
+ '[data-testid="message-channel-intro"]',
+ );
+ const chatTitle = document.querySelector('[data-testid="chat-title"]');
+ return {
+ clockNow: Date.now(),
+ transactionStage: transaction?.stage ?? null,
+ welcomeSurfaceReadyAt:
+ testWindow.__BUZZ_E2E_WELCOME_SURFACE_READY_AT__ ?? null,
+ applyWorkspaceCalls: (window.__BUZZ_E2E_COMMANDS__ ?? []).filter(
+ (command) => command === "apply_workspace",
+ ).length,
+ communityResets: window.__BUZZ_E2E_COMMUNITY_RESETS__ ?? 0,
+ appReadyMounts: window.__BUZZ_E2E_APP_READY_MOUNTS__ ?? 0,
+ enteringCurtainMounted:
+ document.querySelector(
+ '[data-testid="onboarding-entering-curtain"]',
+ ) !== null,
+ appLoadingGateMounted:
+ document.querySelector('[data-testid="app-loading-gate"]') !== null,
+ communitySwitchGateMounted:
+ document.querySelector('[data-testid="community-switch-gate"]') !==
+ null,
+ welcomeChatTitle: chatTitle?.textContent?.trim() ?? null,
+ welcomeContentMounted: welcomeContent !== null,
+ welcomeContentVisible:
+ welcomeContent !== null && welcomeContent.getClientRects().length > 0,
+ welcomeContentText: welcomeContent?.textContent ?? "",
+ };
+ });
+}
+
+async function waitForCommunityLoadingGatesToSettle(page: Page) {
+ await expect(page.getByTestId("app-loading-gate")).toHaveCount(0);
+ await expect(page.getByTestId("community-switch-gate")).toHaveCount(0);
+}
+
async function reopenSameRelayCommunityLink(page: Page, relayUrl: string) {
await page.evaluate(
(input) => {
@@ -249,6 +328,42 @@ async function releaseOneProfileRead(page: Page) {
return releasedOrdinal;
}
+async function releaseAllProfileReads(page: Page) {
+ const released = await page.evaluate(() => {
+ const release = window.__BUZZ_E2E_RELEASE_PROFILE_READS__;
+ if (!release) {
+ throw new Error("Profile read release helper is not installed.");
+ }
+ return release();
+ });
+ expect(released).toBeGreaterThan(0);
+ return released;
+}
+
+async function waitForApplyWorkspacePending(page: Page, expected = 1) {
+ await expect
+ .poll(
+ () =>
+ page.evaluate(
+ () => window.__BUZZ_E2E_APPLY_WORKSPACES_PENDING__?.() ?? 0,
+ ),
+ { message: "waiting for a held apply_workspace call" },
+ )
+ .toBeGreaterThanOrEqual(expected);
+}
+
+async function releaseApplyWorkspaces(page: Page) {
+ const released = await page.evaluate(() => {
+ const release = window.__BUZZ_E2E_RELEASE_APPLY_WORKSPACES__;
+ if (!release) {
+ throw new Error("apply_workspace release helper is not installed.");
+ }
+ return release();
+ });
+ expect(released).toBeGreaterThan(0);
+ return released;
+}
+
async function waitForScopedProfileUpdatePending(page: Page) {
await expect
.poll(() =>
@@ -3239,6 +3354,243 @@ test("first-community direct join reaches profile", async ({ page }) => {
.toEqual({ communityCount: 1, transactionMatchesOnlyCommunity: true });
});
+test("completing a community join does not reinitialize after transaction clear", async ({
+ page,
+}) => {
+ await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
+ await page.addInitScript((pubkey) => {
+ window.localStorage.setItem(
+ `buzz-machine-onboarding-complete.v2:${pubkey}`,
+ "true",
+ );
+ }, BLANK_TYLER_IDENTITY.pubkey);
+ await page.addInitScript((eventName) => {
+ const testWindow = window as Window & {
+ __BUZZ_E2E_WELCOME_SURFACE_READY_AT__?: number | null;
+ };
+ testWindow.__BUZZ_E2E_WELCOME_SURFACE_READY_AT__ = null;
+ window.addEventListener(eventName, () => {
+ testWindow.__BUZZ_E2E_WELCOME_SURFACE_READY_AT__ ??= Date.now();
+ });
+ }, WELCOME_SURFACE_READY_EVENT);
+ await installMockBridge(
+ page,
+ {
+ deferProfileReads: true,
+ profileHasEvent: false,
+ },
+ {
+ relayWsUrl: "ws://localhost:3000",
+ skipOnboardingSeed: true,
+ skipCommunitySeed: true,
+ },
+ );
+ // Install before navigation so every app timer uses the same clock. Keep
+ // page time flowing during startup and the profile flow; pausing too early
+ // would also pause the timers that make those screens actionable.
+ await page.clock.install({
+ time: new Date("2026-10-01T00:00:00.000Z"),
+ });
+ await page.goto("/");
+
+ await page.getByRole("button", { name: /Join a community/ }).click();
+ await page
+ .getByTestId("invite-redeem-input")
+ .fill("wss://onboarding.communities.buzz.xyz");
+ await page.getByTestId("invite-redeem-submit").click();
+
+ await expect.poll(() => commandCount(page, "apply_workspace")).toBe(1);
+ await waitForProfileReadPending(page);
+ await releaseOneProfileRead(page);
+ await expect(
+ page.getByRole("heading", { name: "Build your profile" }),
+ ).toBeVisible();
+ await expect
+ .poll(() => commandCount(page, "get_profile"))
+ .toBeGreaterThanOrEqual(3);
+ await waitForProfileReadPending(page, 2);
+ await releaseAllProfileReads(page);
+
+ await page.getByTestId("community-profile-name-key").fill("New user");
+ await page.getByTestId("community-profile-next").click();
+ await expect(
+ page.getByRole("heading", { name: "Meet your starter team" }),
+ ).toBeVisible();
+ await expect
+ .poll(() => readCommunityOnboardingTransaction(page))
+ .toMatchObject({ stage: "team-intro" });
+ // Pause at a strictly future virtual instant before entering. The one-second
+ // guard absorbs the clock RPC latency without reaching the 500 ms retirement
+ // timer, which does not exist until the entering curtain is ready.
+ const pauseAt = await page.evaluate(() => Date.now() + 1_000);
+ await page.clock.pauseAt(pauseAt);
+ await page.getByTestId("community-team-intro-enter").click();
+ // Keep page time paused while the lazy Welcome route settles. Each bounded
+ // step gives timer-driven Suspense retries a chance to commit, but never
+ // crosses the real Welcome-ready fade or the entering safety deadline.
+ const isReadyForRetirement = (
+ state: Awaited>,
+ ) =>
+ state.transactionStage === "entering" &&
+ state.welcomeSurfaceReadyAt !== null &&
+ state.clockNow < state.welcomeSurfaceReadyAt + ENTERING_CURTAIN_FADE_MS &&
+ state.enteringCurtainMounted &&
+ !state.appLoadingGateMounted &&
+ !state.communitySwitchGateMounted &&
+ state.welcomeChatTitle?.includes("Welcome") === true &&
+ state.welcomeContentMounted &&
+ state.welcomeContentVisible &&
+ state.welcomeContentText.includes("private welcome channel");
+ let beforeRetirement:
+ | Awaited>
+ | undefined;
+ let enteringStartedAt: number | null = null;
+ let advancedMs = 0;
+ while (!beforeRetirement) {
+ const state = await readCommunityRetirementState(page);
+ if (enteringStartedAt === null && state.transactionStage === "entering") {
+ enteringStartedAt = state.clockNow;
+ }
+ if (isReadyForRetirement(state)) {
+ beforeRetirement = state;
+ break;
+ }
+
+ const deadlines = [
+ ENTERING_CLOCK_MAX_ADVANCE_MS - advancedMs,
+ state.welcomeSurfaceReadyAt === null
+ ? Number.POSITIVE_INFINITY
+ : state.welcomeSurfaceReadyAt +
+ ENTERING_CURTAIN_FADE_MS -
+ state.clockNow,
+ enteringStartedAt === null
+ ? Number.POSITIVE_INFINITY
+ : enteringStartedAt + ENTERING_CURTAIN_SAFETY_MS - state.clockNow,
+ ];
+ const stepMs = Math.min(
+ ENTERING_CLOCK_STEP_MS,
+ ...deadlines.map((deadline) => deadline - 1),
+ );
+ if (stepMs < 1) {
+ throw new Error(
+ `Welcome surface did not settle before its production deadline: ${JSON.stringify(state)}`,
+ );
+ }
+ await page.clock.fastForward(stepMs);
+ advancedMs += stepMs;
+ // Yield the host turn so React can commit work released by the timer tick;
+ // this does not advance the paused page clock.
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+
+ if (!beforeRetirement) {
+ throw new Error("The entering curtain never exposed the Welcome surface.");
+ }
+ expect(beforeRetirement).toMatchObject({
+ transactionStage: "entering",
+ applyWorkspaceCalls: 1,
+ communityResets: 0,
+ appReadyMounts: 1,
+ enteringCurtainMounted: true,
+ welcomeContentMounted: true,
+ welcomeContentVisible: true,
+ });
+
+ const welcomeSurfaceReadyAt = beforeRetirement.welcomeSurfaceReadyAt;
+ if (welcomeSurfaceReadyAt === null) {
+ throw new Error("The Welcome surface-ready event was not captured.");
+ }
+ const retirementDeadline = welcomeSurfaceReadyAt + ENTERING_CURTAIN_FADE_MS;
+ const remainingToRetirement = retirementDeadline - beforeRetirement.clockNow;
+ expect(remainingToRetirement).toBeGreaterThan(0);
+ // Advance exactly to the deadline recorded from the actual production event.
+ // This fires the real fade callback; it does not fabricate or directly clear
+ // the onboarding transaction.
+ await page.clock.fastForward(remainingToRetirement);
+ await expect.poll(() => readCommunityOnboardingTransaction(page)).toBeNull();
+ // Only after the production callback has retired the transaction may normal
+ // UI timers resume for the shared Welcome assertions.
+ await page.clock.resume();
+
+ await expect
+ .poll(async () => {
+ const state = await readCommunityRetirementState(page);
+ return {
+ applyWorkspaceCalls: state.applyWorkspaceCalls,
+ communityResets: state.communityResets,
+ appReadyMounts: state.appReadyMounts,
+ };
+ })
+ .toEqual({
+ applyWorkspaceCalls: beforeRetirement.applyWorkspaceCalls,
+ communityResets: beforeRetirement.communityResets,
+ appReadyMounts: beforeRetirement.appReadyMounts,
+ });
+ await waitForCommunityLoadingGatesToSettle(page);
+ await expectWelcomeView(page);
+ const afterRetirement = await readCommunityRetirementState(page);
+ expect(afterRetirement.applyWorkspaceCalls).toBe(
+ beforeRetirement.applyWorkspaceCalls,
+ );
+ expect(afterRetirement.communityResets).toBe(
+ beforeRetirement.communityResets,
+ );
+ expect(afterRetirement.appReadyMounts).toBe(beforeRetirement.appReadyMounts);
+ await expect(page.getByTestId("app-loading-gate")).toHaveCount(0);
+});
+
+test("first-community profile skip retires onboarding without reinitializing", async ({
+ page,
+}) => {
+ await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
+ await page.addInitScript((pubkey) => {
+ window.localStorage.setItem(
+ `buzz-machine-onboarding-complete.v2:${pubkey}`,
+ "true",
+ );
+ }, BLANK_TYLER_IDENTITY.pubkey);
+ await installMockBridge(
+ page,
+ { deferProfileReads: true, profileHasEvent: true },
+ {
+ relayWsUrl: "ws://localhost:3000",
+ skipOnboardingSeed: true,
+ skipCommunitySeed: true,
+ },
+ );
+ await page.goto("/");
+
+ await page.getByRole("button", { name: /Join a community/ }).click();
+ await page
+ .getByTestId("invite-redeem-input")
+ .fill("wss://onboarding.communities.buzz.xyz");
+ await page.getByTestId("invite-redeem-submit").click();
+
+ await expect.poll(() => commandCount(page, "apply_workspace")).toBe(1);
+ await waitForProfileReadPending(page);
+ await waitForCommunityLoadingGatesToSettle(page);
+ const beforeRetirement = await readCommunityInitMetrics(page);
+ await releaseOneProfileRead(page);
+
+ await expect.poll(() => readCommunityOnboardingTransaction(page)).toBeNull();
+ await expect
+ .poll(() => readCommunityInitMetrics(page))
+ .toMatchObject({
+ applyWorkspaceCalls: 1,
+ communityResets: beforeRetirement.communityResets,
+ appReadyMounts: beforeRetirement.appReadyMounts + 1,
+ });
+ await waitForCommunityLoadingGatesToSettle(page);
+ const afterRetirement = await readCommunityInitMetrics(page);
+ expect(afterRetirement.communityResets).toBe(
+ beforeRetirement.communityResets,
+ );
+ expect(afterRetirement.appReadyMounts).toBe(
+ beforeRetirement.appReadyMounts + 1,
+ );
+ await expect(page.getByTestId("app-loading-gate")).toHaveCount(0);
+});
+
test("community onboarding reuses an existing relay profile", async ({
page,
}) => {
@@ -3313,6 +3665,99 @@ test("community onboarding reuses an existing relay profile", async ({
).toHaveAttribute("data-onboarding-direction", "backward");
});
+test("returning-user profile skip retires onboarding without reinitializing", async ({
+ page,
+}) => {
+ const relayUrl = "wss://returning-profile-skip.example";
+ await seedConnectingCorporateProfileTransaction(page, {
+ relayUrl,
+ transactionId: "txn-returning-profile-skip",
+ });
+ await installMockBridge(
+ page,
+ {
+ deferProfileReads: true,
+ profileHasEvent: true,
+ },
+ {
+ relayWsUrl: relayUrl,
+ skipOnboardingSeed: true,
+ skipCommunitySeed: true,
+ },
+ );
+ await page.goto("/");
+
+ await expect.poll(() => commandCount(page, "apply_workspace")).toBe(1);
+ await waitForProfileReadPending(page);
+ await waitForCommunityLoadingGatesToSettle(page);
+ const beforeRetirement = await readCommunityInitMetrics(page);
+ await releaseOneProfileRead(page);
+
+ await expect.poll(() => readCommunityOnboardingTransaction(page)).toBeNull();
+ await expect
+ .poll(() => readCommunityInitMetrics(page))
+ .toMatchObject({
+ applyWorkspaceCalls: 1,
+ communityResets: beforeRetirement.communityResets,
+ appReadyMounts: beforeRetirement.appReadyMounts + 1,
+ });
+ await waitForCommunityLoadingGatesToSettle(page);
+ const afterRetirement = await readCommunityInitMetrics(page);
+ expect(afterRetirement.communityResets).toBe(
+ beforeRetirement.communityResets,
+ );
+ expect(afterRetirement.appReadyMounts).toBe(
+ beforeRetirement.appReadyMounts + 1,
+ );
+ await expect(page.getByTestId("app-loading-gate")).toHaveCount(0);
+});
+
+test("canceling onboarding while retaining the active community does not reinitialize", async ({
+ page,
+}) => {
+ const relayUrl = "wss://cancel-retained-community.example";
+ await seedConnectingCorporateProfileTransaction(page, {
+ addedCommunity: false,
+ relayUrl,
+ transactionId: "txn-cancel-retained-community",
+ });
+ await installMockBridge(
+ page,
+ { deferApplyCommunity: true },
+ {
+ relayWsUrl: relayUrl,
+ skipOnboardingSeed: true,
+ skipCommunitySeed: true,
+ },
+ );
+ await page.goto("/");
+
+ await expect(page.getByText("Connecting securely…")).toBeVisible();
+ await expect.poll(() => commandCount(page, "apply_workspace")).toBe(1);
+ await waitForApplyWorkspacePending(page);
+ const beforeRetirement = await readCommunityInitMetrics(page);
+ await page.getByRole("button", { name: "Cancel" }).click();
+
+ await expect.poll(() => readCommunityOnboardingTransaction(page)).toBeNull();
+ await releaseApplyWorkspaces(page);
+ await expect
+ .poll(() =>
+ readCommunityInitMetrics(page).then((m) => m.applyWorkspaceCalls),
+ )
+ .toBe(1);
+ await waitForCommunityLoadingGatesToSettle(page);
+ await expect
+ .poll(() => readCommunityInitMetrics(page).then((m) => m.appReadyMounts))
+ .toBe(beforeRetirement.appReadyMounts + 1);
+ const afterRetirement = await readCommunityInitMetrics(page);
+ expect(afterRetirement.communityResets).toBe(
+ beforeRetirement.communityResets,
+ );
+ expect(afterRetirement.appReadyMounts).toBe(
+ beforeRetirement.appReadyMounts + 1,
+ );
+});
+
test("first-community direct join cancel returns to request access", async ({
page,
}) => {
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index edee4a4e61..f4dbeafa6f 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -317,6 +317,8 @@ type MockBridgeOptions = {
}>;
/** Delay (ms) for `apply_workspace`; see e2eBridge mock config. */
applyCommunityDelayMs?: number;
+ /** Hold `apply_workspace` until the E2E release seam is called. */
+ deferApplyCommunity?: boolean;
/** Reject `clear_pending_navigation_deep_links` with this message. */
clearPendingNavigationDeepLinksError?: string;
openDmDelayMs?: number;