import React, { act, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useGraphMemberLogPreviews } from '@features/agent-graph/renderer/hooks/useGraphMemberLogPreviews'; import type { MemberLogPreviewResponse } from '@features/member-log-stream/contracts'; const apiMock = vi.hoisted(() => ({ memberLogStream: { getMemberLogPreviews: vi.fn(), }, teams: { onTeamChange: vi.fn(), }, })); vi.mock('@renderer/api', () => ({ api: apiMock, })); function createDeferred(): { promise: Promise; resolve: (value: T) => void; reject: (reason?: unknown) => void; } { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((innerResolve, innerReject) => { resolve = innerResolve; reject = innerReject; }); return { promise, resolve, reject }; } function response(memberName: string, generatedAt: string): MemberLogPreviewResponse { return { generatedAt, members: [ { memberName, items: [ { id: `${memberName}:${generatedAt}`, kind: 'text', provider: 'claude_transcript', timestamp: generatedAt, title: 'Assistant', preview: memberName, tone: 'neutral', }, ], coverage: [{ provider: 'claude_transcript', status: 'included' }], warnings: [], truncated: false, overflowCount: 0, generatedAt, }, ], }; } function emptyResponse(memberName: string, generatedAt: string): MemberLogPreviewResponse { return { generatedAt, members: [ { memberName, items: [], coverage: [{ provider: 'claude_transcript', status: 'skipped' }], warnings: [], truncated: false, overflowCount: 0, generatedAt, }, ], }; } function batchResponse(memberNames: string[], generatedAt: string): MemberLogPreviewResponse { return { generatedAt, members: memberNames.map((memberName) => ({ memberName, items: [ { id: `${memberName}:${generatedAt}`, kind: 'text', provider: 'claude_transcript', timestamp: generatedAt, title: 'Assistant', preview: memberName, tone: 'neutral', }, ], coverage: [{ provider: 'claude_transcript', status: 'included' }], warnings: [], truncated: false, overflowCount: 0, generatedAt, })), }; } const HookProbe = ({ teamName, memberNames, laneIdsByMember, enabled = true, onState, }: { teamName: string; memberNames: string[]; laneIdsByMember?: Record; enabled?: boolean; onState: (state: ReturnType) => void; }): React.JSX.Element | null => { const state = useGraphMemberLogPreviews({ teamName, memberNames, laneIdsByMember, enabled, }); useEffect(() => { onState(state); }, [onState, state]); return null; }; const ReloadProbe = ({ teamName, memberNames, onState, onReload, }: { teamName: string; memberNames: string[]; onState: (state: ReturnType) => void; onReload: (reload: ReturnType['reload']) => void; }): React.JSX.Element | null => { const state = useGraphMemberLogPreviews({ teamName, memberNames, }); useEffect(() => { onState(state); onReload(state.reload); }, [onReload, onState, state]); return null; }; describe('useGraphMemberLogPreviews', () => { beforeEach(() => { vi.useFakeTimers(); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); apiMock.memberLogStream.getMemberLogPreviews.mockReset(); apiMock.teams.onTeamChange.mockReset(); apiMock.teams.onTeamChange.mockReturnValue(() => undefined); Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'visible', }); }); afterEach(() => { document.body.innerHTML = ''; vi.useRealTimers(); vi.unstubAllGlobals(); }); it('debounces visible member batch requests and passes safe lane ids', async () => { apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).not.toHaveBeenCalled(); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ maxItemsPerMember: 3, textLimit: 200, forceRefresh: true, laneIdsByMember: { alice: 'secondary:opencode:alice' }, }) ); act(() => { root.unmount(); }); }); it('shows loading while the first visible preview request is still debounced', async () => { const firstLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews.mockReturnValueOnce(firstLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(apiMock.memberLogStream.getMemberLogPreviews).not.toHaveBeenCalled(); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); await act(async () => { firstLoad.resolve(response('alice', '2026-04-03T00:00:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); act(() => { root.unmount(); }); }); it('does not duplicate the initial debounced request in React StrictMode', async () => { apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); act(() => { root.unmount(); }); }); it('clears a scheduled preview request when unmounted before the debounce fires', async () => { apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); act(() => { root.unmount(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).not.toHaveBeenCalled(); }); it('keeps completed previews cached after the visible member set changes', async () => { const aliceLoad = createDeferred(); const bobLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews .mockReturnValueOnce(aliceLoad.promise) .mockReturnValueOnce(bobLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); await act(async () => { aliceLoad.resolve(response('alice', '2026-04-03T00:00:00.000Z')); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); await act(async () => { bobLoad.resolve(response('bob', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('bob')?.items[0]?.preview).toBe('bob'); act(() => { root.unmount(); }); }); it('keeps cached previews while pan or zoom changes the visible member batch', async () => { const bobLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews .mockResolvedValueOnce(response('alice', '2026-04-03T00:00:00.000Z')) .mockReturnValueOnce(bobLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); await act(async () => { root.render(); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); await act(async () => { bobLoad.resolve(response('bob', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('bob')?.items[0]?.preview).toBe('bob'); act(() => { root.unmount(); }); }); it('does not show stale previews as loaded after switching teams with the same visible member', async () => { const betaLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews .mockResolvedValueOnce(response('alice', '2026-04-03T00:00:00.000Z')) .mockReturnValueOnce(betaLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.id).toBe( 'alice:2026-04-03T00:00:00.000Z' ); await act(async () => { root.render(); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(latestState()?.previewsByMember.get('alice')).toBeUndefined(); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'beta-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); await act(async () => { betaLoad.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.id).toBe( 'alice:2026-04-03T00:01:00.000Z' ); act(() => { root.unmount(); }); }); it('does not duplicate preview requests when the same visible members are reordered', async () => { const firstLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews.mockReturnValueOnce(firstLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice', 'bob'], expect.any(Object) ); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { firstLoad.resolve(batchResponse(['alice', 'bob'], '2026-04-03T00:00:00.000Z')); await Promise.resolve(); }); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); act(() => { root.unmount(); }); }); it('ignores stale responses when the same member receives a newer lane request', async () => { const oldLaneLoad = createDeferred(); const newLaneLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews .mockReturnValueOnce(oldLaneLoad.promise) .mockReturnValueOnce(newLaneLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render( ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { root.render( ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); await act(async () => { newLaneLoad.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.id).toBe( 'alice:2026-04-03T00:01:00.000Z' ); await act(async () => { oldLaneLoad.resolve(response('alice', '2026-04-03T00:00:00.000Z')); await Promise.resolve(); }); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.id).toBe( 'alice:2026-04-03T00:01:00.000Z' ); act(() => { root.unmount(); }); }); it('does not reload when only a non-visible member lane changes', async () => { apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); act(() => { root.unmount(); }); }); it('falls back to normalized lane ids when an exact member key is blank', async () => { apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('Alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['Alice'], expect.objectContaining({ laneIdsByMember: { Alice: 'secondary:opencode:alice', alice: 'secondary:opencode:alice', }, }) ); act(() => { root.unmount(); }); }); it('preserves a pending forced reload when lane metadata rerenders before debounce fires', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) | null = null; apiMock.teams.onTeamChange.mockImplementation((callback) => { teamChangeListener = callback as typeof teamChangeListener; return () => undefined; }); apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'tool-activity' }); root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true, laneIdsByMember: { alice: 'secondary:opencode:alice:new' }, }) ); act(() => { root.unmount(); }); }); it('force refreshes visible previews after returning from a hidden document', async () => { apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'hidden', }); await act(async () => { document.dispatchEvent(new Event('visibilitychange')); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'visible', }); await act(async () => { document.dispatchEvent(new Event('visibilitychange')); vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); act(() => { root.unmount(); }); }); it('marks empty cached previews as loading while a forced event refresh is pending', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) | null = null; const refreshLoad = createDeferred(); apiMock.teams.onTeamChange.mockImplementation((callback) => { teamChangeListener = callback as typeof teamChangeListener; return () => undefined; }); apiMock.memberLogStream.getMemberLogPreviews .mockResolvedValueOnce(emptyResponse('alice', '2026-04-03T00:00:00.000Z')) .mockReturnValueOnce(refreshLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items).toHaveLength(0); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'tool-activity' }); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); await act(async () => { refreshLoad.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); act(() => { root.unmount(); }); }); it('keeps loading when an empty visible response arrives before a pending forced refresh starts', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) | null = null; const initialLoad = createDeferred(); const refreshLoad = createDeferred(); apiMock.teams.onTeamChange.mockImplementation((callback) => { teamChangeListener = callback as typeof teamChangeListener; return () => undefined; }); apiMock.memberLogStream.getMemberLogPreviews .mockReturnValueOnce(initialLoad.promise) .mockReturnValueOnce(refreshLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(latestState()?.loading).toBe(true); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'tool-activity' }); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); await act(async () => { initialLoad.resolve(emptyResponse('alice', '2026-04-03T00:00:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(latestState()?.previewsByMember.get('alice')?.items).toHaveLength(0); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); await act(async () => { refreshLoad.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); act(() => { root.unmount(); }); }); it('marks empty cached previews as loading during a direct forced reload', async () => { const refreshLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews .mockResolvedValueOnce(emptyResponse('alice', '2026-04-03T00:00:00.000Z')) .mockReturnValueOnce(refreshLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); let reload: ReturnType['reload'] | null = null; await act(async () => { root.render( { reload = nextReload; }} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items).toHaveLength(0); await act(async () => { void reload?.({ background: true, forceRefresh: true }); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); await act(async () => { refreshLoad.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); act(() => { root.unmount(); }); }); it('keeps loading and ignores stale errors while a newer empty-preview refresh is in flight', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) | null = null; const staleRefresh = createDeferred(); const latestRefresh = createDeferred(); apiMock.teams.onTeamChange.mockImplementation((callback) => { teamChangeListener = callback as typeof teamChangeListener; return () => undefined; }); apiMock.memberLogStream.getMemberLogPreviews .mockResolvedValueOnce(emptyResponse('alice', '2026-04-03T00:00:00.000Z')) .mockReturnValueOnce(staleRefresh.promise) .mockReturnValueOnce(latestRefresh.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render( ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items).toHaveLength(0); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'tool-activity' }); vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(latestState()?.loading).toBe(true); await act(async () => { root.render( ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(3); await act(async () => { staleRefresh.reject(new Error('stale lane failed')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(latestState()?.error).toBeNull(); expect(latestState()?.previewsByMember.get('alice')?.items).toHaveLength(0); await act(async () => { latestRefresh.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.error).toBeNull(); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.preview).toBe('alice'); act(() => { root.unmount(); }); }); it('ignores hidden member request loading and errors after the visible member changes', async () => { const hiddenAliceLoad = createDeferred(); const visibleBobLoad = createDeferred(); apiMock.memberLogStream.getMemberLogPreviews .mockReturnValueOnce(hiddenAliceLoad.promise) .mockReturnValueOnce(visibleBobLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); expect(latestState()?.loading).toBe(true); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { hiddenAliceLoad.reject(new Error('hidden alice failed before bob starts')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(latestState()?.error).toBeNull(); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); await act(async () => { visibleBobLoad.resolve(emptyResponse('bob', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.error).toBeNull(); expect(latestState()?.previewsByMember.get('bob')?.items).toHaveLength(0); act(() => { root.unmount(); }); }); it('ignores old same-key responses after switching away from and back to a team', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) | null = null; const oldAlphaLoad = createDeferred(); const currentAlphaLoad = createDeferred(); apiMock.teams.onTeamChange.mockImplementation((callback) => { teamChangeListener = callback as typeof teamChangeListener; return () => undefined; }); apiMock.memberLogStream.getMemberLogPreviews .mockReturnValueOnce(oldAlphaLoad.promise) .mockReturnValueOnce(currentAlphaLoad.promise); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const states: ReturnType[] = []; const onState = vi.fn((state: ReturnType) => { states.push(state); }); const latestState = (): ReturnType | undefined => states.at(-1); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { root.render(); await Promise.resolve(); }); expect(latestState()?.previewsByMember.size).toBe(0); await act(async () => { root.render(); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); await act(async () => { oldAlphaLoad.resolve(response('alice', '2026-04-03T00:00:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(true); expect(latestState()?.previewsByMember.get('alice')).toBeUndefined(); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'tool-activity' }); vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); await act(async () => { currentAlphaLoad.resolve(response('alice', '2026-04-03T00:01:00.000Z')); await Promise.resolve(); }); expect(latestState()?.loading).toBe(false); expect(latestState()?.previewsByMember.get('alice')?.items[0]?.id).toBe( 'alice:2026-04-03T00:01:00.000Z' ); act(() => { root.unmount(); }); }); it('reloads visible members on log change events with force refresh', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) | null = null; apiMock.teams.onTeamChange.mockImplementation((callback) => { teamChangeListener = callback as typeof teamChangeListener; return () => undefined; }); apiMock.memberLogStream.getMemberLogPreviews.mockResolvedValue( response('alice', '2026-04-03T00:00:00.000Z') ); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( undefined} /> ); await Promise.resolve(); }); await act(async () => { vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'log-source-change' }); vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(2); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'tool-activity' }); vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(3); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); await act(async () => { teamChangeListener?.(null, { teamName: 'alpha-team', type: 'task-log-change' }); vi.advanceTimersByTime(700); await Promise.resolve(); }); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(4); expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( 'alpha-team', ['alice'], expect.objectContaining({ forceRefresh: true }) ); act(() => { root.unmount(); }); }); });