diff --git a/app/src/utils/tauriCommands/core.test.ts b/app/src/utils/tauriCommands/core.test.ts index 30b1c8002..cef018ffa 100644 --- a/app/src/utils/tauriCommands/core.test.ts +++ b/app/src/utils/tauriCommands/core.test.ts @@ -1,5 +1,5 @@ import { invoke, isTauri } from '@tauri-apps/api/core'; -import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, type Mock, test, vi } from 'vitest'; vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(), isTauri: vi.fn() })); vi.mock('../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() })); @@ -19,6 +19,30 @@ describe('tauriCommands/core', () => { }); describe('restartApp', () => { + // window.location.reload is non-configurable on jsdom's default location; + // swap in a mocked location object for the dev-mode tests and restore after. + let originalLocation: Location; + let reloadSpy: Mock; + + beforeEach(() => { + originalLocation = window.location; + reloadSpy = vi.fn(); + Object.defineProperty(window, 'location', { + value: { ...originalLocation, reload: reloadSpy }, + configurable: true, + writable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'location', { + value: originalLocation, + configurable: true, + writable: true, + }); + vi.unstubAllEnvs(); + }); + test('no-ops when not in Tauri', async () => { mockIsTauri.mockReturnValue(false); const debug = vi.spyOn(console, 'debug').mockImplementation(() => {}); @@ -26,15 +50,45 @@ describe('tauriCommands/core', () => { await restartApp(); expect(mockInvoke).not.toHaveBeenCalled(); + expect(reloadSpy).not.toHaveBeenCalled(); expect(debug).toHaveBeenCalledWith( expect.stringContaining('restartApp: skipped — not running in Tauri') ); debug.mockRestore(); }); - test('invokes restart_app when in Tauri', async () => { + test('reloads webview in dev mode (#1068 — avoid orphaning vite)', async () => { + // setup.ts seeds DEV=true globally; the binding imported above already + // captured that value, so we just need to invoke the dev-mode branch. + const debug = vi.spyOn(console, 'debug').mockImplementation(() => {}); + await restartApp(); + + expect(reloadSpy).toHaveBeenCalledTimes(1); + expect(mockInvoke).not.toHaveBeenCalled(); + expect(debug).toHaveBeenCalledWith( + expect.stringContaining('restartApp: dev mode → window.location.reload()') + ); + debug.mockRestore(); + }); + + test('invokes restart_app in production mode (IS_DEV=false)', async () => { + // setup.ts globally mocks ../config with IS_DEV: true. Override with + // doMock + resetModules so a fresh import of ./core sees IS_DEV=false + // and runs the production branch (#1068 dev workaround should be inert). + vi.doMock('../config', () => ({ + IS_DEV: false, + // Re-export anything else core.ts might end up using; today just IS_DEV. + })); + vi.resetModules(); + const prodCore = await import('./core'); + + await prodCore.restartApp(); + expect(mockInvoke).toHaveBeenCalledWith('restart_app'); + expect(reloadSpy).not.toHaveBeenCalled(); + + vi.doUnmock('../config'); }); }); diff --git a/app/src/utils/tauriCommands/core.ts b/app/src/utils/tauriCommands/core.ts index b19e8c2be..c268b7f35 100644 --- a/app/src/utils/tauriCommands/core.ts +++ b/app/src/utils/tauriCommands/core.ts @@ -4,6 +4,7 @@ import { invoke } from '@tauri-apps/api/core'; import { callCoreRpc } from '../../services/coreRpcClient'; +import { IS_DEV } from '../config'; import { CommandResponse, isTauri } from './common'; export interface CoreUpdateStatus { @@ -61,12 +62,28 @@ export async function restartCoreProcess(): Promise { /** * Restart the desktop shell so CEF relaunches with updated profile paths. + * + * In `pnpm dev:app` the launcher graph is: + * `pnpm tauri dev` → `cargo run` → `tauri-cef` CLI → `vite` (child). + * Tauri's `app.restart()` exits the cargo parent, which orphans/kills the + * vite child and tears down the entire dev session (#1068). Use a webview + * reload in dev mode instead — module init re-runs, so localStorage seeds + * (e.g. `OPENHUMAN_ACTIVE_USER_ID`, set by `setActiveUserId` before the + * caller invokes us) are read fresh and redux-persist re-hydrates from + * the active user's namespace, all without touching the cargo / vite + * processes. Packaged builds keep the original `app.restart()` path — + * there is no vite child to orphan there. */ export async function restartApp(): Promise { if (!isTauri()) { console.debug('[app] restartApp: skipped — not running in Tauri'); return; } + if (IS_DEV) { + console.debug('[app] restartApp: dev mode → window.location.reload()'); + window.location.reload(); + return; + } console.debug('[app] restartApp: invoking restart_app'); await invoke('restart_app'); }