import { gunzipSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import type { PairedTurnAttemptRecord, PairedTurnRecord, WorkItem, } from './db.js'; import type { StatusSnapshot } from './status-dashboard.js'; import type { NewMessage, PairedTask, PairedTurnOutput } from './types.js'; import { handleRoomTimelineRoute, type RoomsTimelineRouteDependencies, } from './web-dashboard-room-routes.js'; function jsonResponse(value: unknown, init?: ResponseInit): Response { return new Response(JSON.stringify(value), { ...init, headers: { 'content-type': 'application/json; charset=utf-8', ...(init?.headers as Record | undefined), }, }); } function makePairedTask(overrides: Partial = {}): PairedTask { return { id: 'paired-room-1', chat_jid: 'dc:ops', group_folder: 'ops-room', owner_service_id: 'codex-main', reviewer_service_id: 'claude-reviewer', owner_agent_type: 'codex', reviewer_agent_type: 'claude-code', arbiter_agent_type: 'codex', title: 'Dashboard PR', source_ref: null, plan_notes: null, review_requested_at: null, round_trip_count: 2, owner_failure_count: 0, owner_step_done_streak: 0, finalize_step_done_count: 0, task_done_then_user_reopen_count: 0, empty_step_done_streak: 0, status: 'in_review', arbiter_verdict: null, arbiter_requested_at: null, completion_reason: null, created_at: '2026-04-26T04:00:00.000Z', updated_at: '2026-04-26T05:20:00.000Z', ...overrides, }; } function snapshot(updatedAt = '2026-04-26T05:22:00.000Z'): StatusSnapshot { return { serviceId: 'codex-main', agentType: 'codex', assistantName: 'Codex', updatedAt, entries: [ { jid: 'dc:ops', name: '#ops', folder: 'ops-room', agentType: 'codex', status: 'processing', elapsedMs: 120_000, pendingMessages: true, pendingTasks: 1, }, ], }; } function roomDeps( overrides: Partial = {}, ): RoomsTimelineRouteDependencies { return { statusMaxAgeMs: 1234, readSnapshots: () => [snapshot()], loadLatestPairedTaskForChat: () => null, loadPairedTurnsForTask: () => [], loadLatestPairedTurnForTask: () => null, loadPairedTurnAttempts: () => [], loadPairedTurnOutputs: () => [], loadRecentPairedTurnOutputsForChat: () => [], loadRecentDeliveredWorkItemsForChat: () => [], loadRecentChatMessages: () => [], ...overrides, }; } function workItem(overrides: Partial = {}): WorkItem { return { id: 9001, group_folder: 'ops-room', chat_jid: 'dc:ops', agent_type: 'codex', service_id: 'codex-main', delivery_role: 'owner', status: 'delivered', start_seq: null, end_seq: null, result_payload: 'owner final output', attachments: [], delivery_attempts: 1, delivery_message_id: 'discord-owner-final', last_error: null, created_at: '2026-04-26T05:18:00.000Z', updated_at: '2026-04-26T05:18:30.000Z', delivered_at: '2026-04-26T05:18:30.000Z', ...overrides, }; } describe('web dashboard room routes', () => { it('serves room timeline with paired progress and recent messages', async () => { const pairedTask = makePairedTask(); const turns: PairedTurnRecord[] = [ { turn_id: 'paired-room-1:reviewer-turn', task_id: pairedTask.id, task_updated_at: pairedTask.updated_at, role: 'reviewer', intent_kind: 'reviewer-turn', state: 'queued', executor_service_id: null, executor_agent_type: null, attempt_no: 0, created_at: '2026-04-26T05:18:30.000Z', updated_at: '2026-04-26T05:21:00.000Z', completed_at: null, last_error: null, progress_text: 'checking current output', progress_updated_at: '2026-04-26T05:20:00.000Z', }, ]; const attempts: PairedTurnAttemptRecord[] = [ { attempt_id: 'paired-room-1:reviewer-turn:attempt:2', parent_attempt_id: null, parent_handoff_id: null, continuation_handoff_id: null, turn_id: 'paired-room-1:reviewer-turn', task_id: pairedTask.id, task_updated_at: pairedTask.updated_at, role: 'reviewer', intent_kind: 'reviewer-turn', state: 'running', executor_service_id: 'claude-reviewer', executor_agent_type: 'claude-code', active_run_id: 'run-reviewer-1', attempt_no: 2, created_at: '2026-04-26T05:19:00.000Z', updated_at: '2026-04-26T05:21:00.000Z', completed_at: null, last_error: 'OPENAI_API_KEY=plain-secret-value', }, ]; const outputs: PairedTurnOutput[] = [ { id: 1, task_id: pairedTask.id, turn_number: 1, role: 'owner', output_text: 'owner final output', verdict: 'step_done', created_at: '2026-04-26T05:18:00.000Z', }, ]; const messages: NewMessage[] = [ { id: 'msg-1', chat_jid: 'dc:ops', sender: 'u1', sender_name: '눈쟁이', content: '진행 어디까지야? BOT_TOKEN=plain-secret-value', timestamp: '2026-04-26T05:17:00.000Z', is_from_me: false, is_bot_message: false, message_source_kind: 'human', }, ]; const requestedMessageLimits: number[] = []; const response = handleRoomTimelineRoute({ url: new URL( `http://localhost/api/rooms/${encodeURIComponent('dc:ops')}/timeline`, ), request: new Request('http://localhost'), jsonResponse, ...roomDeps({ loadLatestPairedTaskForChat: () => pairedTask, loadPairedTurnsForTask: () => turns, loadPairedTurnAttempts: () => attempts, loadPairedTurnOutputs: () => outputs, loadRecentDeliveredWorkItemsForChat: () => [workItem()], loadRecentChatMessages: (_jid, limit) => { requestedMessageLimits.push(limit ?? 20); return messages; }, }), }); expect(response?.status).toBe(200); const body = (await response?.json()) as { pairedTask: { currentTurn: { lastError: string; progressText: string }; outputs: Array<{ outputText: string }>; }; messages: Array<{ content: string; senderName: string }>; }; expect(body.pairedTask.currentTurn).toMatchObject({ lastError: 'OPENAI_API_KEY=', progressText: 'checking current output', }); expect(body.pairedTask.outputs).toEqual([]); expect(requestedMessageLimits).toEqual([8]); expect(body.messages).toEqual([ expect.objectContaining({ content: expect.stringContaining('BOT_TOKEN='), senderName: '눈쟁이', }), expect.objectContaining({ content: 'owner final output', senderName: 'owner', }), ]); expect(body.messages[0]).toMatchObject({ content: expect.stringContaining('BOT_TOKEN='), senderName: '눈쟁이', }); }); it('serves cached room timelines and falls through outside room routes', async () => { const pairedTask = makePairedTask({ id: 'paired-cache-1' }); const response = handleRoomTimelineRoute({ url: new URL('http://localhost/api/rooms-timeline'), request: new Request('http://localhost/api/rooms-timeline', { headers: { 'accept-encoding': 'gzip' }, }), jsonResponse, ...roomDeps({ readSnapshots: () => [snapshot('2026-04-26T06:22:00.000Z')], loadLatestPairedTaskForChat: () => pairedTask, loadLatestPairedTurnForTask: () => null, loadRecentChatMessages: () => [ { id: 'msg-cache', chat_jid: 'dc:ops', sender: 'u1', sender_name: '눈쟁이', content: 'cache message', timestamp: '2026-04-26T06:17:00.000Z', is_from_me: false, is_bot_message: false, message_source_kind: 'human', }, ], }), }); expect(response?.status).toBe(200); expect(response?.headers.get('content-encoding')).toBe('gzip'); const bytes = new Uint8Array(await response!.arrayBuffer()); const body = JSON.parse( new TextDecoder().decode(gunzipSync(bytes)), ) as Record; expect(body['dc:ops']?.pairedTask.id).toBe('paired-cache-1'); expect( handleRoomTimelineRoute({ url: new URL('http://localhost/api/overview'), request: new Request('http://localhost/api/overview'), jsonResponse, ...roomDeps(), }), ).toBeNull(); }); });