/** * Tests for useRealtimeSync — event → query invalidation mapping. * * Uses .tsx extension so Vite/OXC can parse JSX in the wrapper component. */ import { renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import * as useSSEModule from './useSSE' import { useRealtimeSync } from './useRealtimeSync' import React from 'react' import type { SSEMessage } from '../services/sse' describe('useRealtimeSync', () => { let queryClient: QueryClient let mockSSEOnMessage: ((msg: { type: string; data: unknown }) => void) | null = null beforeEach(() => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) mockSSEOnMessage = null // Spy on useSSE to capture the onMessage callback vi.spyOn(useSSEModule, 'useSSE').mockImplementation((opts) => { mockSSEOnMessage = opts?.onMessage ?? null return { status: 'connected' } }) }) afterEach(() => { vi.restoreAllMocks() }) function render() { return renderHook(() => useRealtimeSync(), { wrapper: ({ children }: { children: React.ReactNode }) => ( React.createElement(QueryClientProvider, { client: queryClient }, children) ), }) } it('invalidates ["agents"] on agent.status event', async () => { const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries') render() const msg: SSEMessage = { type: 'agent.status', data: { agentId: 'a1', status: 'active' }, } mockSSEOnMessage!(msg) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['agents'] }) expect(invalidateSpy).toHaveBeenCalledTimes(1) }) it('invalidates ["tasks"] and ["agents"] on agent.task event', async () => { const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries') render() const msg: SSEMessage = { type: 'agent.task', data: { agentId: 'a1', taskId: 't1', title: 'Test', action: 'assigned' }, } mockSSEOnMessage!(msg) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tasks'] }) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['agents'] }) expect(invalidateSpy).toHaveBeenCalledTimes(2) }) it('invalidates ["tasks"] and ["agents"] on agent.progress event', async () => { const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries') render() const msg: SSEMessage = { type: 'agent.progress', data: { agentId: 'a1', taskId: 't1', progress: 50, message: 'working' }, } mockSSEOnMessage!(msg) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tasks'] }) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['agents'] }) expect(invalidateSpy).toHaveBeenCalledTimes(2) }) it('invalidates ["agents"], ["sessions"], ["tasks"] on fleet.update event', async () => { const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries') render() const msg: SSEMessage = { type: 'fleet.update', data: { timestamp: '2026-05-20T12:00:00Z', agentCount: 5 }, } mockSSEOnMessage!(msg) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['agents'] }) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['sessions'] }) expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tasks'] }) expect(invalidateSpy).toHaveBeenCalledTimes(3) }) it('does nothing on connected event', async () => { const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries') render() const msg: SSEMessage = { type: 'connected', data: { clientCount: 1 }, } mockSSEOnMessage!(msg) expect(invalidateSpy).not.toHaveBeenCalled() }) it('does nothing on unknown event types', async () => { const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries') render() mockSSEOnMessage!({ type: 'unknown.event', data: {} }) expect(invalidateSpy).not.toHaveBeenCalled() }) it('returns sseStatus from useSSE', () => { const { result } = render() expect(result.current.sseStatus).toBe('connected') }) })