Merge branch 'cw/radar-task-JJ3yFeDd1HIKtqYWuF36n' into cw-merge-1772824727227
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -7,8 +8,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useSubscriptionWithErrorHandling } from '@/hooks'
|
||||
import type { DrilldownDialogProps } from './types'
|
||||
|
||||
const RELEVANT_EVENTS = ['agent:waiting']
|
||||
|
||||
function formatTimestamp(ts: string): string {
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
@@ -47,12 +51,48 @@ export function CompactionEventsDialog({
|
||||
onOpenChange,
|
||||
agentId,
|
||||
agentName,
|
||||
isAgentRunning,
|
||||
}: DrilldownDialogProps) {
|
||||
const { data, isLoading } = trpc.agent.getCompactionEvents.useQuery(
|
||||
const { data, isLoading, refetch } = trpc.agent.getCompactionEvents.useQuery(
|
||||
{ agentId },
|
||||
{ enabled: open }
|
||||
)
|
||||
|
||||
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null)
|
||||
const [secondsAgo, setSecondsAgo] = useState(0)
|
||||
|
||||
useSubscriptionWithErrorHandling(
|
||||
() => trpc.onEvent.useSubscription(undefined),
|
||||
{
|
||||
enabled: open && !!isAgentRunning,
|
||||
onData: (event: any) => {
|
||||
const eventType: string = event?.data?.type ?? event?.type ?? ''
|
||||
const eventAgentId: string = event?.data?.agentId ?? event?.agentId ?? ''
|
||||
if (RELEVANT_EVENTS.some(e => eventType.startsWith(e)) && eventAgentId === agentId) {
|
||||
void refetch().then(() => {
|
||||
setLastRefreshedAt(new Date())
|
||||
setSecondsAgo(0)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !isAgentRunning || !lastRefreshedAt) return
|
||||
const interval = setInterval(() => {
|
||||
setSecondsAgo(Math.floor((Date.now() - lastRefreshedAt.getTime()) / 1000))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [open, isAgentRunning, lastRefreshedAt])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setLastRefreshedAt(null)
|
||||
setSecondsAgo(0)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
@@ -105,6 +145,12 @@ export function CompactionEventsDialog({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAgentRunning && lastRefreshedAt && (
|
||||
<p className="mt-2 text-xs text-muted-foreground text-right">
|
||||
Last refreshed: {secondsAgo === 0 ? 'just now' : `${secondsAgo}s ago`}
|
||||
</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -9,8 +9,11 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useSubscriptionWithErrorHandling } from '@/hooks'
|
||||
import type { DrilldownDialogProps } from './types'
|
||||
|
||||
const RELEVANT_EVENTS = ['conversation:created', 'conversation:answered']
|
||||
|
||||
function formatTimestamp(ts: string): string {
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
@@ -49,16 +52,49 @@ export function InterAgentMessagesDialog({
|
||||
onOpenChange,
|
||||
agentId,
|
||||
agentName,
|
||||
isAgentRunning,
|
||||
}: DrilldownDialogProps) {
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null)
|
||||
|
||||
const { data, isLoading } = trpc.conversation.getByFromAgent.useQuery(
|
||||
const { data, isLoading, refetch } = trpc.conversation.getByFromAgent.useQuery(
|
||||
{ agentId },
|
||||
{ enabled: open }
|
||||
)
|
||||
|
||||
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null)
|
||||
const [secondsAgo, setSecondsAgo] = useState(0)
|
||||
|
||||
useSubscriptionWithErrorHandling(
|
||||
() => trpc.onEvent.useSubscription(undefined),
|
||||
{
|
||||
enabled: open && !!isAgentRunning,
|
||||
onData: (event: any) => {
|
||||
const eventType: string = event?.data?.type ?? event?.type ?? ''
|
||||
const eventAgentId: string = event?.data?.fromAgentId ?? event?.data?.agentId ?? event?.agentId ?? ''
|
||||
if (RELEVANT_EVENTS.some(e => eventType.startsWith(e)) && eventAgentId === agentId) {
|
||||
void refetch().then(() => {
|
||||
setLastRefreshedAt(new Date())
|
||||
setSecondsAgo(0)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setExpandedIndex(null)
|
||||
if (!open || !isAgentRunning || !lastRefreshedAt) return
|
||||
const interval = setInterval(() => {
|
||||
setSecondsAgo(Math.floor((Date.now() - lastRefreshedAt.getTime()) / 1000))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [open, isAgentRunning, lastRefreshedAt])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setExpandedIndex(null)
|
||||
setLastRefreshedAt(null)
|
||||
setSecondsAgo(0)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
@@ -161,6 +197,12 @@ export function InterAgentMessagesDialog({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAgentRunning && lastRefreshedAt && (
|
||||
<p className="mt-2 text-xs text-muted-foreground text-right">
|
||||
Last refreshed: {secondsAgo === 0 ? 'just now' : `${secondsAgo}s ago`}
|
||||
</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -8,8 +8,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useSubscriptionWithErrorHandling } from '@/hooks'
|
||||
import type { DrilldownDialogProps } from './types'
|
||||
|
||||
const RELEVANT_EVENTS = ['agent:waiting']
|
||||
|
||||
function formatTimestamp(ts: string): string {
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
@@ -52,16 +55,49 @@ export function QuestionsAskedDialog({
|
||||
onOpenChange,
|
||||
agentId,
|
||||
agentName,
|
||||
isAgentRunning,
|
||||
}: DrilldownDialogProps) {
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null)
|
||||
|
||||
const { data, isLoading } = trpc.agent.getQuestionsAsked.useQuery(
|
||||
const { data, isLoading, refetch } = trpc.agent.getQuestionsAsked.useQuery(
|
||||
{ agentId },
|
||||
{ enabled: open }
|
||||
)
|
||||
|
||||
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null)
|
||||
const [secondsAgo, setSecondsAgo] = useState(0)
|
||||
|
||||
useSubscriptionWithErrorHandling(
|
||||
() => trpc.onEvent.useSubscription(undefined),
|
||||
{
|
||||
enabled: open && !!isAgentRunning,
|
||||
onData: (event: any) => {
|
||||
const eventType: string = event?.data?.type ?? event?.type ?? ''
|
||||
const eventAgentId: string = event?.data?.agentId ?? event?.agentId ?? ''
|
||||
if (RELEVANT_EVENTS.some(e => eventType.startsWith(e)) && eventAgentId === agentId) {
|
||||
void refetch().then(() => {
|
||||
setLastRefreshedAt(new Date())
|
||||
setSecondsAgo(0)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setExpandedIndex(null)
|
||||
if (!open || !isAgentRunning || !lastRefreshedAt) return
|
||||
const interval = setInterval(() => {
|
||||
setSecondsAgo(Math.floor((Date.now() - lastRefreshedAt.getTime()) / 1000))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [open, isAgentRunning, lastRefreshedAt])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setExpandedIndex(null)
|
||||
setLastRefreshedAt(null)
|
||||
setSecondsAgo(0)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
@@ -153,6 +189,12 @@ export function QuestionsAskedDialog({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAgentRunning && lastRefreshedAt && (
|
||||
<p className="mt-2 text-xs text-muted-foreground text-right">
|
||||
Last refreshed: {secondsAgo === 0 ? 'just now' : `${secondsAgo}s ago`}
|
||||
</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -8,8 +8,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useSubscriptionWithErrorHandling } from '@/hooks'
|
||||
import type { DrilldownDialogProps } from './types'
|
||||
|
||||
const RELEVANT_EVENTS = ['agent:waiting']
|
||||
|
||||
function formatTimestamp(ts: string): string {
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
@@ -48,16 +51,49 @@ export function SubagentSpawnsDialog({
|
||||
onOpenChange,
|
||||
agentId,
|
||||
agentName,
|
||||
isAgentRunning,
|
||||
}: DrilldownDialogProps) {
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null)
|
||||
|
||||
const { data, isLoading } = trpc.agent.getSubagentSpawns.useQuery(
|
||||
const { data, isLoading, refetch } = trpc.agent.getSubagentSpawns.useQuery(
|
||||
{ agentId },
|
||||
{ enabled: open }
|
||||
)
|
||||
|
||||
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null)
|
||||
const [secondsAgo, setSecondsAgo] = useState(0)
|
||||
|
||||
useSubscriptionWithErrorHandling(
|
||||
() => trpc.onEvent.useSubscription(undefined),
|
||||
{
|
||||
enabled: open && !!isAgentRunning,
|
||||
onData: (event: any) => {
|
||||
const eventType: string = event?.data?.type ?? event?.type ?? ''
|
||||
const eventAgentId: string = event?.data?.agentId ?? event?.agentId ?? ''
|
||||
if (RELEVANT_EVENTS.some(e => eventType.startsWith(e)) && eventAgentId === agentId) {
|
||||
void refetch().then(() => {
|
||||
setLastRefreshedAt(new Date())
|
||||
setSecondsAgo(0)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setExpandedIndex(null)
|
||||
if (!open || !isAgentRunning || !lastRefreshedAt) return
|
||||
const interval = setInterval(() => {
|
||||
setSecondsAgo(Math.floor((Date.now() - lastRefreshedAt.getTime()) / 1000))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [open, isAgentRunning, lastRefreshedAt])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setExpandedIndex(null)
|
||||
setLastRefreshedAt(null)
|
||||
setSecondsAgo(0)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
@@ -137,6 +173,12 @@ export function SubagentSpawnsDialog({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAgentRunning && lastRefreshedAt && (
|
||||
<p className="mt-2 text-xs text-muted-foreground text-right">
|
||||
Last refreshed: {secondsAgo === 0 ? 'just now' : `${secondsAgo}s ago`}
|
||||
</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { render, screen, act, waitFor } from '@testing-library/react'
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
|
||||
let mockUseQueryReturn: { data: unknown; isLoading: boolean } = {
|
||||
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', () => ({
|
||||
@@ -15,6 +21,9 @@ vi.mock('@/lib/trpc', () => ({
|
||||
useQuery: vi.fn((_args: unknown, _opts: unknown) => mockUseQueryReturn),
|
||||
},
|
||||
},
|
||||
onEvent: {
|
||||
useSubscription: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -30,7 +39,20 @@ const defaultProps = {
|
||||
describe('CompactionEventsDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseQueryReturn = { data: undefined, isLoading: false }
|
||||
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', () => {
|
||||
@@ -39,7 +61,7 @@ describe('CompactionEventsDialog', () => {
|
||||
})
|
||||
|
||||
it('shows skeleton rows when loading', () => {
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true }
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<CompactionEventsDialog {...defaultProps} />)
|
||||
const skeletons = document.querySelectorAll('.animate-pulse')
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(3)
|
||||
@@ -47,7 +69,7 @@ describe('CompactionEventsDialog', () => {
|
||||
})
|
||||
|
||||
it('shows "No data found" when data is empty', () => {
|
||||
mockUseQueryReturn = { data: [], isLoading: false }
|
||||
mockUseQueryReturn = { data: [], isLoading: false, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<CompactionEventsDialog {...defaultProps} />)
|
||||
expect(screen.getByText('No data found')).toBeInTheDocument()
|
||||
})
|
||||
@@ -56,6 +78,7 @@ describe('CompactionEventsDialog', () => {
|
||||
mockUseQueryReturn = {
|
||||
data: [{ timestamp: '2026-03-06T10:00:00.000Z', sessionNumber: 3 }],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<CompactionEventsDialog {...defaultProps} />)
|
||||
expect(screen.getByText('3')).toBeInTheDocument()
|
||||
@@ -68,15 +91,61 @@ describe('CompactionEventsDialog', () => {
|
||||
mockUseQueryReturn = {
|
||||
data: Array(200).fill({ timestamp: '2026-03-06T10:00:00.000Z', sessionNumber: 1 }),
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<CompactionEventsDialog {...defaultProps} />)
|
||||
expect(screen.getByText('Showing first 200 instances.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders dialog title and subtitle', () => {
|
||||
mockUseQueryReturn = { data: [], isLoading: false }
|
||||
mockUseQueryReturn = { data: [], isLoading: false, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<CompactionEventsDialog {...defaultProps} />)
|
||||
expect(screen.getByText(/Compaction Events — test-agent/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/context-window compaction/)).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: [{ timestamp: '2026-03-06T10:00:00.000Z', sessionNumber: 1 }] })
|
||||
mockUseQueryReturn = {
|
||||
data: [{ timestamp: '2026-03-06T10:00:00.000Z', sessionNumber: 1 }],
|
||||
isLoading: false,
|
||||
refetch: mockRefetch,
|
||||
}
|
||||
|
||||
render(<CompactionEventsDialog {...defaultProps} agentId="agent-1" isAgentRunning={true} />)
|
||||
|
||||
await act(async () => {
|
||||
capturedOnData?.({ data: { type: 'agent:waiting', agentId: 'agent-1' } })
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Last refreshed: just now/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show "Last refreshed" when isAgentRunning=false', () => {
|
||||
render(<CompactionEventsDialog {...defaultProps} isAgentRunning={false} />)
|
||||
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show "Last refreshed" when isAgentRunning is not provided', () => {
|
||||
render(<CompactionEventsDialog {...defaultProps} />)
|
||||
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('subscription is enabled only when open=true and isAgentRunning=true', () => {
|
||||
render(<CompactionEventsDialog open={false} onOpenChange={vi.fn()} agentId="agent-1" agentName="test-agent" isAgentRunning={true} />)
|
||||
expect(mockUseSubscriptionWithErrorHandling).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ enabled: false })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
|
||||
let mockUseQueryReturn: { data: unknown; isLoading: boolean } = {
|
||||
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', () => ({
|
||||
@@ -15,6 +21,9 @@ vi.mock('@/lib/trpc', () => ({
|
||||
useQuery: vi.fn((_args: unknown, _opts: unknown) => mockUseQueryReturn),
|
||||
},
|
||||
},
|
||||
onEvent: {
|
||||
useSubscription: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -30,7 +39,20 @@ const defaultProps = {
|
||||
describe('InterAgentMessagesDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseQueryReturn = { data: undefined, isLoading: false }
|
||||
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', () => {
|
||||
@@ -39,7 +61,7 @@ describe('InterAgentMessagesDialog', () => {
|
||||
})
|
||||
|
||||
it('shows skeleton rows when loading', () => {
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true }
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<InterAgentMessagesDialog {...defaultProps} />)
|
||||
const skeletons = document.querySelectorAll('.animate-pulse')
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(3)
|
||||
@@ -47,7 +69,7 @@ describe('InterAgentMessagesDialog', () => {
|
||||
})
|
||||
|
||||
it('shows "No data found" when data is empty', () => {
|
||||
mockUseQueryReturn = { data: [], isLoading: false }
|
||||
mockUseQueryReturn = { data: [], isLoading: false, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<InterAgentMessagesDialog {...defaultProps} />)
|
||||
expect(screen.getByText('No data found')).toBeInTheDocument()
|
||||
})
|
||||
@@ -68,6 +90,7 @@ describe('InterAgentMessagesDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<InterAgentMessagesDialog {...defaultProps} />)
|
||||
expect(screen.getByText('target-agent')).toBeInTheDocument()
|
||||
@@ -91,6 +114,7 @@ describe('InterAgentMessagesDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<InterAgentMessagesDialog {...defaultProps} />)
|
||||
|
||||
@@ -116,6 +140,7 @@ describe('InterAgentMessagesDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<InterAgentMessagesDialog {...defaultProps} />)
|
||||
|
||||
@@ -141,6 +166,7 @@ describe('InterAgentMessagesDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<InterAgentMessagesDialog {...defaultProps} />)
|
||||
|
||||
@@ -165,8 +191,54 @@ describe('InterAgentMessagesDialog', () => {
|
||||
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 })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
|
||||
let mockUseQueryReturn: { data: unknown; isLoading: boolean } = {
|
||||
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', () => ({
|
||||
@@ -15,6 +21,9 @@ vi.mock('@/lib/trpc', () => ({
|
||||
useQuery: vi.fn((_args: unknown, _opts: unknown) => mockUseQueryReturn),
|
||||
},
|
||||
},
|
||||
onEvent: {
|
||||
useSubscription: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -30,7 +39,20 @@ const defaultProps = {
|
||||
describe('QuestionsAskedDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseQueryReturn = { data: undefined, isLoading: false }
|
||||
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', () => {
|
||||
@@ -39,7 +61,7 @@ describe('QuestionsAskedDialog', () => {
|
||||
})
|
||||
|
||||
it('shows skeleton rows when loading', () => {
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true }
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
const skeletons = document.querySelectorAll('.animate-pulse')
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(3)
|
||||
@@ -47,7 +69,7 @@ describe('QuestionsAskedDialog', () => {
|
||||
})
|
||||
|
||||
it('shows "No data found" when data is empty', () => {
|
||||
mockUseQueryReturn = { data: [], isLoading: false }
|
||||
mockUseQueryReturn = { data: [], isLoading: false, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
expect(screen.getByText('No data found')).toBeInTheDocument()
|
||||
})
|
||||
@@ -64,6 +86,7 @@ describe('QuestionsAskedDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
expect(screen.getByText('2 questions')).toBeInTheDocument()
|
||||
@@ -83,6 +106,7 @@ describe('QuestionsAskedDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
|
||||
@@ -104,6 +128,7 @@ describe('QuestionsAskedDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
|
||||
@@ -123,6 +148,7 @@ describe('QuestionsAskedDialog', () => {
|
||||
],
|
||||
}),
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
expect(screen.getByText('Showing first 200 instances.')).toBeInTheDocument()
|
||||
@@ -139,9 +165,55 @@ describe('QuestionsAskedDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
expect(screen.getByText('1 question')).toBeInTheDocument()
|
||||
expect(screen.queryByText('1 questions')).toBeNull()
|
||||
})
|
||||
|
||||
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(<QuestionsAskedDialog {...defaultProps} agentId="agent-1" isAgentRunning={true} />)
|
||||
|
||||
await act(async () => {
|
||||
capturedOnData?.({ data: { type: 'agent:waiting', agentId: 'agent-1' } })
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Last refreshed: just now/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show "Last refreshed" when isAgentRunning=false', () => {
|
||||
render(<QuestionsAskedDialog {...defaultProps} isAgentRunning={false} />)
|
||||
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show "Last refreshed" when isAgentRunning is not provided', () => {
|
||||
render(<QuestionsAskedDialog {...defaultProps} />)
|
||||
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('subscription is enabled only when open=true and isAgentRunning=true', () => {
|
||||
render(<QuestionsAskedDialog open={false} onOpenChange={vi.fn()} agentId="agent-1" agentName="test-agent" isAgentRunning={true} />)
|
||||
expect(mockUseSubscriptionWithErrorHandling).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ enabled: false })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
|
||||
let mockUseQueryReturn: { data: unknown; isLoading: boolean } = {
|
||||
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', () => ({
|
||||
@@ -15,6 +21,9 @@ vi.mock('@/lib/trpc', () => ({
|
||||
useQuery: vi.fn((_args: unknown, _opts: unknown) => mockUseQueryReturn),
|
||||
},
|
||||
},
|
||||
onEvent: {
|
||||
useSubscription: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -30,7 +39,20 @@ const defaultProps = {
|
||||
describe('SubagentSpawnsDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseQueryReturn = { data: undefined, isLoading: false }
|
||||
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', () => {
|
||||
@@ -39,7 +61,7 @@ describe('SubagentSpawnsDialog', () => {
|
||||
})
|
||||
|
||||
it('shows skeleton rows when loading', () => {
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true }
|
||||
mockUseQueryReturn = { data: undefined, isLoading: true, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<SubagentSpawnsDialog {...defaultProps} />)
|
||||
const skeletons = document.querySelectorAll('.animate-pulse')
|
||||
expect(skeletons.length).toBeGreaterThanOrEqual(3)
|
||||
@@ -47,7 +69,7 @@ describe('SubagentSpawnsDialog', () => {
|
||||
})
|
||||
|
||||
it('shows "No data found" when data is empty', () => {
|
||||
mockUseQueryReturn = { data: [], isLoading: false }
|
||||
mockUseQueryReturn = { data: [], isLoading: false, refetch: vi.fn().mockResolvedValue({}) }
|
||||
render(<SubagentSpawnsDialog {...defaultProps} />)
|
||||
expect(screen.getByText('No data found')).toBeInTheDocument()
|
||||
})
|
||||
@@ -63,6 +85,7 @@ describe('SubagentSpawnsDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<SubagentSpawnsDialog {...defaultProps} />)
|
||||
expect(screen.getByText('my task')).toBeInTheDocument()
|
||||
@@ -81,6 +104,7 @@ describe('SubagentSpawnsDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<SubagentSpawnsDialog {...defaultProps} />)
|
||||
|
||||
@@ -106,6 +130,7 @@ describe('SubagentSpawnsDialog', () => {
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<SubagentSpawnsDialog {...defaultProps} />)
|
||||
expect(screen.getByText('…')).toBeInTheDocument()
|
||||
@@ -120,8 +145,54 @@ describe('SubagentSpawnsDialog', () => {
|
||||
fullPrompt: 'full prompt',
|
||||
}),
|
||||
isLoading: false,
|
||||
refetch: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
render(<SubagentSpawnsDialog {...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(<SubagentSpawnsDialog {...defaultProps} agentId="agent-1" isAgentRunning={true} />)
|
||||
|
||||
await act(async () => {
|
||||
capturedOnData?.({ data: { type: 'agent:waiting', agentId: 'agent-1' } })
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Last refreshed: just now/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show "Last refreshed" when isAgentRunning=false', () => {
|
||||
render(<SubagentSpawnsDialog {...defaultProps} isAgentRunning={false} />)
|
||||
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show "Last refreshed" when isAgentRunning is not provided', () => {
|
||||
render(<SubagentSpawnsDialog {...defaultProps} />)
|
||||
expect(screen.queryByText(/Last refreshed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('subscription is enabled only when open=true and isAgentRunning=true', () => {
|
||||
render(<SubagentSpawnsDialog open={false} onOpenChange={vi.fn()} agentId="agent-1" agentName="test-agent" isAgentRunning={true} />)
|
||||
expect(mockUseSubscriptionWithErrorHandling).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ enabled: false })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user