From 7187ce3f2781edc9cfcb34108c1ccb73a63d503b Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Fri, 12 Jun 2026 19:54:17 +0530 Subject: [PATCH] fix(intelligence): refetch memory sources when session becomes authenticated (#3449) (#3621) --- .../intelligence/MemorySourcesRegistry.tsx | 18 ++++- .../__tests__/MemorySourcesRegistry.test.tsx | 67 ++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/app/src/components/intelligence/MemorySourcesRegistry.tsx b/app/src/components/intelligence/MemorySourcesRegistry.tsx index cb15d71ba..b12edb91d 100644 --- a/app/src/components/intelligence/MemorySourcesRegistry.tsx +++ b/app/src/components/intelligence/MemorySourcesRegistry.tsx @@ -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([]); const [statuses, setStatuses] = useState([]); 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; diff --git a/app/src/components/intelligence/__tests__/MemorySourcesRegistry.test.tsx b/app/src/components/intelligence/__tests__/MemorySourcesRegistry.test.tsx index 1e86e1660..2628025ab 100644 --- a/app/src/components/intelligence/__tests__/MemorySourcesRegistry.test.tsx +++ b/app/src/components/intelligence/__tests__/MemorySourcesRegistry.test.tsx @@ -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) => ( + + + + {/* pollIntervalMs=0 disables the background poll, so the only way + the second fetch can fire is the auth transition. */} + + + + + ); + + 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(); + }); });