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>
245 lines
8.0 KiB
TypeScript
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 })
|
|
)
|
|
})
|
|
})
|
|
})
|