From 8657255596e27f5f782d252107d5d34dc7dfa8a7 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Tue, 21 Jul 2026 10:39:07 -0700 Subject: [PATCH 1/5] fix(desktop): keep machine onboarding for unrecognized identities after reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a dev-build sign-out+reset the Rust webview wipe targets the .app-bundle WebKit directory, which does not exist for `tauri dev` builds. The frontend's localStorage survived, including the active community entry. On relaunch the #1936 migration heuristic (migrateMachineOnboardingCompletion) counted that stale community as proof of prior completion, auto-marking the freshly generated key as onboarded and skipping machine onboarding entirely — landing the user in a half-onboarded relay-profile flow with no 'Use an existing key' option. Fix A (the regression): the hasConfiguredCommunity arm now checks Community.pubkey against the current pubkey. A community whose recorded pubkey doesn't match the live key is not a valid voucher. Absent pubkey (legacy entries predating the field) still vouches, so the veteran-upgrade path is preserved. Fix B (defence-in-depth): the sign-out confirm handler clears localStorage and sessionStorage on the resolve path only, covering the dev-build gap where the Rust wipe misses the process-keyed WebKit directory. The clear is a no-op in production (Rust already handles it) and intentionally does not run on the reject path (a failed sign-out must not wipe a still-running app). Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/scripts/check-file-sizes.mjs | 5 +- desktop/src/app/App.tsx | 4 +- .../onboarding/machineOnboarding.test.mjs | 169 ++++++++++++++++++ .../features/onboarding/machineOnboarding.ts | 35 +++- .../settings/ui/ProfileSettingsCard.tsx | 28 ++- 5 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 desktop/src/features/onboarding/machineOnboarding.test.mjs diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 845f991623..3aeb04996c 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -375,7 +375,10 @@ const overrides = new Map([ // queued to split when ProfileSettingsCard is broken into sub-components. // +20 lines: scroll-position save/restore across avatar editor open/close // to prevent layout shift from the Sign Out section causing a viewport jump. - ["src/features/settings/ui/ProfileSettingsCard.tsx", 1033], + // +11 lines: signout-dev-webview-state — clear localStorage/sessionStorage + // on successful signOut() resolve so dev-build webview state doesn't survive + // a reset and vouch for the fresh key. Comment explains the race/redundancy. + ["src/features/settings/ui/ProfileSettingsCard.tsx", 1044], // keyring-dev-isolation: keyring_service() fn (7 lines) replaces the const // to return "buzz-desktop-dev" in debug builds. Load-bearing isolation fix. // +10 (1042 -> 1052): media_fetch_client with redirect::Policy::none() so a diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index c3ab69c786..d304f638e2 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -494,7 +494,9 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) { const { activeCommunity } = useCommunities(); const communityOnboarding = useCommunityOnboarding(); const machine = useMachineOnboardingState({ - hasConfiguredCommunity: activeCommunity !== null, + activeCommunityPubkey: activeCommunity + ? (activeCommunity.pubkey ?? null) + : undefined, isSharedIdentity: sharedIdentity, }); const [machineInitialPage, setMachineInitialPage] = diff --git a/desktop/src/features/onboarding/machineOnboarding.test.mjs b/desktop/src/features/onboarding/machineOnboarding.test.mjs new file mode 100644 index 0000000000..fae946bed3 --- /dev/null +++ b/desktop/src/features/onboarding/machineOnboarding.test.mjs @@ -0,0 +1,169 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + migrateMachineOnboardingCompletion, + readMachineOnboardingCompletion, +} from "./machineOnboarding.ts"; + +// machineOnboarding.ts reads/writes window.localStorage directly, so we inject +// a minimal in-memory storage into globalThis.window before each test and +// restore it afterward. + +function createMemoryStorage(initial = {}) { + const values = new Map(Object.entries(initial)); + return { + getItem: (key) => values.get(key) ?? null, + setItem: (key, value) => values.set(key, String(value)), + removeItem: (key) => values.delete(key), + clear: () => values.clear(), + key: (index) => Array.from(values.keys())[index] ?? null, + get length() { + return values.size; + }, + }; +} + +function withFakeWindow(initial, fn) { + const prev = globalThis.window; + const storage = createMemoryStorage(initial); + globalThis.window = { + localStorage: storage, + // location.href without a machineOnboarding=1 param → forceMachineOnboarding() returns false + location: { href: "http://localhost/" }, + }; + try { + return fn(storage); + } finally { + globalThis.window = prev; + } +} + +const PUBKEY_A = + "aaaaaa1111112222223333334444445555556666667777778888889999990000aa"; +const PUBKEY_B = + "bbbbbb1111112222223333334444445555556666667777778888889999990000bb"; +const LEGACY_KEY = `buzz-onboarding-complete.v1:${PUBKEY_A}`; +const V2_KEY = `buzz-machine-onboarding-complete.v2:${PUBKEY_A}`; + +// ── Fix A regression case ──────────────────────────────────────────────────── + +test("migrate_mismatched_community_pubkey_does_not_vouch_for_current_key", () => { + // Community pubkey is PUBKEY_B (stale from a previous identity); current + // pubkey is PUBKEY_A (freshly generated after a dev reset). The mismatch + // must NOT grant completion. + withFakeWindow({}, (storage) => { + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + PUBKEY_B, // activeCommunityPubkey — a different identity's community + false, + ); + assert.equal(result, false, "mismatched pubkey must not vouch"); + assert.equal( + storage.getItem(V2_KEY), + null, + "completion must not be written to storage", + ); + assert.equal( + readMachineOnboardingCompletion(PUBKEY_A), + false, + "readMachineOnboardingCompletion must return false", + ); + }); +}); + +// ── Matching pubkey vouches ────────────────────────────────────────────────── + +test("migrate_matching_community_pubkey_vouches_for_current_key", () => { + // Community pubkey matches current pubkey → legitimate veteran, grant. + withFakeWindow({}, (storage) => { + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + PUBKEY_A, // same pubkey — community belongs to this identity + false, + ); + assert.equal(result, true, "matching pubkey must vouch"); + assert.equal(storage.getItem(V2_KEY), "true"); + assert.equal(readMachineOnboardingCompletion(PUBKEY_A), true); + }); +}); + +// ── Absent community pubkey (legacy entry) ─────────────────────────────────── + +test("migrate_absent_community_pubkey_vouches_for_current_key", () => { + // Community exists but has no pubkey field (pre-field legacy entry). + // `null` represents absent → treated as a match to preserve the + // veteran-upgrade path. + withFakeWindow({}, (storage) => { + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + null, // absent pubkey on a community that does exist + false, + ); + assert.equal( + result, + true, + "absent pubkey must vouch (legacy compatibility)", + ); + assert.equal(storage.getItem(V2_KEY), "true"); + }); +}); + +// ── No community configured ────────────────────────────────────────────────── + +test("migrate_no_community_does_not_vouch_for_current_key", () => { + // `undefined` = no active community at all. + withFakeWindow({}, (storage) => { + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + undefined, // no community + false, + ); + assert.equal(result, false, "no community must not vouch"); + assert.equal(storage.getItem(V2_KEY), null); + }); +}); + +// ── Legacy onboarding completion still grants (pubkey-scoped) ─────────────── + +test("migrate_legacy_completion_key_still_grants_migration", () => { + withFakeWindow({ [LEGACY_KEY]: "true" }, (storage) => { + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + undefined, // no community — but legacy key is present + false, + ); + assert.equal(result, true, "legacy completion key must grant migration"); + assert.equal(storage.getItem(V2_KEY), "true"); + }); +}); + +// ── Shared identity grants regardless of community ─────────────────────────── + +test("migrate_shared_identity_grants_regardless_of_community", () => { + withFakeWindow({}, () => { + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + undefined, // no community + true, // isSharedIdentity + ); + assert.equal(result, true, "shared identity must always grant"); + }); +}); + +// ── Already-completed v2 key returns true without re-writing ───────────────── + +test("migrate_already_completed_pubkey_returns_true_immediately", () => { + withFakeWindow({ [V2_KEY]: "true" }, (storage) => { + // Pass mismatched community to prove early-exit, not community vouching. + const result = migrateMachineOnboardingCompletion( + PUBKEY_A, + PUBKEY_B, + false, + ); + assert.equal(result, true, "already-completed pubkey must return true"); + // Value was already there; the function should not have touched it + // (but a redundant write is also acceptable — just verify it's still true). + assert.equal(storage.getItem(V2_KEY), "true"); + }); +}); diff --git a/desktop/src/features/onboarding/machineOnboarding.ts b/desktop/src/features/onboarding/machineOnboarding.ts index a85e5abec1..734ba93f1f 100644 --- a/desktop/src/features/onboarding/machineOnboarding.ts +++ b/desktop/src/features/onboarding/machineOnboarding.ts @@ -36,15 +36,27 @@ function clearMachineOnboardingCompletion(pubkey: string | null) { } function forceMachineOnboarding() { - if (!import.meta.env.DEV || typeof window === "undefined") return false; + if (!import.meta.env?.DEV || typeof window === "undefined") return false; return ( new URL(window.location.href).searchParams.get("machineOnboarding") === "1" ); } -function migrateMachineOnboardingCompletion( +/** @internal Exported for unit testing only. */ +export function migrateMachineOnboardingCompletion( pubkey: string, - hasConfiguredCommunity: boolean, + /** + * The `pubkey` field of the active community from localStorage, or + * `undefined` if no community is configured. When a community is present + * but its `pubkey` field is absent (legacy entries predating the field), + * pass `null` — absent is treated as a match so the veteran-upgrade path + * is preserved. Pass `undefined` when there is no active community at all. + * + * Using the community's own pubkey rather than a bare boolean prevents a + * freshly generated post-reset key from being vouched for by a stale + * community entry that survived the webview wipe. + */ + activeCommunityPubkey: string | null | undefined, isSharedIdentity: boolean, ) { if (forceMachineOnboarding()) return false; @@ -54,9 +66,16 @@ function migrateMachineOnboardingCompletion( window.localStorage.getItem( completionKey(LEGACY_ONBOARDING_COMPLETION_STORAGE_KEY, pubkey), ) === "true"; + + // A community entry vouches for the current pubkey only when its recorded + // pubkey matches (or is absent — legacy entries predate the field). + const communityVouchesForPubkey = + activeCommunityPubkey !== undefined && + (activeCommunityPubkey === null || activeCommunityPubkey === pubkey); + if ( !completedLegacyOnboarding && - !hasConfiguredCommunity && + !communityVouchesForPubkey && !isSharedIdentity ) { return false; @@ -74,10 +93,10 @@ function identitySettled(status: QueryStatus, isFetching: boolean) { } export function useMachineOnboardingState({ - hasConfiguredCommunity, + activeCommunityPubkey, isSharedIdentity, }: { - hasConfiguredCommunity: boolean; + activeCommunityPubkey: string | null | undefined; isSharedIdentity: boolean; }) { const queryClient = useQueryClient(); @@ -130,7 +149,7 @@ export function useMachineOnboardingState({ if ( migrateMachineOnboardingCompletion( currentPubkey, - hasConfiguredCommunity, + activeCommunityPubkey, isSharedIdentity, ) ) { @@ -139,7 +158,7 @@ export function useMachineOnboardingState({ setEvaluatedPubkey(currentPubkey); }, [ currentPubkey, - hasConfiguredCommunity, + activeCommunityPubkey, identityLost, identityQuery.status, isSharedIdentity, diff --git a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx index 0e90d87e3f..3849e904c4 100644 --- a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx +++ b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx @@ -1009,13 +1009,27 @@ export function ProfileSettingsCard({ onClick={() => { setIsSignOutPending(true); // Keep the pending state if signOut() resolves before restart. - signOut().catch((err: unknown) => { - setIsSignOutPending(false); - setIsSignOutOpen(false); - toast.error( - err instanceof Error ? err.message : "Sign out failed.", - ); - }); + signOut() + .then(() => { + // Clear web storage for this origin on the success path + // only. This covers dev builds where the Rust webview wipe + // targets the .app-bundle WebKit dir (missing in `tauri + // dev`), preventing stale community config from vouching + // for the fresh key on next boot. In production the Rust + // wipe already handles this; the clear here is redundant + // but harmless. The restart may race this clear — that is + // acceptable; Fix A (pubkey-scoped heuristic) is the + // correctness gate. + window.localStorage.clear(); + window.sessionStorage.clear(); + }) + .catch((err: unknown) => { + setIsSignOutPending(false); + setIsSignOutOpen(false); + toast.error( + err instanceof Error ? err.message : "Sign out failed.", + ); + }); }} > {isSignOutPending ? "Signing out…" : "Delete My Data"} From 151d1de49c46b056751ce8f3d29836f890cc1389 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Tue, 21 Jul 2026 10:54:33 -0700 Subject: [PATCH 2/5] fix(desktop): stamp pubkey on community creation; tighten migration voucher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thufir pass 1: addCommunity() in App.tsx's first-community flow and AppShell.tsx's add-community dialog both created records without stamping Community.pubkey. Any such record — including a current-format entry created in the session before a dev reset — lands in localStorage with pubkey absent. The previous migration treated absent-pubkey as a legacy signal and granted completion to every pubkey, so the regression was still reachable. This pass: - Stamps pubkey at both addCommunity call sites (App.tsx and AppShell.tsx) using the active identity pubkey at create time. - Removes the null-vouching arm from migrateMachineOnboardingCompletion: absent pubkey no longer vouches. A stale community without a stamp (legacy or pre-fix build) cannot trigger machine-onboarding bypass. - Adds the required regression case: absent-pubkey community + fresh identity → machine onboarding remains incomplete. - Updates currentPubkey in the useCallback deps array (biome lint). Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/src/app/App.tsx | 5 ++++ desktop/src/app/AppShell.tsx | 6 ++++- .../onboarding/machineOnboarding.test.mjs | 23 +++++++++++-------- .../features/onboarding/machineOnboarding.ts | 22 ++++++++++-------- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index d304f638e2..56b84118ad 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -259,9 +259,11 @@ function AppReady({ } function CommunityApp({ + currentPubkey, onBackToMachineConfig, sharedIdentity, }: { + currentPubkey: string | null; onBackToMachineConfig: () => void; sharedIdentity: boolean; }) { @@ -325,6 +327,7 @@ function CommunityApp({ relayUrl: transaction.relayUrl, token: transaction.token, reposDir: transaction.reposDir, + pubkey: currentPubkey ?? undefined, addedAt: new Date().toISOString(), }); communityOnboarding.update({ @@ -340,6 +343,7 @@ function CommunityApp({ addCommunity, communities, communityOnboarding, + currentPubkey, reconnectCommunity, switchCommunity, ]); @@ -550,6 +554,7 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) { if (machine.stage === "ready") { return ( diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 39c13527a7..369241bac2 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -789,7 +789,11 @@ export function AppShell() { isCreateChannelOpen={isCreateChannelOpen} isPresencePending={presenceSession.isPending} onAddCommunity={(community) => { - const id = communitiesHook.addCommunity(community); + const id = communitiesHook.addCommunity({ + ...community, + pubkey: + community.pubkey ?? identityQuery.data?.pubkey, + }); handleSwitchCommunity(id); }} onAddCommunityOpenChange={ diff --git a/desktop/src/features/onboarding/machineOnboarding.test.mjs b/desktop/src/features/onboarding/machineOnboarding.test.mjs index fae946bed3..ec564d83d1 100644 --- a/desktop/src/features/onboarding/machineOnboarding.test.mjs +++ b/desktop/src/features/onboarding/machineOnboarding.test.mjs @@ -88,24 +88,29 @@ test("migrate_matching_community_pubkey_vouches_for_current_key", () => { }); }); -// ── Absent community pubkey (legacy entry) ─────────────────────────────────── +// ── Absent community pubkey — regression case (Thufir pass 1) ─────────────── -test("migrate_absent_community_pubkey_vouches_for_current_key", () => { - // Community exists but has no pubkey field (pre-field legacy entry). - // `null` represents absent → treated as a match to preserve the - // veteran-upgrade path. +test("migrate_absent_community_pubkey_does_not_vouch_for_fresh_identity", () => { + // Stale current-format community with no pubkey stamp (created before the + // stamp was added, or from an older build) + freshly generated identity + // after a dev reset. The absent pubkey must NOT vouch — this was the + // reachable producer of the half-onboarded state. withFakeWindow({}, (storage) => { const result = migrateMachineOnboardingCompletion( PUBKEY_A, - null, // absent pubkey on a community that does exist + null, // absent pubkey — could be legacy OR an unstamped modern entry false, ); assert.equal( result, - true, - "absent pubkey must vouch (legacy compatibility)", + false, + "absent pubkey must not vouch for a fresh identity", + ); + assert.equal( + storage.getItem(V2_KEY), + null, + "completion must not be written to storage", ); - assert.equal(storage.getItem(V2_KEY), "true"); }); }); diff --git a/desktop/src/features/onboarding/machineOnboarding.ts b/desktop/src/features/onboarding/machineOnboarding.ts index 734ba93f1f..8bc4cadaa9 100644 --- a/desktop/src/features/onboarding/machineOnboarding.ts +++ b/desktop/src/features/onboarding/machineOnboarding.ts @@ -47,14 +47,14 @@ export function migrateMachineOnboardingCompletion( pubkey: string, /** * The `pubkey` field of the active community from localStorage, or - * `undefined` if no community is configured. When a community is present - * but its `pubkey` field is absent (legacy entries predating the field), - * pass `null` — absent is treated as a match so the veteran-upgrade path - * is preserved. Pass `undefined` when there is no active community at all. + * `undefined` if no community is configured. Community-creation paths stamp + * the current identity's pubkey on write; absent pubkey (`null`) therefore + * indicates a legacy entry that pre-dates the stamp and is NOT treated as a + * voucher. Pass `undefined` when there is no active community at all. * - * Using the community's own pubkey rather than a bare boolean prevents a - * freshly generated post-reset key from being vouched for by a stale - * community entry that survived the webview wipe. + * Using the community's own pubkey prevents a freshly generated post-reset + * key from being vouched for by a stale community entry that survived the + * webview wipe. */ activeCommunityPubkey: string | null | undefined, isSharedIdentity: boolean, @@ -68,10 +68,14 @@ export function migrateMachineOnboardingCompletion( ) === "true"; // A community entry vouches for the current pubkey only when its recorded - // pubkey matches (or is absent — legacy entries predate the field). + // pubkey matches. Absent pubkey (legacy entries predating the stamp) and + // no community at all (undefined) do not vouch — after community creation + // paths stamp pubkey on write, absent means the entry pre-dates the stamp + // and cannot be trusted to identify which identity created it. const communityVouchesForPubkey = activeCommunityPubkey !== undefined && - (activeCommunityPubkey === null || activeCommunityPubkey === pubkey); + activeCommunityPubkey !== null && + activeCommunityPubkey === pubkey; if ( !completedLegacyOnboarding && From 8e47cc6cb3fdeb2fb5e48234628ea3d1840be40d Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Tue, 21 Jul 2026 11:18:29 -0700 Subject: [PATCH 3/5] test(desktop): stamp active pubkey on E2E default community fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seedDefaultCommunity() created the community without a pubkey field. With the strict migrateMachineOnboardingCompletion voucher (absent pubkey no longer grants), any test that seeds this community but skips the v1 onboarding-completion seed would be routed to machine onboarding — breaking six smoke E2E tests that use a Tyler identity. Pass the active identity pubkey into seedDefaultCommunity and stamp it on the fixture community, matching what production creation paths now do. Uses identity?.pubkey ?? DEFAULT_MOCK_PUBKEY so mock-mode tests (where the Tauri mock drives a deadbeef identity) also get a matched record. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/tests/helpers/bridge.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 71bb7ba6e3..58d58822c6 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -580,14 +580,19 @@ async function seedOnboardingCompletionForKnownIdentities( ); } -async function seedDefaultCommunity(page: Page, relayWsUrl?: string) { +async function seedDefaultCommunity( + page: Page, + activePubkey: string, + relayWsUrl?: string, +) { await page.addInitScript( - ({ relayUrl }) => { + ({ pubkey, relayUrl }) => { const communityId = "e2e-default-community"; const community = { id: communityId, name: "E2E Test", relayUrl, + pubkey, addedAt: new Date().toISOString(), }; window.localStorage.setItem( @@ -596,7 +601,7 @@ async function seedDefaultCommunity(page: Page, relayWsUrl?: string) { ); window.localStorage.setItem("buzz-active-community-id", communityId); }, - { relayUrl: relayWsUrl ?? DEFAULT_RELAY_WS_URL }, + { pubkey: activePubkey, relayUrl: relayWsUrl ?? DEFAULT_RELAY_WS_URL }, ); } @@ -619,8 +624,11 @@ export async function installBridge(page: Page, options: BridgeOptions) { // Most specs seed a community so useCommunityInit doesn't show WelcomeSetup. // skipOnboardingSeed only controls the onboarding-completion flag. + // The community is stamped with the active identity's pubkey so the strict + // migrateMachineOnboardingCompletion voucher recognises it. if (!options.skipCommunitySeed) { - await seedDefaultCommunity(page, options.relayWsUrl); + const activePubkey = identity?.pubkey ?? DEFAULT_MOCK_PUBKEY; + await seedDefaultCommunity(page, activePubkey, options.relayWsUrl); } if (!options.skipOnboardingSeed) { await seedOnboardingCompletionForKnownIdentities(page, options.relayWsUrl); From 7c9abab9ac223523e1cada1666d9800b49c99a2e Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Tue, 21 Jul 2026 11:31:33 -0700 Subject: [PATCH 4/5] test(desktop): read seedActiveIdentity override in community fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous stamp used identity?.pubkey ?? DEFAULT_MOCK_PUBKEY, which is always DEFAULT_MOCK_PUBKEY in mock mode regardless of seedActiveIdentity overrides. Tests that call seedActiveIdentity(BLANK_TYLER_IDENTITY) before installMockBridge() end up with a community stamped deadbeef... while the app identity is Tyler — strict matching rejects the voucher. Read buzz:e2e-identity-override.v1 from localStorage inside the init script (addInitScript runs in registration order, so the seedActiveIdentity write always precedes this read). Fall back to the passed pubkey (bridge identity in relay mode, DEFAULT_MOCK_PUBKEY for mock) only when no valid override exists. Mirror the storage key as a const in the init-script args so the script closure doesn't capture module-scope imports. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/tests/helpers/bridge.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 58d58822c6..39b81f0de2 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -582,17 +582,25 @@ async function seedOnboardingCompletionForKnownIdentities( async function seedDefaultCommunity( page: Page, - activePubkey: string, + fallbackPubkey: string, relayWsUrl?: string, ) { await page.addInitScript( - ({ pubkey, relayUrl }) => { + ({ fallback, identityOverrideKey, relayUrl }) => { + // If seedActiveIdentity() ran before this script (the normal ordering), + // use its pubkey so the community matches the active identity and + // migrateMachineOnboardingCompletion's strict voucher accepts it. + const overrideRaw = window.localStorage.getItem(identityOverrideKey); + const overridePubkey = + overrideRaw !== null + ? (JSON.parse(overrideRaw) as { pubkey?: string }).pubkey + : undefined; const communityId = "e2e-default-community"; const community = { id: communityId, name: "E2E Test", relayUrl, - pubkey, + pubkey: overridePubkey ?? fallback, addedAt: new Date().toISOString(), }; window.localStorage.setItem( @@ -601,7 +609,11 @@ async function seedDefaultCommunity( ); window.localStorage.setItem("buzz-active-community-id", communityId); }, - { pubkey: activePubkey, relayUrl: relayWsUrl ?? DEFAULT_RELAY_WS_URL }, + { + fallback: fallbackPubkey, + identityOverrideKey: "buzz:e2e-identity-override.v1", + relayUrl: relayWsUrl ?? DEFAULT_RELAY_WS_URL, + }, ); } @@ -625,7 +637,9 @@ export async function installBridge(page: Page, options: BridgeOptions) { // Most specs seed a community so useCommunityInit doesn't show WelcomeSetup. // skipOnboardingSeed only controls the onboarding-completion flag. // The community is stamped with the active identity's pubkey so the strict - // migrateMachineOnboardingCompletion voucher recognises it. + // migrateMachineOnboardingCompletion voucher recognises it. The init script + // reads the seedActiveIdentity override key (if present) and falls back to + // the bridge identity's pubkey or DEFAULT_MOCK_PUBKEY for mock mode. if (!options.skipCommunitySeed) { const activePubkey = identity?.pubkey ?? DEFAULT_MOCK_PUBKEY; await seedDefaultCommunity(page, activePubkey, options.relayWsUrl); From bcf9f2a9ea6b1bbf2ed4c26e08d38cb9c9d206a3 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Tue, 21 Jul 2026 11:50:20 -0700 Subject: [PATCH 5/5] test: guard JSON.parse in seedDefaultCommunity init script Mirror readStoredIdentityOverride()'s shape validation exactly: wrap the parse in try/catch and only set overridePubkey when privateKey, pubkey, and username are all strings. Malformed JSON, stored null, and partial objects (e.g. missing privateKey/username) now fall through to the fallback pubkey instead of throwing or accepting a shape that runtime readStoredIdentityOverride() would reject. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/tests/helpers/bridge.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 39b81f0de2..83e3f8fd18 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -590,11 +590,28 @@ async function seedDefaultCommunity( // If seedActiveIdentity() ran before this script (the normal ordering), // use its pubkey so the community matches the active identity and // migrateMachineOnboardingCompletion's strict voucher accepts it. - const overrideRaw = window.localStorage.getItem(identityOverrideKey); - const overridePubkey = - overrideRaw !== null - ? (JSON.parse(overrideRaw) as { pubkey?: string }).pubkey - : undefined; + // Mirror readStoredIdentityOverride()'s shape validation: require all + // three fields to be non-empty strings; fall through to fallback on + // malformed JSON, stored null, or partial objects. + let overridePubkey: string | undefined; + try { + const overrideRaw = window.localStorage.getItem(identityOverrideKey); + if (overrideRaw !== null) { + const parsed: unknown = JSON.parse(overrideRaw); + if ( + parsed !== null && + typeof parsed === "object" && + typeof (parsed as Record).pubkey === "string" && + typeof (parsed as Record).privateKey === + "string" && + typeof (parsed as Record).username === "string" + ) { + overridePubkey = (parsed as { pubkey: string }).pubkey; + } + } + } catch { + // malformed entry — fall through to fallback + } const communityId = "e2e-default-community"; const community = { id: communityId,