diff --git a/app/src/pages/Brain.tsx b/app/src/pages/Brain.tsx index 8bc80b05f..875137fc6 100644 --- a/app/src/pages/Brain.tsx +++ b/app/src/pages/Brain.tsx @@ -26,6 +26,7 @@ import MemoryDebugPanel from '../components/settings/panels/MemoryDebugPanel'; import BetaBanner from '../components/ui/BetaBanner'; import { useSubconscious } from '../hooks/useSubconscious'; import { useT } from '../lib/i18n/I18nContext'; +import { useCoreState } from '../providers/CoreStateProvider'; import type { ToastNotification } from '../types/intelligence'; import { type GraphExportResponse, @@ -96,6 +97,16 @@ export default function Brain() { const [refreshKey, setRefreshKey] = useState(0); const [toasts, setToasts] = useState([]); + // The memory graph is read from the on-disk store, but the read only fired on + // mount — so after a logout→login cycle the page kept whatever (empty) state + // it had when the core was signed-out / mid identity-flip and never refetched + // once auth was restored, showing an empty graph for an account whose data is + // still on disk (#4149). Key the load on the authenticated identity so a + // re-auth (null→user, or A→B) re-pulls the persisted graph, mirroring the + // thread-cache reload CoreStateProvider already does on identity change. + const { snapshot } = useCoreState(); + const authUserId = snapshot.auth.userId; + const sub = useSubconscious(); const addToast = useCallback((toast: Omit) => { @@ -136,7 +147,10 @@ export default function Brain() { cancelled = true; window.removeEventListener('openhuman:memory-tree-completed', onTreeDone); }; - }, [mode, refreshKey]); + // `authUserId` is a dependency so a logout→login (identity becomes + // available again) re-pulls the persisted graph instead of leaving the + // signed-out empty state on screen (#4149). + }, [mode, refreshKey, authUserId]); const cardClass = 'rounded-lg border border-line bg-surface p-4'; diff --git a/app/src/pages/__tests__/Brain.test.tsx b/app/src/pages/__tests__/Brain.test.tsx index 5120218c5..e4436b250 100644 --- a/app/src/pages/__tests__/Brain.test.tsx +++ b/app/src/pages/__tests__/Brain.test.tsx @@ -5,12 +5,23 @@ import { renderWithProviders } from '../../test/test-utils'; import Brain from '../Brain'; const graphExportMock = vi.hoisted(() => vi.fn()); +// Controllable authenticated identity so we can simulate a logout→login cycle +// (userId null → set) and assert the graph reloads (#4149). +const coreAuthRef = vi.hoisted(() => ({ current: 'user-A' as string | null })); vi.mock('../../utils/tauriCommands', () => ({ memoryTreeGraphExport: graphExportMock, isTauri: () => false, })); +vi.mock('../../providers/CoreStateProvider', () => ({ + useCoreState: () => ({ + snapshot: { + auth: { userId: coreAuthRef.current, isAuthenticated: coreAuthRef.current != null }, + }, + }), +})); + vi.mock('../../components/intelligence/MemoryGraph', async () => { const React = await import('react'); return { @@ -94,6 +105,7 @@ const makeGraph = (n: number) => ({ describe('Brain page', () => { beforeEach(() => { vi.clearAllMocks(); + coreAuthRef.current = 'user-A'; }); afterEach(() => { @@ -120,6 +132,30 @@ describe('Brain page', () => { }); }); + it('reloads the memory graph from the store when the user re-authenticates (#4149)', async () => { + // Start signed-out / mid identity-flip: the first fetch resolves empty. + coreAuthRef.current = null; + graphExportMock.mockResolvedValue(makeGraph(0)); + let view!: ReturnType; + await act(async () => { + view = renderWithProviders(); + }); + await waitFor(() => expect(graphExportMock).toHaveBeenCalledTimes(1)); + expect(screen.getByTestId('memory-graph')).toHaveTextContent('nodes:0'); + + // Re-login: identity becomes available — the graph must re-pull from the + // persistent store rather than keep the signed-out empty state. + coreAuthRef.current = 'user-A'; + graphExportMock.mockResolvedValue(makeGraph(5)); + await act(async () => { + view.rerender(); + }); + await waitFor(() => expect(graphExportMock).toHaveBeenCalledTimes(2)); + await waitFor(() => { + expect(screen.getByTestId('memory-graph')).toHaveTextContent('nodes:5'); + }); + }); + it('surfaces an error alert when the fetch fails', async () => { graphExportMock.mockRejectedValue(new Error('boom')); await act(async () => {