From b9c88abe90e2827a2203026c69808e269aa2fa7c Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Thu, 9 Jul 2026 05:25:55 +0530 Subject: [PATCH] fix(boot): skip stale local active-user seed in cloud mode (#4545) (#4704) --- app/src/main.tsx | 20 ++-- .../__tests__/bootstrapActiveUser.test.ts | 92 +++++++++++++++++++ app/src/utils/bootstrapActiveUser.ts | 46 ++++++++++ 3 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 app/src/utils/__tests__/bootstrapActiveUser.test.ts create mode 100644 app/src/utils/bootstrapActiveUser.ts diff --git a/app/src/main.tsx b/app/src/main.tsx index 5cfb609db..5a8eeeb40 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -16,7 +16,9 @@ import { setStoreForApiClient } from './services/apiClient'; import { primeActiveUserId } from './store/userScopedStorage'; import './styles/code-highlight.css'; import './styles/theme.css'; +import { resolveActiveUserBootstrap } from './utils/bootstrapActiveUser'; import { APP_VERSION } from './utils/config'; +import { getStoredCoreMode } from './utils/configPersistence'; import { setupDesktopDeepLinkListener } from './utils/desktopDeepLinkListener'; import { missingHashRedirectTarget } from './utils/hashRouterBootstrap'; import { getActiveUserIdFromCore } from './utils/tauriCommands'; @@ -103,14 +105,16 @@ function bootRender() { root.render({tree}); } -// The mascot and notch windows live in native WKWebViews (no Tauri IPC), so -// `getActiveUserIdFromCore()` would just reject after a roundtrip and -// delay first paint for nothing. Skip the bootstrap entirely in those -// paths — neither UI reads user-scoped storage. -const activeUserBootstrap = - isMascotWindow || isNotchWindow - ? Promise.resolve(null) - : getActiveUserIdFromCore(); +// Decide which source (Rust IPC vs preserved localStorage seed) primes +// `userScopedStorage` at boot. Cloud mode and standalone native windows +// both fall through to `primeActiveUserId(null)`, which preserves whatever +// `setActiveUserId(...)` wrote in a prior session — see +// `resolveActiveUserBootstrap` for the full rationale (#4545, #900). +const activeUserBootstrap = resolveActiveUserBootstrap({ + isStandaloneNativeWindow: isMascotWindow || isNotchWindow, + coreMode: getStoredCoreMode(), + getActiveUserIdFromCore, +}); activeUserBootstrap .then(id => primeActiveUserId(id)) diff --git a/app/src/utils/__tests__/bootstrapActiveUser.test.ts b/app/src/utils/__tests__/bootstrapActiveUser.test.ts new file mode 100644 index 000000000..bf4ec3cc0 --- /dev/null +++ b/app/src/utils/__tests__/bootstrapActiveUser.test.ts @@ -0,0 +1,92 @@ +/** + * Regression tests for the active-user boot decision that gates #4545. + * + * The end-to-end loop the fix prevents (see `resolveActiveUserBootstrap` + * docblock): + * 1. User picks Cloud/Remote core → boot succeeds + * 2. `CoreStateProvider` fetches the remote snapshot → nextIdentity = REMOTE user + * 3. Boot primes seed from local `active_user.toml` → seedUserId = OLD LOCAL user + * 4. `seedUserId !== nextIdentity` → `handleIdentityFlip` → `restartApp` + * 5. Repeat forever + * + * The fix short-circuits step 3 whenever `coreMode === 'cloud'` (or the + * window is a standalone native mascot/notch webview with no Tauri IPC). + */ +import { describe, expect, test, vi } from 'vitest'; + +import { resolveActiveUserBootstrap, shouldSkipLocalActiveUserRead } from '../bootstrapActiveUser'; + +describe('shouldSkipLocalActiveUserRead', () => { + test('cloud mode skips the local read', () => { + expect( + shouldSkipLocalActiveUserRead({ isStandaloneNativeWindow: false, coreMode: 'cloud' }) + ).toBe(true); + }); + + test('local mode reads through', () => { + expect( + shouldSkipLocalActiveUserRead({ isStandaloneNativeWindow: false, coreMode: 'local' }) + ).toBe(false); + }); + + test('unset mode reads through (fresh install falls back to local file → likely null → prime(null))', () => { + expect(shouldSkipLocalActiveUserRead({ isStandaloneNativeWindow: false, coreMode: null })).toBe( + false + ); + }); + + test('standalone native window skips regardless of core mode', () => { + expect( + shouldSkipLocalActiveUserRead({ isStandaloneNativeWindow: true, coreMode: 'local' }) + ).toBe(true); + expect( + shouldSkipLocalActiveUserRead({ isStandaloneNativeWindow: true, coreMode: 'cloud' }) + ).toBe(true); + }); +}); + +describe('resolveActiveUserBootstrap', () => { + test('cloud mode resolves null WITHOUT calling getActiveUserIdFromCore (#4545)', async () => { + const getActiveUserIdFromCore = vi.fn().mockResolvedValue('stale-local-user'); + const result = await resolveActiveUserBootstrap({ + isStandaloneNativeWindow: false, + coreMode: 'cloud', + getActiveUserIdFromCore, + }); + expect(result).toBeNull(); + expect(getActiveUserIdFromCore).not.toHaveBeenCalled(); + }); + + test('local mode delegates to getActiveUserIdFromCore', async () => { + const getActiveUserIdFromCore = vi.fn().mockResolvedValue('user-A'); + const result = await resolveActiveUserBootstrap({ + isStandaloneNativeWindow: false, + coreMode: 'local', + getActiveUserIdFromCore, + }); + expect(result).toBe('user-A'); + expect(getActiveUserIdFromCore).toHaveBeenCalledTimes(1); + }); + + test('unset mode delegates to getActiveUserIdFromCore (pre-picker cold boot)', async () => { + const getActiveUserIdFromCore = vi.fn().mockResolvedValue(null); + const result = await resolveActiveUserBootstrap({ + isStandaloneNativeWindow: false, + coreMode: null, + getActiveUserIdFromCore, + }); + expect(result).toBeNull(); + expect(getActiveUserIdFromCore).toHaveBeenCalledTimes(1); + }); + + test('standalone native window resolves null WITHOUT calling the IPC', async () => { + const getActiveUserIdFromCore = vi.fn().mockResolvedValue('should-not-be-read'); + const result = await resolveActiveUserBootstrap({ + isStandaloneNativeWindow: true, + coreMode: 'local', + getActiveUserIdFromCore, + }); + expect(result).toBeNull(); + expect(getActiveUserIdFromCore).not.toHaveBeenCalled(); + }); +}); diff --git a/app/src/utils/bootstrapActiveUser.ts b/app/src/utils/bootstrapActiveUser.ts new file mode 100644 index 000000000..ed5781834 --- /dev/null +++ b/app/src/utils/bootstrapActiveUser.ts @@ -0,0 +1,46 @@ +/** + * Decide which async source seeds `userScopedStorage`'s active-user id at + * boot, before `primeActiveUserId(...)` runs. + * + * Three source shapes: + * 1. Mascot/notch native windows — no Tauri IPC, cannot invoke commands. + * 2. Cloud/remote core mode — the local `~/.openhuman/active_user.toml` + * is either empty (no prior local session) or bound to a prior LOCAL + * session's user id. In both cases it doesn't match the REMOTE core's + * authenticated user, and priming from it overwrites the correct + * `localStorage` seed that `handleIdentityFlip` writes just before + * `restartApp`. That mismatch drives the infinite + * `identityFlip → restartApp` restart loop reported in #4545. + * 3. Local core mode — read the Rust `active_user.toml` via IPC. This is + * the profile-independent source of truth the local sidecar writes + * atomically during `auth_store_session` (#900). + * + * Cases (1) and (2) resolve `null`; `primeActiveUserId(null)` then preserves + * the existing `localStorage` seed rather than wiping it. See + * `userScopedStorage.ts::primeActiveUserId` and the "cloud-mode reload + * survival" test. + */ +export interface BootstrapContext { + isStandaloneNativeWindow: boolean; + coreMode: 'local' | 'cloud' | null; + getActiveUserIdFromCore: () => Promise; +} + +export function shouldSkipLocalActiveUserRead(opts: { + isStandaloneNativeWindow: boolean; + coreMode: 'local' | 'cloud' | null; +}): boolean { + return opts.isStandaloneNativeWindow || opts.coreMode === 'cloud'; +} + +export function resolveActiveUserBootstrap(ctx: BootstrapContext): Promise { + if ( + shouldSkipLocalActiveUserRead({ + isStandaloneNativeWindow: ctx.isStandaloneNativeWindow, + coreMode: ctx.coreMode, + }) + ) { + return Promise.resolve(null); + } + return ctx.getActiveUserIdFromCore(); +}