Files
Codewalkers/apps/web/src/components/radar/__tests__/InterAgentMessagesDialog.test.tsx
Lukas May 7c48c70d47 feat: Add SSE-driven real-time refresh and last-refreshed timestamp to drilldown dialogs
Add isAgentRunning prop to all four radar drilldown dialog components.
When true, subscribe to relevant SSE events and trigger refetch on
matching events for the current agentId. Show a "Last refreshed: just now"
timestamp that ticks to "Xs ago" in the dialog footer. Reset on close.

- CompactionEventsDialog, SubagentSpawnsDialog, QuestionsAskedDialog:
  subscribe to agent:waiting events
- InterAgentMessagesDialog: subscribe to conversation:created and
  conversation:answered events (matches on fromAgentId)
- Update DrilldownDialogProps type with isAgentRunning?: boolean
- Add test coverage for all new behavior across all four dialogs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:18:17 +01:00

245 lines
8.0 KiB
TypeScript

// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
import { vi, describe, it, expect, beforeEach } from 'vitest'
const mockUseSubscriptionWithErrorHandling = vi.hoisted(() => vi.fn())
vi.mock('@/hooks', () => ({
useSubscriptionWithErrorHandling: mockUseSubscriptionWithErrorHandling,
}))
let mockUseQueryReturn: { data: unknown; isLoading: boolean; refetch?: () => Promise<unknown> } = {
data: undefined,
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
vi.mock('@/lib/trpc', () => ({
trpc: {
conversation: {
getByFromAgent: {
useQuery: vi.fn((_args: unknown, _opts: unknown) => mockUseQueryReturn),
},
},
onEvent: {
useSubscription: vi.fn(),
},
},
}))
import { InterAgentMessagesDialog } from '../InterAgentMessagesDialog'
const defaultProps = {
open: true,
onOpenChange: vi.fn(),
agentId: 'agent-123',
agentName: 'test-agent',
}
describe('InterAgentMessagesDialog', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseQueryReturn = {
data: undefined,
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
mockUseSubscriptionWithErrorHandling.mockReturnValue({
isConnected: false,
isConnecting: false,
error: null,
reconnectAttempts: 0,
lastEventId: null,
reconnect: vi.fn(),
reset: vi.fn(),
})
})
it('does not render dialog content when open=false', () => {
render(<InterAgentMessagesDialog {...defaultProps} open={false} />)
expect(screen.queryByText(/Inter-Agent Messages/)).toBeNull()
})
it('shows skeleton rows when loading', () => {
mockUseQueryReturn = { data: undefined, isLoading: true, refetch: vi.fn().mockResolvedValue({}) }
render(<InterAgentMessagesDialog {...defaultProps} />)
const skeletons = document.querySelectorAll('.animate-pulse')
expect(skeletons.length).toBeGreaterThanOrEqual(3)
expect(screen.queryByRole('table')).toBeNull()
})
it('shows "No data found" when data is empty', () => {
mockUseQueryReturn = { data: [], isLoading: false, refetch: vi.fn().mockResolvedValue({}) }
render(<InterAgentMessagesDialog {...defaultProps} />)
expect(screen.getByText('No data found')).toBeInTheDocument()
})
it('renders data rows for answered conversation', () => {
mockUseQueryReturn = {
data: [
{
id: 'c1',
timestamp: '2026-03-06T10:00:00.000Z',
toAgentName: 'target-agent',
toAgentId: 'agent-2',
question: 'What is the export path?',
answer: 'It is src/api/index.ts',
status: 'answered',
taskId: null,
phaseId: null,
},
],
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
render(<InterAgentMessagesDialog {...defaultProps} />)
expect(screen.getByText('target-agent')).toBeInTheDocument()
expect(screen.getByText('answered')).toBeInTheDocument()
expect(screen.queryByText('What is the export path?')).toBeNull()
})
it('expands answered row to show question and answer', () => {
mockUseQueryReturn = {
data: [
{
id: 'c1',
timestamp: '2026-03-06T10:00:00.000Z',
toAgentName: 'target-agent',
toAgentId: 'agent-2',
question: 'What is the export path?',
answer: 'It is src/api/index.ts',
status: 'answered',
taskId: null,
phaseId: null,
},
],
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
render(<InterAgentMessagesDialog {...defaultProps} />)
fireEvent.click(screen.getByText('target-agent').closest('tr')!)
expect(screen.getByText('What is the export path?')).toBeInTheDocument()
expect(screen.getByText('It is src/api/index.ts')).toBeInTheDocument()
expect(screen.queryByText('No answer yet')).toBeNull()
})
it('expands pending row to show question and "No answer yet"', () => {
mockUseQueryReturn = {
data: [
{
id: 'c2',
timestamp: '2026-03-06T10:00:00.000Z',
toAgentName: 'target-agent',
toAgentId: 'agent-2',
question: 'What is the export path?',
answer: null,
status: 'pending',
taskId: null,
phaseId: null,
},
],
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
render(<InterAgentMessagesDialog {...defaultProps} />)
fireEvent.click(screen.getByText('target-agent').closest('tr')!)
expect(screen.getByText('What is the export path?')).toBeInTheDocument()
expect(screen.getByText('No answer yet')).toBeInTheDocument()
expect(screen.queryByText('It is src/api/index.ts')).toBeNull()
})
it('collapses row when clicked again', () => {
mockUseQueryReturn = {
data: [
{
id: 'c1',
timestamp: '2026-03-06T10:00:00.000Z',
toAgentName: 'target-agent',
toAgentId: 'agent-2',
question: 'What is the export path?',
answer: 'It is src/api/index.ts',
status: 'answered',
taskId: null,
phaseId: null,
},
],
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
render(<InterAgentMessagesDialog {...defaultProps} />)
const row = screen.getByText('target-agent').closest('tr')!
fireEvent.click(row)
expect(screen.getByText('What is the export path?')).toBeInTheDocument()
fireEvent.click(row)
expect(screen.queryByText('What is the export path?')).toBeNull()
})
it('shows 200-instance note when data length is 200', () => {
mockUseQueryReturn = {
data: Array(200).fill({
id: 'c1',
timestamp: '2026-03-06T10:00:00.000Z',
toAgentName: 'target-agent',
toAgentId: 'agent-2',
question: 'What is the export path?',
answer: null,
status: 'pending',
taskId: null,
phaseId: null,
}),
isLoading: false,
refetch: vi.fn().mockResolvedValue({}),
}
render(<InterAgentMessagesDialog {...defaultProps} />)
expect(screen.getByText('Showing first 200 instances.')).toBeInTheDocument()
})
describe('isAgentRunning behavior', () => {
it('shows "Last refreshed: just now" after SSE-triggered refetch when isAgentRunning=true', async () => {
let capturedOnData: ((event: any) => void) | undefined
mockUseSubscriptionWithErrorHandling.mockImplementation((_getter: any, opts: any) => {
capturedOnData = opts.onData
return { isConnected: true, isConnecting: false, error: null, reconnectAttempts: 0, lastEventId: null, reconnect: vi.fn(), reset: vi.fn() }
})
const mockRefetch = vi.fn().mockResolvedValue({ data: [] })
mockUseQueryReturn = {
data: [],
isLoading: false,
refetch: mockRefetch,
}
render(<InterAgentMessagesDialog {...defaultProps} agentId="agent-1" isAgentRunning={true} />)
await act(async () => {
capturedOnData?.({ data: { type: 'conversation:created', fromAgentId: 'agent-1' } })
})
await waitFor(() => {
expect(screen.getByText(/Last refreshed: just now/)).toBeInTheDocument()
})
})
it('does not show "Last refreshed" when isAgentRunning=false', () => {
render(<InterAgentMessagesDialog {...defaultProps} isAgentRunning={false} />)
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
})
it('does not show "Last refreshed" when isAgentRunning is not provided', () => {
render(<InterAgentMessagesDialog {...defaultProps} />)
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
})
it('subscription is enabled only when open=true and isAgentRunning=true', () => {
render(<InterAgentMessagesDialog open={false} onOpenChange={vi.fn()} agentId="agent-1" agentName="test-agent" isAgentRunning={true} />)
expect(mockUseSubscriptionWithErrorHandling).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ enabled: false })
)
})
})
})