fix(desktop): retain community init after onboarding

Retire temporary onboarding state without resetting the applied community.
Bind lifecycle regression coverage to the real Welcome-ready event and
retirement callback with a paused, bounded browser clock.

Signed-off-by: bb-expert <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz>
This commit is contained in:
Codex
2026-10-01 16:29:11 -04:00
committed by bb-expert
parent b9d88de41c
commit 4ef08e23f7
5 changed files with 581 additions and 30 deletions

View File

@@ -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 ? <E2eAppReadyMountTelemetry /> : null;
if (onboarding.stage === "reset-failed") {
return <ResetFailedScreen />;
return (
<>
{e2eTelemetry}
<ResetFailedScreen />
</>
);
}
if (onboarding.stage === "keyring-locked") {
return <KeyringLockedScreen />;
return (
<>
{e2eTelemetry}
<KeyringLockedScreen />
</>
);
}
if (onboarding.stage === "relaunch-required") {
return <RelaunchRequiredScreen />;
return (
<>
{e2eTelemetry}
<RelaunchRequiredScreen />
</>
);
}
if (
@@ -353,35 +388,46 @@ function AppReady({
(continueOnboarding && onboarding.stage === "blocking")
) {
return (
<OnboardingFlow
actions={onboarding.flow.actions}
identityLost={onboarding.identityLost}
initialProfile={onboarding.flow.initialProfile}
initialProfileDecisionSettled={
onboarding.flow.initialProfileDecisionSettled
}
key={onboarding.currentPubkey ?? "anonymous"}
/>
<>
{e2eTelemetry}
<OnboardingFlow
actions={onboarding.flow.actions}
identityLost={onboarding.identityLost}
initialProfile={onboarding.flow.initialProfile}
initialProfileDecisionSettled={
onboarding.flow.initialProfileDecisionSettled
}
key={onboarding.currentPubkey ?? "anonymous"}
/>
</>
);
}
if (onboarding.stage === "blocking") {
return isCommunitySwitch ? <CommunitySwitchGate /> : <AppLoadingGate />;
return (
<>
{e2eTelemetry}
{isCommunitySwitch ? <CommunitySwitchGate /> : <AppLoadingGate />}
</>
);
}
return (
<EncryptedBackupProvider
onOpenSettings={() =>
void router.navigate({
to: "/settings",
search: { section: "profile" },
})
}
>
<KnownAgentPubkeysProvider>
<RouterProvider router={router} />
</KnownAgentPubkeysProvider>
</EncryptedBackupProvider>
<>
{e2eTelemetry}
<EncryptedBackupProvider
onOpenSettings={() =>
void router.navigate({
to: "/settings",
search: { section: "profile" },
})
}
>
<KnownAgentPubkeysProvider>
<RouterProvider router={router} />
</KnownAgentPubkeysProvider>
</EncryptedBackupProvider>
</>
);
}
@@ -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,

View File

@@ -71,6 +71,13 @@ async function resetCommunityState({
}: {
resetAvatarState: boolean;
}): Promise<void> {
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();

View File

@@ -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<void>((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) =>

View File

@@ -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<ReturnType<typeof readCommunityRetirementState>>,
) =>
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<ReturnType<typeof readCommunityRetirementState>>
| 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,
}) => {

View File

@@ -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;