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();
+}