fix: break Vite refresh loop by ignoring __tests__ in route generation

TanStack Router plugin was picking up test files under routes/__tests__/,
causing routeTree.gen.ts to regenerate in a loop. Added routeFileIgnorePattern
and removed stale radar.test.tsx.
This commit is contained in:
Lukas May
2026-03-06 20:34:35 +01:00
parent f63b1c5eec
commit a41caa633b
3 changed files with 28 additions and 452 deletions

View File

@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as RadarRouteImport } from './routes/radar'
import { Route as InboxRouteImport } from './routes/inbox'
import { Route as HqRouteImport } from './routes/hq'
import { Route as AgentsRouteImport } from './routes/agents'
@@ -25,6 +26,11 @@ const SettingsRoute = SettingsRouteImport.update({
path: '/settings',
getParentRoute: () => rootRouteImport,
} as any)
const RadarRoute = RadarRouteImport.update({
id: '/radar',
path: '/radar',
getParentRoute: () => rootRouteImport,
} as any)
const InboxRoute = InboxRouteImport.update({
id: '/inbox',
path: '/inbox',
@@ -76,6 +82,7 @@ export interface FileRoutesByFullPath {
'/agents': typeof AgentsRoute
'/hq': typeof HqRoute
'/inbox': typeof InboxRoute
'/radar': typeof RadarRoute
'/settings': typeof SettingsRouteWithChildren
'/initiatives/$id': typeof InitiativesIdRoute
'/settings/health': typeof SettingsHealthRoute
@@ -88,6 +95,7 @@ export interface FileRoutesByTo {
'/agents': typeof AgentsRoute
'/hq': typeof HqRoute
'/inbox': typeof InboxRoute
'/radar': typeof RadarRoute
'/initiatives/$id': typeof InitiativesIdRoute
'/settings/health': typeof SettingsHealthRoute
'/settings/projects': typeof SettingsProjectsRoute
@@ -100,6 +108,7 @@ export interface FileRoutesById {
'/agents': typeof AgentsRoute
'/hq': typeof HqRoute
'/inbox': typeof InboxRoute
'/radar': typeof RadarRoute
'/settings': typeof SettingsRouteWithChildren
'/initiatives/$id': typeof InitiativesIdRoute
'/settings/health': typeof SettingsHealthRoute
@@ -114,6 +123,7 @@ export interface FileRouteTypes {
| '/agents'
| '/hq'
| '/inbox'
| '/radar'
| '/settings'
| '/initiatives/$id'
| '/settings/health'
@@ -126,6 +136,7 @@ export interface FileRouteTypes {
| '/agents'
| '/hq'
| '/inbox'
| '/radar'
| '/initiatives/$id'
| '/settings/health'
| '/settings/projects'
@@ -137,6 +148,7 @@ export interface FileRouteTypes {
| '/agents'
| '/hq'
| '/inbox'
| '/radar'
| '/settings'
| '/initiatives/$id'
| '/settings/health'
@@ -150,6 +162,7 @@ export interface RootRouteChildren {
AgentsRoute: typeof AgentsRoute
HqRoute: typeof HqRoute
InboxRoute: typeof InboxRoute
RadarRoute: typeof RadarRoute
SettingsRoute: typeof SettingsRouteWithChildren
InitiativesIdRoute: typeof InitiativesIdRoute
InitiativesIndexRoute: typeof InitiativesIndexRoute
@@ -164,6 +177,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/radar': {
id: '/radar'
path: '/radar'
fullPath: '/radar'
preLoaderRoute: typeof RadarRouteImport
parentRoute: typeof rootRouteImport
}
'/inbox': {
id: '/inbox'
path: '/inbox'
@@ -251,6 +271,7 @@ const rootRouteChildren: RootRouteChildren = {
AgentsRoute: AgentsRoute,
HqRoute: HqRoute,
InboxRoute: InboxRoute,
RadarRoute: RadarRoute,
SettingsRoute: SettingsRouteWithChildren,
InitiativesIdRoute: InitiativesIdRoute,
InitiativesIndexRoute: InitiativesIndexRoute,

View File

@@ -1,451 +0,0 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { render, screen, fireEvent, within } from '@testing-library/react'
import { vi, describe, it, expect, beforeEach } from 'vitest'
vi.mock('@/components/radar/CompactionEventsDialog', () => ({
CompactionEventsDialog: ({ open, agentId, agentName, isAgentRunning, onOpenChange }: any) =>
open ? (
<div data-testid="compaction-dialog" data-agent-id={agentId} data-agent-name={agentName} data-is-running={String(isAgentRunning)}>
<button onClick={() => onOpenChange(false)}>close</button>
</div>
) : null,
}))
vi.mock('@/components/radar/SubagentSpawnsDialog', () => ({
SubagentSpawnsDialog: ({ open, agentId, agentName, isAgentRunning, onOpenChange }: any) =>
open ? (
<div data-testid="subagents-dialog" data-agent-id={agentId} data-agent-name={agentName} data-is-running={String(isAgentRunning)}>
<button onClick={() => onOpenChange(false)}>close</button>
</div>
) : null,
}))
vi.mock('@/components/radar/QuestionsAskedDialog', () => ({
QuestionsAskedDialog: ({ open, agentId, agentName, isAgentRunning, onOpenChange }: any) =>
open ? (
<div data-testid="questions-dialog" data-agent-id={agentId} data-agent-name={agentName} data-is-running={String(isAgentRunning)}>
<button onClick={() => onOpenChange(false)}>close</button>
</div>
) : null,
}))
vi.mock('@/components/radar/InterAgentMessagesDialog', () => ({
InterAgentMessagesDialog: ({ open, agentId, agentName, isAgentRunning, onOpenChange }: any) =>
open ? (
<div data-testid="messages-dialog" data-agent-id={agentId} data-agent-name={agentName} data-is-running={String(isAgentRunning)}>
<button onClick={() => onOpenChange(false)}>close</button>
</div>
) : null,
}))
type AgentRadarRow = {
id: string
name: string
mode: string
status: string
initiativeId: string | null
initiativeName: string | null
taskId: string | null
taskName: string | null
createdAt: string
questionsCount: number
messagesCount: number
subagentsCount: number
compactionsCount: number
}
// --- Hoisted mocks ---
const mockListForRadarUseQuery = vi.hoisted(() => vi.fn())
const mockListInitiativesUseQuery = vi.hoisted(() => vi.fn())
const mockListAgentsUseQuery = vi.hoisted(() => vi.fn())
const mockNavigate = vi.hoisted(() => vi.fn())
const mockUseSearch = vi.hoisted(() =>
vi.fn().mockReturnValue({
timeRange: '24h',
status: 'all',
initiativeId: undefined,
mode: 'all',
})
)
vi.mock('@/lib/trpc', () => ({
trpc: {
agent: {
listForRadar: { useQuery: mockListForRadarUseQuery },
},
listInitiatives: { useQuery: mockListInitiativesUseQuery },
listAgents: { useQuery: mockListAgentsUseQuery },
},
}))
vi.mock('@/hooks', () => ({
useLiveUpdates: vi.fn(),
LiveUpdateRule: undefined,
}))
vi.mock('@/components/ThemeToggle', () => ({
ThemeToggle: () => null,
}))
vi.mock('@/components/HealthDot', () => ({
HealthDot: () => null,
}))
vi.mock('@/components/NavBadge', () => ({
NavBadge: () => null,
}))
vi.mock('@tanstack/react-router', () => ({
createFileRoute: () => () => ({ component: null }),
useNavigate: () => mockNavigate,
useSearch: mockUseSearch,
Link: ({
to,
search,
children,
}: {
to: string
search?: Record<string, string>
children: React.ReactNode | ((props: { isActive: boolean }) => React.ReactNode)
}) => {
const params = search ? new URLSearchParams(search).toString() : ''
const href = params ? `${to}?${params}` : to
const content = typeof children === 'function' ? children({ isActive: false }) : children
return <a href={href}>{content}</a>
},
}))
// Import after mocks
import { RadarPage } from '../radar'
import { AppLayout } from '../../layouts/AppLayout'
function makeAgent(overrides?: Partial<AgentRadarRow>): AgentRadarRow {
return {
id: 'agent-1',
name: 'jolly-penguin',
mode: 'execute',
status: 'running',
initiativeId: null,
initiativeName: null,
taskId: null,
taskName: null,
createdAt: new Date(Date.now() - 3600_000).toISOString(),
questionsCount: 0,
messagesCount: 0,
subagentsCount: 0,
compactionsCount: 0,
...overrides,
}
}
describe('RadarPage', () => {
beforeEach(() => {
vi.clearAllMocks()
mockListInitiativesUseQuery.mockReturnValue({ data: [], isLoading: false })
mockUseSearch.mockReturnValue({
timeRange: '24h',
status: 'all',
initiativeId: undefined,
mode: 'all',
})
})
it('renders "Radar" heading', () => {
mockListForRadarUseQuery.mockReturnValue({ data: [], isLoading: false })
render(<RadarPage />)
expect(screen.getByRole('heading', { name: /radar/i })).toBeInTheDocument()
})
it('renders 4 summary stat cards with correct aggregated values', () => {
// Use distinct totals; scope number checks to stat card containers to avoid table collisions
const agents = [
makeAgent({ id: 'a1', questionsCount: 3, messagesCount: 10, subagentsCount: 2, compactionsCount: 1 }),
makeAgent({ id: 'a2', questionsCount: 4, messagesCount: 5, subagentsCount: 1, compactionsCount: 3 }),
]
mockListForRadarUseQuery.mockReturnValue({ data: agents, isLoading: false })
render(<RadarPage />)
// Verify labels exist
expect(screen.getByText('Total Questions Asked')).toBeInTheDocument()
expect(screen.getByText('Total Inter-Agent Messages')).toBeInTheDocument()
expect(screen.getByText('Total Subagent Spawns')).toBeInTheDocument()
expect(screen.getByText('Total Compaction Events')).toBeInTheDocument()
// Verify aggregated totals by scoping to the stat card's container
// Total Questions: 3+4=7
const questionsContainer = screen.getByText('Total Questions Asked').parentElement!
expect(questionsContainer.querySelector('.text-3xl')).toHaveTextContent('7')
// Total Messages: 10+5=15
const messagesContainer = screen.getByText('Total Inter-Agent Messages').parentElement!
expect(messagesContainer.querySelector('.text-3xl')).toHaveTextContent('15')
})
it('table renders one row per agent', () => {
const agents = [
makeAgent({ id: 'a1', name: 'agent-one' }),
makeAgent({ id: 'a2', name: 'agent-two' }),
makeAgent({ id: 'a3', name: 'agent-three' }),
]
mockListForRadarUseQuery.mockReturnValue({ data: agents, isLoading: false })
render(<RadarPage />)
const tbody = document.querySelector('tbody')!
const rows = within(tbody).getAllByRole('row')
expect(rows).toHaveLength(3)
})
it('default sort: newest first', () => {
const older = makeAgent({ id: 'a1', name: 'older-agent', createdAt: new Date(Date.now() - 7200_000).toISOString() })
const newer = makeAgent({ id: 'a2', name: 'newer-agent', createdAt: new Date(Date.now() - 1800_000).toISOString() })
mockListForRadarUseQuery.mockReturnValue({ data: [older, newer], isLoading: false })
render(<RadarPage />)
const tbody = document.querySelector('tbody')!
const rows = within(tbody).getAllByRole('row')
expect(rows[0]).toHaveTextContent('newer-agent')
expect(rows[1]).toHaveTextContent('older-agent')
})
it('clicking "Started" column header sorts ascending, clicking again sorts descending', () => {
const older = makeAgent({ id: 'a1', name: 'older-agent', createdAt: new Date(Date.now() - 7200_000).toISOString() })
const newer = makeAgent({ id: 'a2', name: 'newer-agent', createdAt: new Date(Date.now() - 1800_000).toISOString() })
mockListForRadarUseQuery.mockReturnValue({ data: [older, newer], isLoading: false })
render(<RadarPage />)
// First click: default is desc (newest first), clicking toggles to asc (oldest first)
fireEvent.click(screen.getByRole('columnheader', { name: /started/i }))
const rowsAsc = within(document.querySelector('tbody')!).getAllByRole('row')
expect(rowsAsc[0]).toHaveTextContent('older-agent')
expect(rowsAsc[1]).toHaveTextContent('newer-agent')
// Second click: re-query header (text content changed to '▲'), toggle back to desc
fireEvent.click(screen.getByRole('columnheader', { name: /started/i }))
const rowsDesc = within(document.querySelector('tbody')!).getAllByRole('row')
expect(rowsDesc[0]).toHaveTextContent('newer-agent')
expect(rowsDesc[1]).toHaveTextContent('older-agent')
})
it('agent name cell renders a Link to /agents with selected param', () => {
const agent = makeAgent({ id: 'agent-xyz', name: 'test-agent' })
mockListForRadarUseQuery.mockReturnValue({ data: [agent], isLoading: false })
render(<RadarPage />)
const link = screen.getByRole('link', { name: 'test-agent' })
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute('href', expect.stringContaining('/agents'))
expect(link).toHaveAttribute('href', expect.stringContaining('agent-xyz'))
})
it('non-zero metric cell has cursor-pointer class; zero cell does not', () => {
mockListForRadarUseQuery.mockReturnValue({
data: [makeAgent({ id: 'a1', questionsCount: 5 })],
isLoading: false,
})
render(<RadarPage />)
// Find all cells with text "5" — the non-zero questions cell
const tbody = document.querySelector('tbody')!
const cells = tbody.querySelectorAll('td')
// Find the cell containing "5" (questionsCount)
const nonZeroCell = Array.from(cells).find(cell => cell.textContent === '5')
expect(nonZeroCell).toBeTruthy()
expect(nonZeroCell!.className).toContain('cursor-pointer')
// Find a zero cell (messagesCount=0)
const zeroCell = Array.from(cells).find(cell => cell.textContent === '0' && !cell.className.includes('cursor-pointer'))
expect(zeroCell).toBeTruthy()
})
it('selecting mode filter calls navigate with mode param', () => {
mockListForRadarUseQuery.mockReturnValue({ data: [], isLoading: false })
render(<RadarPage />)
const selects = screen.getAllByRole('combobox')
// Mode select is the 4th select (timeRange, status, initiative, mode)
const modeSelect = selects[3]
fireEvent.change(modeSelect, { target: { value: 'execute' } })
expect(mockNavigate).toHaveBeenCalledWith(
expect.objectContaining({
search: expect.any(Function),
})
)
// Call the search function to verify the result
const call = mockNavigate.mock.calls[0][0]
const result = call.search({ timeRange: '24h', status: 'all', initiativeId: undefined, mode: 'all' })
expect(result).toMatchObject({ mode: 'execute' })
})
it('selecting status filter calls navigate with status param', () => {
mockListForRadarUseQuery.mockReturnValue({ data: [], isLoading: false })
render(<RadarPage />)
const selects = screen.getAllByRole('combobox')
// Status select is the 2nd select
const statusSelect = selects[1]
fireEvent.change(statusSelect, { target: { value: 'running' } })
expect(mockNavigate).toHaveBeenCalledWith(
expect.objectContaining({
search: expect.any(Function),
})
)
const call = mockNavigate.mock.calls[0][0]
const result = call.search({ timeRange: '24h', status: 'all', initiativeId: undefined, mode: 'all' })
expect(result).toMatchObject({ status: 'running' })
})
it('empty state shown when agents returns []', () => {
mockListForRadarUseQuery.mockReturnValue({ data: [], isLoading: false })
render(<RadarPage />)
expect(screen.getByText('No agent activity in this time period')).toBeInTheDocument()
})
it('loading skeleton shown when isLoading is true', () => {
mockListForRadarUseQuery.mockReturnValue({ data: undefined, isLoading: true })
render(<RadarPage />)
const skeletons = document.querySelectorAll('.animate-pulse')
expect(skeletons.length).toBeGreaterThanOrEqual(5)
})
describe('dialog integration', () => {
const alphaAgent = {
id: 'agent-alpha-id',
name: 'agent-alpha',
mode: 'execute',
status: 'running',
initiativeId: null,
initiativeName: null,
taskId: null,
taskName: null,
createdAt: new Date(Date.now() - 3600_000).toISOString(),
questionsCount: 3,
messagesCount: 2,
subagentsCount: 1,
compactionsCount: 4,
}
const betaAgent = {
id: 'agent-beta-id',
name: 'agent-beta',
mode: 'execute',
status: 'stopped',
initiativeId: null,
initiativeName: null,
taskId: null,
taskName: null,
createdAt: new Date(Date.now() - 7200_000).toISOString(),
questionsCount: 0,
messagesCount: 0,
subagentsCount: 0,
compactionsCount: 0,
}
beforeEach(() => {
mockListForRadarUseQuery.mockReturnValue({ data: [alphaAgent, betaAgent], isLoading: false })
})
it('clicking a non-zero Compactions cell opens CompactionEventsDialog with correct agentId and agentName', async () => {
render(<RadarPage />)
const cell = screen.getByTestId(`cell-compactions-agent-alpha-id`)
fireEvent.click(cell)
const dialog = await screen.findByTestId('compaction-dialog')
expect(dialog).toBeInTheDocument()
expect(dialog).toHaveAttribute('data-agent-name', 'agent-alpha')
expect(dialog).toHaveAttribute('data-agent-id', 'agent-alpha-id')
})
it('clicking a non-zero Subagents cell opens SubagentSpawnsDialog', async () => {
render(<RadarPage />)
const cell = screen.getByTestId(`cell-subagents-agent-alpha-id`)
fireEvent.click(cell)
const dialog = await screen.findByTestId('subagents-dialog')
expect(dialog).toBeInTheDocument()
expect(dialog).toHaveAttribute('data-agent-name', 'agent-alpha')
expect(dialog).toHaveAttribute('data-agent-id', 'agent-alpha-id')
})
it('clicking a non-zero Questions cell opens QuestionsAskedDialog', async () => {
render(<RadarPage />)
const cell = screen.getByTestId(`cell-questions-agent-alpha-id`)
fireEvent.click(cell)
const dialog = await screen.findByTestId('questions-dialog')
expect(dialog).toBeInTheDocument()
expect(dialog).toHaveAttribute('data-agent-name', 'agent-alpha')
expect(dialog).toHaveAttribute('data-agent-id', 'agent-alpha-id')
})
it('clicking a non-zero Messages cell opens InterAgentMessagesDialog', async () => {
render(<RadarPage />)
const cell = screen.getByTestId(`cell-messages-agent-alpha-id`)
fireEvent.click(cell)
const dialog = await screen.findByTestId('messages-dialog')
expect(dialog).toBeInTheDocument()
expect(dialog).toHaveAttribute('data-agent-name', 'agent-alpha')
expect(dialog).toHaveAttribute('data-agent-id', 'agent-alpha-id')
})
it('dialog closes when onOpenChange(false) fires', async () => {
render(<RadarPage />)
const cell = screen.getByTestId(`cell-compactions-agent-alpha-id`)
fireEvent.click(cell)
const dialog = await screen.findByTestId('compaction-dialog')
expect(dialog).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'close' }))
expect(screen.queryByTestId('compaction-dialog')).not.toBeInTheDocument()
})
it('zero metric cells do not open any dialog when clicked', () => {
render(<RadarPage />)
// agent-beta has all zeros — click each zero metric cell
fireEvent.click(screen.getByTestId('cell-compactions-agent-beta-id'))
fireEvent.click(screen.getByTestId('cell-subagents-agent-beta-id'))
fireEvent.click(screen.getByTestId('cell-questions-agent-beta-id'))
fireEvent.click(screen.getByTestId('cell-messages-agent-beta-id'))
expect(screen.queryByTestId('compaction-dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('subagents-dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('questions-dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('messages-dialog')).not.toBeInTheDocument()
})
it('passes isAgentRunning=true when the agent status is "running"', async () => {
render(<RadarPage />)
const cell = screen.getByTestId(`cell-compactions-agent-alpha-id`)
fireEvent.click(cell)
const dialog = await screen.findByTestId('compaction-dialog')
expect(dialog).toHaveAttribute('data-is-running', 'true')
})
it('passes isAgentRunning=false when the agent status is "stopped"', async () => {
// Use a stopped agent with non-zero compactions
const stoppedAgent = { ...betaAgent, id: 'stopped-agent-id', name: 'stopped-agent', compactionsCount: 2, status: 'stopped' }
mockListForRadarUseQuery.mockReturnValue({ data: [stoppedAgent], isLoading: false })
render(<RadarPage />)
const cell = screen.getByTestId('cell-compactions-stopped-agent-id')
fireEvent.click(cell)
const dialog = await screen.findByTestId('compaction-dialog')
expect(dialog).toHaveAttribute('data-is-running', 'false')
})
})
})
describe('AppLayout - Radar nav item', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('"Radar" appears in AppLayout nav', () => {
mockListAgentsUseQuery.mockReturnValue({ data: [], isLoading: false })
render(
<AppLayout connectionState="connected">
<div>content</div>
</AppLayout>
)
const radarLink = screen.getByRole('link', { name: 'Radar' })
expect(radarLink).toBeInTheDocument()
expect(radarLink).toHaveAttribute('href', expect.stringContaining('/radar'))
})
})

View File

@@ -4,7 +4,13 @@ import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react()],
plugins: [
TanStackRouterVite({
autoCodeSplitting: true,
routeFileIgnorePattern: '__tests__',
}),
react(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),