mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
+12
-8
@@ -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(<React.StrictMode>{tree}</React.StrictMode>);
|
||||
}
|
||||
|
||||
// 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<string | null>(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))
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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<string | null>;
|
||||
}
|
||||
|
||||
export function shouldSkipLocalActiveUserRead(opts: {
|
||||
isStandaloneNativeWindow: boolean;
|
||||
coreMode: 'local' | 'cloud' | null;
|
||||
}): boolean {
|
||||
return opts.isStandaloneNativeWindow || opts.coreMode === 'cloud';
|
||||
}
|
||||
|
||||
export function resolveActiveUserBootstrap(ctx: BootstrapContext): Promise<string | null> {
|
||||
if (
|
||||
shouldSkipLocalActiveUserRead({
|
||||
isStandaloneNativeWindow: ctx.isStandaloneNativeWindow,
|
||||
coreMode: ctx.coreMode,
|
||||
})
|
||||
) {
|
||||
return Promise.resolve<string | null>(null);
|
||||
}
|
||||
return ctx.getActiveUserIdFromCore();
|
||||
}
|
||||
Reference in New Issue
Block a user