Fence onboarding profile work by init attempt

Signed-off-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz>
Signed-off-by: bb-expert <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz>
This commit is contained in:
coder 0
2026-09-29 20:03:02 -06:00
committed by bb-expert
parent fca69a0ae1
commit 99c8219e1d
3 changed files with 642 additions and 22 deletions

View File

@@ -32,7 +32,6 @@ import {
markCommunityOnboardingComplete,
resolveProfileCheckAction,
isTransactionCurrentStage,
isTransactionStillConnecting,
} from "@/features/onboarding/communityOnboarding";
import { CommunityOnboardingFlow } from "@/features/onboarding/ui/CommunityOnboardingFlow";
import {
@@ -93,6 +92,31 @@ const INITIAL_RENDER_READY_EVENT = "initial-render-ready";
type BootSplashPhase = "holding" | "fading" | "done";
type CommunityOnboardingWorkOwner = {
transactionId: string;
initAttempt: number;
};
function sameCommunityOnboardingWorkOwner(
a: CommunityOnboardingWorkOwner | null,
b: CommunityOnboardingWorkOwner,
): boolean {
return (
a?.transactionId === b.transactionId && a.initAttempt === b.initAttempt
);
}
function isTransactionCurrentGenerationStage(
live: Parameters<typeof isTransactionCurrentStage>[0],
owner: CommunityOnboardingWorkOwner,
stage: Parameters<typeof isTransactionCurrentStage>[2],
): boolean {
return (
(live?.initAttempt ?? 0) === owner.initAttempt &&
isTransactionCurrentStage(live, owner.transactionId, stage)
);
}
function useInitialRenderReady() {
useLayoutEffect(() => {
if (!isTauri()) {
@@ -384,10 +408,12 @@ function CommunityApp({
} = useCommunities();
const communityOnboarding = useCommunityOnboarding();
const connectingTransactionRef = useRef<string | null>(null);
// Tracks the ID of the profile-check request that has been launched for the
// current connecting transaction. Prevents the effect from launching a
// second request if it re-runs while a fetch is in flight.
const profileCheckTransactionRef = useRef<string | null>(null);
// Tracks the transaction generation that launched the downstream profile
// branch for the current connecting transaction. Same-relay reopen preserves
// the transaction id while incrementing initAttempt, so the generation is part
// of both launch ownership and async completion ownership.
const profileCheckTransactionRef =
useRef<CommunityOnboardingWorkOwner | null>(null);
// Always reflects the live transaction object so async callbacks can perform
// an atomic check of both ID and stage before mutating state.
const transactionRef = useRef(communityOnboarding.transaction);
@@ -546,6 +572,11 @@ function CommunityApp({
useEffect(() => {
if (transaction?.stage !== "connecting") {
connectingTransactionRef.current = null;
}
if (
transaction?.stage !== "connecting" &&
transaction?.stage !== "corporate-profile"
) {
profileCheckTransactionRef.current = null;
}
}, [transaction?.stage]);
@@ -582,11 +613,23 @@ function CommunityApp({
transaction,
]);
useEffect(() => {
if (transaction?.stage !== "connecting" || !targetIsReady) return;
const canLaunchProfileDownstream =
transaction?.stage === "connecting" ||
(transaction?.stage === "corporate-profile" &&
!transaction.error &&
Boolean(enterpriseProfile));
if (!canLaunchProfileDownstream || !targetIsReady) return;
const transactionId = transaction.id;
const relayUrl = transaction.relayUrl;
if (profileCheckTransactionRef.current === transactionId) return;
profileCheckTransactionRef.current = transactionId;
const workOwner = { transactionId, initAttempt: communityInitAttempt };
if (
sameCommunityOnboardingWorkOwner(
profileCheckTransactionRef.current,
workOwner,
)
)
return;
profileCheckTransactionRef.current = workOwner;
if (enterpriseProfile) {
if (!identityPubkey) {
@@ -612,10 +655,11 @@ function CommunityApp({
})
.then(() => {
if (
!isTransactionCurrentStage(transactionRef.current, transactionId, [
"connecting",
"corporate-profile",
])
!isTransactionCurrentGenerationStage(
transactionRef.current,
workOwner,
["connecting", "corporate-profile"],
)
)
return;
communityOnboarding.update(
@@ -625,10 +669,11 @@ function CommunityApp({
})
.catch((error) => {
if (
!isTransactionCurrentStage(transactionRef.current, transactionId, [
"connecting",
"corporate-profile",
])
!isTransactionCurrentGenerationStage(
transactionRef.current,
workOwner,
["connecting", "corporate-profile"],
)
)
return;
profileCheckTransactionRef.current = null;
@@ -649,11 +694,17 @@ function CommunityApp({
// resolveProfileCheckAction resolves exactly once (Promise.race + timer
// cleared on settle), so no settled flag is needed here.
void resolveProfileCheckAction(getProfile, 10_000).then((result) => {
// Atomic staleness guard via isTransactionStillConnecting: the
// transaction must still be the same one that launched this request
// AND still be in connecting. Covers cancel+replacement (B's ID !== A's)
// and cancel-without-replacement (transactionRef.current is null).
if (!isTransactionStillConnecting(transactionRef.current, transactionId))
// Atomic staleness guard: the transaction must still be the same
// generation that launched this request and still be in connecting.
// Covers same-relay retry (same ID, newer initAttempt), cancel +
// replacement (B's ID !== A's), and cancel without replacement.
if (
!isTransactionCurrentGenerationStage(
transactionRef.current,
workOwner,
"connecting",
)
)
return;
if (result.action === "skip") {
@@ -672,8 +723,10 @@ function CommunityApp({
transaction?.stage,
transaction?.id,
transaction?.relayUrl,
transaction?.error,
enterpriseProfile,
identityPubkey,
communityInitAttempt,
]);
// During "entering" the transaction stays alive as a curtain: the app mounts
// underneath (already pointed at the Welcome channel route) while the

View File

@@ -449,6 +449,7 @@ type E2eConfig = {
/** Hold `get_profile` responses until the E2E release seam is invoked. */
deferProfileReads?: boolean;
profileReadError?: string;
profileReadErrors?: (string | null)[];
/** Override whether get_profile reports a real kind:0 event. */
profileHasEvent?: boolean;
profileUpdateError?: string;
@@ -1632,14 +1633,30 @@ declare global {
__BUZZ_E2E_HOLD_USERS_BATCH__?: (hold: boolean) => number;
/** Number of `get_users_batch` calls currently held. */
__BUZZ_E2E_USERS_BATCH_PENDING__?: () => number;
/** Release the oldest `get_profile` response held by `deferProfileReads`. */
__BUZZ_E2E_RELEASE_ONE_PROFILE_READ__?: () => number;
/** Oldest-to-newest ordinals for `get_profile` responses currently held. */
__BUZZ_E2E_PROFILE_READ_PENDING_ORDINALS__?: () => number[];
/** Release every `get_profile` response held by `deferProfileReads`. */
__BUZZ_E2E_RELEASE_PROFILE_READS__?: () => number;
/** Number of `get_profile` responses currently held. */
__BUZZ_E2E_PROFILE_READS_PENDING__?: () => number;
/** Release the oldest `update_profile_at_relay` response held by `deferProfileUpdates`. */
__BUZZ_E2E_RELEASE_ONE_PROFILE_UPDATE__?: () => number;
/** Oldest-to-newest ordinals for `update_profile_at_relay` responses currently held. */
__BUZZ_E2E_PROFILE_UPDATE_PENDING_ORDINALS__?: () => number[];
/** Release every `update_profile_at_relay` response held by `deferProfileUpdates`. */
__BUZZ_E2E_RELEASE_PROFILE_UPDATES__?: () => number;
/** Number of `update_profile_at_relay` responses currently held. */
__BUZZ_E2E_PROFILE_UPDATES_PENDING__?: () => number;
/** Append a mocked Rust-side pending community deep link. */
__BUZZ_E2E_ENQUEUE_COMMUNITY_DEEP_LINK__?: (pending: {
id: string;
kind: "connect" | "join" | "add-community";
relayUrl: string;
code?: string | null;
name?: string | null;
}) => number;
/** Uploads that passed mock-native registration and began relay work. */
__BUZZ_E2E_LINK_PREVIEW_UPLOAD_STARTS__?: number;
/** Hold renderer-owned media fetches until their cancellation command. */
@@ -1766,18 +1783,22 @@ let deferredLinkPreviewMetadataQueue: Array<() => void> = [];
let deferredLinkPreviewUploadQueue: Array<() => void> = [];
let deferredThreadRepliesQueue: Array<() => void> = [];
type DeferredProfileRead = {
ordinal: number;
reject: (reason: unknown) => void;
resolve: (value: unknown) => void;
run: () => Promise<unknown>;
};
type DeferredProfileUpdate = {
ordinal: number;
reject: (reason: unknown) => void;
resolve: (value: unknown) => void;
run: () => Promise<unknown>;
};
let deferredProfileReadQueue: DeferredProfileRead[] = [];
let nextProfileReadOrdinal = 1;
let profileReadsReleased = false;
let deferredProfileUpdateQueue: DeferredProfileUpdate[] = [];
let nextProfileUpdateOrdinal = 1;
let profileUpdatesReleased = false;
// ── get_users_batch hold seam ───────────────────────────────────────────────
// Toggled at runtime by `__BUZZ_E2E_HOLD_USERS_BATCH__(hold)` rather than fixed
@@ -6830,6 +6851,11 @@ async function handleGetChannels(
async function runGetProfile(config: E2eConfig | undefined) {
const identity = getIdentity(config);
const profileReadErrors = config?.mock?.profileReadErrors;
const nextProfileReadError = profileReadErrors?.shift();
if (nextProfileReadError) {
throw new Error(nextProfileReadError);
}
const profileReadDelayMs = config?.mock?.profileReadDelayMs ?? 0;
if (profileReadDelayMs > 0) {
await new Promise<void>((resolve) => {
@@ -6887,10 +6913,12 @@ async function handleGetProfile(config: E2eConfig | undefined) {
return new Promise<unknown>((resolve, reject) => {
deferredProfileReadQueue.push({
ordinal: nextProfileReadOrdinal,
resolve,
reject,
run: () => runGetProfile(config),
});
nextProfileReadOrdinal += 1;
});
}
@@ -7000,10 +7028,12 @@ async function handleUpdateProfileAtRelay(
return new Promise<unknown>((resolve, reject) => {
deferredProfileUpdateQueue.push({
ordinal: nextProfileUpdateOrdinal,
resolve,
reject,
run: () => runUpdateProfile(args, config),
});
nextProfileUpdateOrdinal += 1;
});
}
@@ -11494,8 +11524,10 @@ export function maybeInstallE2eTauriMocks() {
deferredLinkPreviewUploadQueue = [];
deferredThreadRepliesQueue = [];
deferredProfileReadQueue = [];
nextProfileReadOrdinal = 1;
profileReadsReleased = false;
deferredProfileUpdateQueue = [];
nextProfileUpdateOrdinal = 1;
profileUpdatesReleased = false;
holdUsersBatch = false;
heldUsersBatchReleases = [];
@@ -11525,6 +11557,25 @@ export function maybeInstallE2eTauriMocks() {
};
window.__BUZZ_E2E_THREAD_REPLIES_PENDING__ = () =>
deferredThreadRepliesQueue.length;
const releaseProfileRead = () => {
const deferred = deferredProfileReadQueue.shift();
if (!deferred) return 0;
window.__BUZZ_E2E_COMMAND_LOG__?.push({
command: "get_profile:released",
payload: { ordinal: deferred.ordinal },
});
void deferred
.run()
.then(deferred.resolve, deferred.reject)
.finally(() => {
window.__BUZZ_E2E_COMMAND_LOG__?.push({
command: "get_profile:settled",
payload: { ordinal: deferred.ordinal },
});
});
return deferred.ordinal;
};
window.__BUZZ_E2E_RELEASE_ONE_PROFILE_READ__ = releaseProfileRead;
window.__BUZZ_E2E_RELEASE_PROFILE_READS__ = () => {
profileReadsReleased = true;
const queued = deferredProfileReadQueue.splice(0);
@@ -11535,6 +11586,27 @@ export function maybeInstallE2eTauriMocks() {
};
window.__BUZZ_E2E_PROFILE_READS_PENDING__ = () =>
deferredProfileReadQueue.length;
window.__BUZZ_E2E_PROFILE_READ_PENDING_ORDINALS__ = () =>
deferredProfileReadQueue.map(({ ordinal }) => ordinal);
const releaseProfileUpdate = () => {
const deferred = deferredProfileUpdateQueue.shift();
if (!deferred) return 0;
window.__BUZZ_E2E_COMMAND_LOG__?.push({
command: "update_profile_at_relay:released",
payload: { ordinal: deferred.ordinal },
});
void deferred
.run()
.then(deferred.resolve, deferred.reject)
.finally(() => {
window.__BUZZ_E2E_COMMAND_LOG__?.push({
command: "update_profile_at_relay:settled-ordinal",
payload: { ordinal: deferred.ordinal },
});
});
return deferred.ordinal;
};
window.__BUZZ_E2E_RELEASE_ONE_PROFILE_UPDATE__ = releaseProfileUpdate;
window.__BUZZ_E2E_RELEASE_PROFILE_UPDATES__ = () => {
profileUpdatesReleased = true;
const queued = deferredProfileUpdateQueue.splice(0);
@@ -11545,6 +11617,16 @@ export function maybeInstallE2eTauriMocks() {
};
window.__BUZZ_E2E_PROFILE_UPDATES_PENDING__ = () =>
deferredProfileUpdateQueue.length;
window.__BUZZ_E2E_PROFILE_UPDATE_PENDING_ORDINALS__ = () =>
deferredProfileUpdateQueue.map(({ ordinal }) => ordinal);
window.__BUZZ_E2E_ENQUEUE_COMMUNITY_DEEP_LINK__ = (pending) => {
mockPendingCommunityDeepLinks.push({
...pending,
code: pending.code ?? null,
name: pending.name ?? null,
});
return mockPendingCommunityDeepLinks.length;
};
window.__BUZZ_E2E_HOLD_USERS_BATCH__ = (hold: boolean) => {
holdUsersBatch = hold;
// Releasing on the way out of the hold, not on the way in, is what lets a

View File

@@ -1,5 +1,11 @@
import { hexToBytes } from "@noble/hashes/utils.js";
import { expect, test, type Locator, type Page } from "@playwright/test";
import {
expect,
test,
type Locator,
type Page,
type TestInfo,
} from "@playwright/test";
import { nsecEncode, npubEncode } from "nostr-tools/nip19";
import {
@@ -197,12 +203,52 @@ async function readCommunityOnboardingTransaction(page: Page) {
id: string;
relayUrl: string;
stage: string;
initAttempt?: number;
error?: string;
})
: null;
}, COMMUNITY_ONBOARDING_TRANSACTION_STORAGE_KEY);
}
async function reopenSameRelayCommunityLink(page: Page, relayUrl: string) {
await page.evaluate(
(input) => {
const enqueue = window.__BUZZ_E2E_ENQUEUE_COMMUNITY_DEEP_LINK__;
const emit = window.__BUZZ_E2E_EMIT_TAURI_EVENT__;
if (!enqueue || !emit) {
throw new Error("Community deep-link E2E helpers are not installed.");
}
enqueue({
id: `reopen-${Date.now()}`,
kind: "connect",
relayUrl: input.relayUrl,
});
void emit("deep-link-connect", null);
},
{ relayUrl },
);
}
async function waitForProfileReadPending(page: Page, expected = 1) {
await expect
.poll(() =>
page.evaluate(() => window.__BUZZ_E2E_PROFILE_READS_PENDING__?.() ?? 0),
)
.toBeGreaterThanOrEqual(expected);
}
async function releaseOneProfileRead(page: Page) {
const releasedOrdinal = await page.evaluate(() => {
const release = window.__BUZZ_E2E_RELEASE_ONE_PROFILE_READ__;
if (!release) {
throw new Error("Profile read release helper is not installed.");
}
return release();
});
expect(releasedOrdinal).toBeGreaterThan(0);
return releasedOrdinal;
}
async function waitForScopedProfileUpdatePending(page: Page) {
await expect
.poll(() =>
@@ -215,6 +261,109 @@ async function waitForScopedProfileUpdatePending(page: Page) {
.toBeGreaterThan(0);
}
async function writeProfileUpdateGenerationArtifact(
page: Page,
testInfo: TestInfo,
label: string,
snapshots: unknown[],
) {
const finalState = await page.evaluate(() => ({
commandLog: window.__BUZZ_E2E_COMMAND_LOG__ ?? [],
pendingUpdateOrdinals:
window.__BUZZ_E2E_PROFILE_UPDATE_PENDING_ORDINALS__?.() ?? [],
pendingUpdateCount: window.__BUZZ_E2E_PROFILE_UPDATES_PENDING__?.() ?? 0,
transactionRaw: window.localStorage.getItem(
"buzz-community-onboarding-transaction.v1",
),
}));
await testInfo.attach(label, {
body: JSON.stringify({ snapshots, finalState }, null, 2),
contentType: "application/json",
});
}
async function captureProfileUpdateGenerationSnapshot(
page: Page,
label: string,
) {
return page.evaluate((snapshotLabel) => {
const transactionRaw = window.localStorage.getItem(
"buzz-community-onboarding-transaction.v1",
);
const transaction = transactionRaw
? (JSON.parse(transactionRaw) as {
id?: string;
initAttempt?: number;
stage?: string;
})
: null;
return {
label: snapshotLabel,
pendingUpdateOrdinals:
window.__BUZZ_E2E_PROFILE_UPDATE_PENDING_ORDINALS__?.() ?? [],
pendingUpdateCount: window.__BUZZ_E2E_PROFILE_UPDATES_PENDING__?.() ?? 0,
settledUpdateOrdinals: (window.__BUZZ_E2E_COMMAND_LOG__ ?? [])
.filter(
(entry) =>
entry.command === "update_profile_at_relay:settled-ordinal",
)
.map(
(entry) => (entry.payload as { ordinal?: number } | null)?.ordinal,
),
transaction: transaction
? {
id: transaction.id,
initAttempt: transaction.initAttempt,
stage: transaction.stage,
}
: null,
};
}, label);
}
async function attachProfileUpdateGenerationSnapshots(
page: Page,
testInfo: TestInfo,
label: string,
snapshots: unknown[],
) {
try {
await writeProfileUpdateGenerationArtifact(
page,
testInfo,
label,
snapshots,
);
} catch {
// Diagnostic attachment must not mask the primary assertion failure.
}
}
async function releaseOneProfileUpdateAndWaitForSettlement(page: Page) {
const releasedOrdinal = await page.evaluate(() => {
const release = window.__BUZZ_E2E_RELEASE_ONE_PROFILE_UPDATE__;
if (!release) {
throw new Error("Profile update release helper is not installed.");
}
return release();
});
expect(releasedOrdinal).toBeGreaterThan(0);
await expect
.poll(() =>
page.evaluate((ordinal) => {
return (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).some((entry) => {
if (entry.command !== "update_profile_at_relay:settled-ordinal") {
return false;
}
const payload = entry.payload as { ordinal?: number } | null;
return payload?.ordinal === ordinal;
});
}, releasedOrdinal),
)
.toBe(true);
return releasedOrdinal;
}
async function releaseScopedProfileUpdateAndWaitForSettlement(page: Page) {
const settledBefore = await page.evaluate(
() =>
@@ -2200,6 +2349,342 @@ test("joining enterprise login retry reruns init for the active community", asyn
await expect(page.getByText("Browser login was rejected")).toHaveCount(0);
});
test("same-relay reopen fences stale profile read success by init attempt", async ({
page,
}) => {
const relayUrl = "wss://profile-read-generation-success.example";
const transactionId = "txn-profile-read-generation-success";
await seedConnectingCorporateProfileTransaction(page, {
relayUrl,
transactionId,
});
await installMockBridge(
page,
{ deferProfileReads: true, profileHasEvent: false },
{
relayWsUrl: relayUrl,
skipOnboardingSeed: true,
skipCommunitySeed: true,
},
);
await page.goto("/");
await waitForProfileReadPending(page, 1);
const generationA = await readCommunityOnboardingTransaction(page);
expect(generationA).toMatchObject({
id: transactionId,
stage: "connecting",
});
expect(generationA).not.toHaveProperty("initAttempt");
await reopenSameRelayCommunityLink(page, relayUrl);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "connecting",
initAttempt: 1,
});
await waitForProfileReadPending(page, 2);
await releaseOneProfileRead(page);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "connecting",
initAttempt: 1,
});
await expect(
page.getByRole("heading", { name: "Build your profile" }),
).toHaveCount(0);
await expect
.poll(() => commandCount(page, "get_profile"))
.toBeGreaterThanOrEqual(2);
await waitForProfileReadPending(page, 1);
await releaseOneProfileRead(page);
await expect(
page.getByRole("heading", { name: "Build your profile" }),
).toBeVisible();
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({ id: transactionId, stage: "profile", initAttempt: 1 });
});
test("same-relay reopen fences stale profile read rejection by init attempt", async ({
page,
}) => {
const relayUrl = "wss://profile-read-generation-rejection.example";
const transactionId = "txn-profile-read-generation-rejection";
await seedConnectingCorporateProfileTransaction(page, {
relayUrl,
transactionId,
});
await installMockBridge(
page,
{
deferProfileReads: true,
profileHasEvent: false,
profileReadErrors: ["stale profile read failed", null],
},
{
relayWsUrl: relayUrl,
skipOnboardingSeed: true,
skipCommunitySeed: true,
},
);
await page.goto("/");
await waitForProfileReadPending(page, 1);
await reopenSameRelayCommunityLink(page, relayUrl);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "connecting",
initAttempt: 1,
});
await waitForProfileReadPending(page, 2);
await releaseOneProfileRead(page);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "connecting",
initAttempt: 1,
});
await expect(page.getByText("stale profile read failed")).toHaveCount(0);
await expect(
page.getByRole("heading", { name: "Build your profile" }),
).toHaveCount(0);
await releaseOneProfileRead(page);
await expect(
page.getByRole("heading", { name: "Build your profile" }),
).toBeVisible();
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({ id: transactionId, stage: "profile", initAttempt: 1 });
});
test("same-relay reopen fences stale corporate profile save success by init attempt", async ({
page,
}, testInfo) => {
const snapshots: unknown[] = [];
try {
const relayUrl = "wss://profile-save-generation-success.example";
const transactionId = "txn-profile-save-generation-success";
await seedConnectingCorporateProfileTransaction(page, {
relayUrl,
transactionId,
});
await installMockBridge(
page,
{
enterpriseLoginGate: { status: "required" },
enterpriseAuth: {
email: "brad@example.com",
expiresAt: "2099-01-01T00:00:00Z",
profileProjection: {
username: "seiler",
displayName: "Brad Seiler",
},
},
deferProfileUpdates: true,
},
{
relayWsUrl: relayUrl,
skipOnboardingSeed: true,
skipCommunitySeed: true,
},
);
await page.goto("/");
await expect(page.getByText("Saving your company profile…")).toBeVisible();
await waitForScopedProfileUpdatePending(page);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "A queued"),
);
await reopenSameRelayCommunityLink(page, relayUrl);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "corporate-profile",
initAttempt: 1,
});
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "reopen applied"),
);
await expect
.poll(() =>
page.evaluate(
() => window.__BUZZ_E2E_PROFILE_UPDATES_PENDING__?.() ?? 0,
),
)
.toBeGreaterThanOrEqual(2);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "B queued"),
);
const releasedA = await releaseOneProfileUpdateAndWaitForSettlement(page);
expect(releasedA).toBe(1);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "A released/settled"),
);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "corporate-profile",
initAttempt: 1,
});
await expect(
page.getByRole("heading", { name: "Meet your starter team" }),
).toHaveCount(0);
await expect
.poll(() =>
page.evaluate(
() => window.__BUZZ_E2E_PROFILE_UPDATE_PENDING_ORDINALS__?.() ?? [],
),
)
.toContain(2);
const releasedB = await releaseOneProfileUpdateAndWaitForSettlement(page);
expect(releasedB).toBe(2);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "B released/settled"),
);
await expect(
page.getByRole("heading", { name: "Meet your starter team" }),
).toBeVisible();
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "team-intro",
initAttempt: 1,
});
} finally {
await attachProfileUpdateGenerationSnapshots(
page,
testInfo,
"profile-save-generation-success-diagnostics",
snapshots,
);
}
});
test("same-relay reopen fences stale corporate profile save rejection by init attempt", async ({
page,
}, testInfo) => {
const snapshots: unknown[] = [];
try {
const staleError = "Stale generation corporate profile failure.";
const relayUrl = "wss://profile-save-generation-rejection.example";
const transactionId = "txn-profile-save-generation-rejection";
await seedConnectingCorporateProfileTransaction(page, {
relayUrl,
transactionId,
});
await installMockBridge(
page,
{
enterpriseLoginGate: { status: "required" },
enterpriseAuth: {
email: "brad@example.com",
expiresAt: "2099-01-01T00:00:00Z",
profileProjection: {
username: "seiler",
displayName: "Brad Seiler",
},
},
deferProfileUpdates: true,
profileUpdateErrors: [staleError, null],
},
{
relayWsUrl: relayUrl,
skipOnboardingSeed: true,
skipCommunitySeed: true,
},
);
await page.goto("/");
await expect(page.getByText("Saving your company profile…")).toBeVisible();
await waitForScopedProfileUpdatePending(page);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "A queued"),
);
await reopenSameRelayCommunityLink(page, relayUrl);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "corporate-profile",
initAttempt: 1,
});
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "reopen applied"),
);
await expect
.poll(() =>
page.evaluate(
() => window.__BUZZ_E2E_PROFILE_UPDATES_PENDING__?.() ?? 0,
),
)
.toBeGreaterThanOrEqual(2);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "B queued"),
);
const releasedA = await releaseOneProfileUpdateAndWaitForSettlement(page);
expect(releasedA).toBe(1);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "A released/settled"),
);
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "corporate-profile",
initAttempt: 1,
});
await expect(page.getByText(staleError)).toHaveCount(0);
await expect
.poll(() =>
page.evaluate(
() => window.__BUZZ_E2E_PROFILE_UPDATE_PENDING_ORDINALS__?.() ?? [],
),
)
.toContain(2);
const releasedB = await releaseOneProfileUpdateAndWaitForSettlement(page);
expect(releasedB).toBe(2);
snapshots.push(
await captureProfileUpdateGenerationSnapshot(page, "B released/settled"),
);
await expect(
page.getByRole("heading", { name: "Meet your starter team" }),
).toBeVisible();
await expect
.poll(() => readCommunityOnboardingTransaction(page))
.toMatchObject({
id: transactionId,
stage: "team-intro",
initAttempt: 1,
});
} finally {
await attachProfileUpdateGenerationSnapshots(
page,
testInfo,
"profile-save-generation-rejection-diagnostics",
snapshots,
);
}
});
test("returning enterprise user can cancel pending browser login and retry into the app", async ({
page,
}) => {