chat-sidebar-background-task-section.test.tsx
1 // @vitest-environment jsdom 2 import { cleanup, fireEvent, render, screen } from '@testing-library/react' 3 import { afterEach, describe, expect, it, vi } from 'vitest' 4 5 import { 6 BackgroundTaskSection, 7 groupBackgroundTasks, 8 } from '@/features/chat/components/chat-page/chat-sidebar/tasks-card' 9 import type { TaskSummary } from '@/lib/shared/chat' 10 11 function buildTask(partial: Partial<TaskSummary>): TaskSummary { 12 return { 13 id: partial.id ?? 'task-1', 14 sessionId: partial.sessionId ?? 'session-1', 15 runId: partial.runId ?? null, 16 backendId: partial.backendId ?? 'codex-cli', 17 title: partial.title ?? 'Task', 18 kind: partial.kind ?? 'agent_run', 19 status: partial.status ?? 'running', 20 progressPercent: partial.progressPercent ?? null, 21 progressLabel: partial.progressLabel ?? null, 22 attachedMessageId: partial.attachedMessageId ?? null, 23 startedAt: partial.startedAt ?? '2026-03-03T13:00:00.000Z', 24 updatedAt: partial.updatedAt ?? '2026-03-03T13:00:00.000Z', 25 completedAt: partial.completedAt ?? null, 26 error: partial.error ?? null, 27 source: partial.source ?? 'chat-stream', 28 } 29 } 30 31 describe('background task sidebar helpers', () => { 32 afterEach(() => { 33 cleanup() 34 }) 35 36 it('groups active, waiting, and failed tasks with failure limit', () => { 37 const tasks: TaskSummary[] = [ 38 buildTask({ id: 'running', status: 'running' }), 39 buildTask({ id: 'queued', status: 'queued' }), 40 buildTask({ id: 'waiting', status: 'waiting_input' }), 41 ...Array.from({ length: 8 }, (_, index) => 42 buildTask({ 43 id: `failed-${index}`, 44 status: 'failed', 45 updatedAt: `2026-03-03T13:${String(index).padStart(2, '0')}:00.000Z`, 46 }), 47 ), 48 ] 49 50 const grouped = groupBackgroundTasks(tasks) 51 expect(grouped.active.map((task) => task.id).sort()).toEqual([ 52 'queued', 53 'running', 54 ]) 55 expect(grouped.waiting.map((task) => task.id)).toEqual(['waiting']) 56 expect(grouped.failures).toHaveLength(6) 57 }) 58 59 it('renders section rows and fires detail/cancel actions', () => { 60 const onToggleDetails = vi.fn() 61 const onCancel = vi.fn(async (_taskId: string) => {}) 62 render( 63 <BackgroundTaskSection 64 title="Active" 65 icon={<span>i</span>} 66 rows={[ 67 buildTask({ 68 id: 'task-running', 69 title: 'Running task', 70 status: 'running', 71 progressLabel: 'Working...', 72 }), 73 buildTask({ 74 id: 'task-failed', 75 title: 'Failed task', 76 status: 'failed', 77 error: 'Boom', 78 }), 79 ]} 80 expandedTaskId="task-running" 81 eventsByTaskId={{ 82 'task-running': [ 83 { 84 id: 'event-1', 85 taskId: 'task-running', 86 at: '2026-03-03T13:01:00.000Z', 87 level: 'info', 88 type: 'tool.complete', 89 text: 'Tool completed.', 90 payload: null, 91 }, 92 ], 93 }} 94 loadingTaskId={null} 95 cancelTaskId={null} 96 eventsError={null} 97 onToggleDetails={onToggleDetails} 98 onCancel={onCancel} 99 />, 100 ) 101 102 expect(screen.getByText('Running task')).toBeTruthy() 103 expect(screen.getByText('Failed task')).toBeTruthy() 104 expect(screen.getByText('Tool completed.')).toBeTruthy() 105 106 const cancelButtons = screen.getAllByRole('button', { name: 'Cancel' }) 107 expect(cancelButtons).toHaveLength(1) 108 fireEvent.click(cancelButtons[0]) 109 expect(onCancel).toHaveBeenCalledWith('task-running') 110 111 const hideButtons = screen.getAllByRole('button', { name: 'Hide' }) 112 fireEvent.click(hideButtons[0]) 113 expect(onToggleDetails).toHaveBeenCalledWith('task-running') 114 }) 115 })