import { renderHook, act } from '@testing-library/react'; import { useQueryClient } from '@tanstack/react-query'; import { useHealthCheck } from '../connection'; import { QueryKeys, Time, dataService } from 'librechat-data-provider'; // Mock dependencies jest.mock('@tanstack/react-query'); jest.mock('librechat-data-provider', () => ({ QueryKeys: { health: 'health' }, Time: { TEN_MINUTES: 600000, FIVE_MINUTES: 300000 }, dataService: { healthCheck: jest.fn() }, })); jest.mock('~/utils', () => ({ logger: { log: jest.fn() }, })); // Mock timers jest.useFakeTimers(); const mockQueryClient = { fetchQuery: jest.fn(), getQueryState: jest.fn(), getQueryData: jest.fn(), invalidateQueries: jest.fn(), } as any; const mockUseQueryClient = useQueryClient as jest.MockedFunction; describe('useHealthCheck', () => { let addEventListenerSpy: jest.SpyInstance; let removeEventListenerSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); mockUseQueryClient.mockReturnValue(mockQueryClient); addEventListenerSpy = jest.spyOn(window, 'addEventListener'); removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); mockQueryClient.fetchQuery.mockResolvedValue({}); mockQueryClient.getQueryState.mockReturnValue(null); }); afterEach(() => { addEventListenerSpy.mockRestore(); removeEventListenerSpy.mockRestore(); }); describe('when not authenticated', () => { it('should not start health check', () => { renderHook(() => useHealthCheck(false)); // Fast-forward past the delay act(() => { jest.advanceTimersByTime(1000); }); expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); expect(addEventListenerSpy).not.toHaveBeenCalled(); }); }); describe('when authenticated', () => { it('should start health check after delay', async () => { renderHook(() => useHealthCheck(true)); // Should not run immediately expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); // Should run after 500ms delay await act(async () => { jest.advanceTimersByTime(500); }); expect(mockQueryClient.fetchQuery).toHaveBeenCalledWith( [QueryKeys.health], expect.any(Function), { retry: false, cacheTime: 0, staleTime: 0, }, ); }); it('should set up 10-minute interval', async () => { renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); // Initial delay }); // Clear the initial call mockQueryClient.fetchQuery.mockClear(); // Advance by 10 minutes await act(async () => { jest.advanceTimersByTime(Time.TEN_MINUTES); }); expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(1); }); it('should run health check continuously every 10 minutes', async () => { renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); // Initial delay }); // Clear the initial call mockQueryClient.fetchQuery.mockClear(); // Test multiple intervals to ensure it keeps running for (let i = 1; i <= 5; i++) { await act(async () => { jest.advanceTimersByTime(Time.TEN_MINUTES); }); expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(i); } // Verify it's been called 5 times total (once per interval) expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(5); // Test that it continues after longer periods await act(async () => { jest.advanceTimersByTime(Time.TEN_MINUTES * 3); // Advance 30 more minutes }); // Should have been called 3 more times (total of 8) expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(8); }); it('should add window focus event listener', async () => { renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); expect(addEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); }); it('should handle window focus correctly when no previous check', async () => { renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); // Get the focus handler const focusHandler = addEventListenerSpy.mock.calls[0][1]; // Mock no query state (no previous check) mockQueryClient.getQueryState.mockReturnValue(null); mockQueryClient.fetchQuery.mockClear(); // Trigger focus event await act(async () => { focusHandler(); }); expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(1); }); it('should handle window focus correctly when check is recent', async () => { renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); // Get the focus handler const focusHandler = addEventListenerSpy.mock.calls[0][1]; // Mock recent query state (within 10 minutes) mockQueryClient.getQueryState.mockReturnValue({ dataUpdatedAt: Date.now() - 300000, // 5 minutes ago }); mockQueryClient.fetchQuery.mockClear(); // Trigger focus event await act(async () => { focusHandler(); }); expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); }); it('should handle window focus correctly when check is old', async () => { renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); // Get the focus handler const focusHandler = addEventListenerSpy.mock.calls[0][1]; // Mock old query state (older than 10 minutes) mockQueryClient.getQueryState.mockReturnValue({ dataUpdatedAt: Date.now() - 700000, // 11+ minutes ago }); mockQueryClient.fetchQuery.mockClear(); // Trigger focus event await act(async () => { focusHandler(); }); expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(1); }); it('should prevent multiple initializations', async () => { const { rerender } = renderHook(({ auth }) => useHealthCheck(auth), { initialProps: { auth: true }, }); await act(async () => { jest.advanceTimersByTime(500); }); const initialCallCount = addEventListenerSpy.mock.calls.length; // Re-render with same auth state rerender({ auth: true }); await act(async () => { jest.advanceTimersByTime(500); }); // Should not add more event listeners expect(addEventListenerSpy).toHaveBeenCalledTimes(initialCallCount); }); it('should handle API errors gracefully', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); mockQueryClient.fetchQuery.mockRejectedValue(new Error('API Error')); renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); expect(consoleSpy).toHaveBeenCalledWith('Health check failed:', expect.any(Error)); consoleSpy.mockRestore(); }); }); describe('cleanup', () => { it('should clear intervals on unmount', async () => { const { unmount } = renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); unmount(); expect(clearIntervalSpy).toHaveBeenCalled(); clearIntervalSpy.mockRestore(); }); it('should remove event listeners on unmount', async () => { const { unmount } = renderHook(() => useHealthCheck(true)); await act(async () => { jest.advanceTimersByTime(500); }); unmount(); expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); }); it('should clear timeout on unmount before initialization', () => { const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const { unmount } = renderHook(() => useHealthCheck(true)); // Unmount before delay completes unmount(); expect(clearTimeoutSpy).toHaveBeenCalled(); clearTimeoutSpy.mockRestore(); }); }); describe('authentication state changes', () => { it('should start health check when authentication becomes true', async () => { const { rerender } = renderHook(({ auth }) => useHealthCheck(auth), { initialProps: { auth: false }, }); // Should not start when false act(() => { jest.advanceTimersByTime(1000); }); expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); // Should start when becomes true rerender({ auth: true }); await act(async () => { jest.advanceTimersByTime(500); }); expect(mockQueryClient.fetchQuery).toHaveBeenCalled(); }); }); });