fix(brain): reload memory graph after re-login (#4223)

This commit is contained in:
Mega Mind
2026-06-28 21:28:23 -07:00
committed by GitHub
parent 3cc9e35d8f
commit 90d574917d
2 changed files with 51 additions and 1 deletions
+15 -1
View File
@@ -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<ToastNotification[]>([]);
// 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<ToastNotification, 'id'>) => {
@@ -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';
+36
View File
@@ -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<typeof renderWithProviders>;
await act(async () => {
view = renderWithProviders(<Brain />);
});
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(<Brain />);
});
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 () => {