/ tests / chat-sidebar-background-task-section.test.tsx
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  })