fix(intelligence): refetch memory sources when session becomes authenticated (#3449) (#3621)

This commit is contained in:
sanil-23
2026-06-12 19:54:17 +05:30
committed by GitHub
parent 59aadab2d5
commit 7187ce3f27
2 changed files with 81 additions and 4 deletions
@@ -9,9 +9,10 @@
* background and emits MemorySyncStageChanged events.
*/
import debug from 'debug';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useT } from '../../lib/i18n/I18nContext';
import { CoreStateContext } from '../../providers/coreStateContext';
import {
applyAllIn,
type FreshnessLabel,
@@ -122,6 +123,12 @@ export function MemorySourcesRegistry({
pollIntervalMs = 5000,
}: MemorySourcesRegistryProps) {
const { t } = useT();
// Read the core snapshot directly (not via the throwing `useCoreState`
// hook) so this component still renders in unit tests that mount it
// without a CoreStateProvider — there `ctx` is null and `isAuthenticated`
// stays a stable `false`, so the load effect behaves exactly as before.
const coreState = useContext(CoreStateContext);
const isAuthenticated = coreState?.snapshot.auth.isAuthenticated ?? false;
const [sources, setSources] = useState<MemorySourceEntry[]>([]);
const [statuses, setStatuses] = useState<SourceStatus[]>([]);
const [loading, setLoading] = useState(true);
@@ -282,9 +289,16 @@ export function MemorySourcesRegistry({
}
}, []);
// Load on mount AND whenever the session transitions to authenticated.
// After a page reload the registry can mount (e.g. via a persisted
// `?tab=memory` deep link) *before* CoreStateProvider has restored the
// session, so the initial fetch runs against a not-yet-ready core and
// surfaces nothing. Re-running when `isAuthenticated` flips true picks up
// sources immediately instead of waiting for the next 5s poll — which
// under CI load was racing the E2E visibility timeout (#3449).
useEffect(() => {
void refresh();
}, [refresh]);
}, [refresh, isAuthenticated]);
useEffect(() => {
if (!pollIntervalMs) return undefined;
@@ -2,12 +2,16 @@
* Unit tests for MemorySourcesRegistry — All In button, gear/settings panel,
* per-kind field visibility, Save, and existing toggle behaviour.
*/
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as service from '../../../services/memorySourcesService';
import { getCoreStateSnapshot } from '../../../lib/coreState/store';
import { CoreStateContext, type CoreStateContextValue } from '../../../providers/coreStateContext';
import type { MemorySourceEntry } from '../../../services/memorySourcesService';
import { renderWithProviders } from '../../../test/test-utils';
import { createTestStore, renderWithProviders } from '../../../test/test-utils';
import {
openhumanGetMemorySyncSettings,
openhumanUpdateMemorySyncSettings,
@@ -430,4 +434,63 @@ describe('MemorySourcesRegistry', () => {
expect(onToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
});
});
// -------------------------------------------------------------------------
// Refetch when the session becomes authenticated (#3449)
//
// After a page reload the registry can mount before CoreStateProvider has
// restored the session. The first fetch then runs against a not-yet-ready
// core; sources must reappear as soon as auth flips true, without waiting
// for the next background poll.
// -------------------------------------------------------------------------
it('refetches sources when the session transitions to authenticated', async () => {
mockedList.mockResolvedValue([makeSource({ label: 'Reloaded Repo' })]);
mockedStatus.mockResolvedValue([]);
const coreState = (isAuthenticated: boolean): CoreStateContextValue =>
({
...getCoreStateSnapshot(),
snapshot: {
...getCoreStateSnapshot().snapshot,
auth: {
isAuthenticated,
userId: isAuthenticated ? 'u1' : null,
user: null,
profileId: null,
},
},
refresh: async () => {},
refreshTeams: async () => {},
refreshTeamMembers: async () => {},
refreshTeamInvites: async () => {},
setAnalyticsEnabled: async () => {},
setMeetAutoOrchestratorHandoff: async () => {},
setOnboardingCompletedFlag: async () => {},
setEncryptionKey: async () => {},
patchSnapshot: () => {},
setOnboardingTasks: async () => {},
storeSessionToken: async () => {},
clearSession: async () => {},
}) as CoreStateContextValue;
const store = createTestStore();
const tree = (isAuthenticated: boolean) => (
<Provider store={store}>
<CoreStateContext.Provider value={coreState(isAuthenticated)}>
<MemoryRouter>
{/* pollIntervalMs=0 disables the background poll, so the only way
the second fetch can fire is the auth transition. */}
<MemorySourcesRegistry pollIntervalMs={0} />
</MemoryRouter>
</CoreStateContext.Provider>
</Provider>
);
const { rerender } = render(tree(false));
await waitFor(() => expect(mockedList).toHaveBeenCalledTimes(1));
rerender(tree(true));
await waitFor(() => expect(mockedList).toHaveBeenCalledTimes(2));
expect(await screen.findByText('Reloaded Repo')).toBeInTheDocument();
});
});