import { describe, expect, test } from 'bun:test'; import type { PatternDefinition } from '@shared/domain/pattern'; import { appendRunActivityEvent, completeSessionRunRecord, createSessionRunRecord, normalizeSessionRunRecords, upsertRunApprovalEvent, upsertRunMessageEvent, } from '@shared/domain/runTimeline'; import type { ProjectRecord } from '@shared/domain/project'; import type { PendingApprovalRecord } from '@shared/domain/approval'; function createPattern(): PatternDefinition { return { id: 'pattern-sequential', name: 'Sequential Trio Review', description: 'Sequential handoff review flow.', mode: 'sequential', availability: 'available', maxIterations: 1, agents: [ { id: 'agent-writer', name: 'Writer', description: 'Writes the draft.', instructions: 'Write.', model: 'gpt-5.4', }, { id: 'agent-reviewer', name: 'Reviewer', description: 'Reviews the draft.', instructions: 'Review.', model: 'claude-sonnet-4.5', }, ], createdAt: '2026-03-23T00:00:00.000Z', updatedAt: '2026-03-23T00:00:00.000Z', }; } function createProject(): ProjectRecord { return { id: 'project-1', name: 'alpha', path: 'C:\\workspace\\alpha', addedAt: '2026-03-23T00:00:00.000Z', }; } describe('run timeline helpers', () => { test('creates a persistent run record with agent lanes and a trigger event', () => { const run = createSessionRunRecord({ requestId: 'turn-1', project: createProject(), workspaceKind: 'project', pattern: createPattern(), triggerMessageId: 'msg-user-1', startedAt: '2026-03-23T00:00:01.000Z', }); expect(run).toMatchObject({ requestId: 'turn-1', projectId: 'project-1', projectPath: 'C:\\workspace\\alpha', patternId: 'pattern-sequential', patternName: 'Sequential Trio Review', patternMode: 'sequential', triggerMessageId: 'msg-user-1', status: 'running', }); expect(run.agents).toEqual([ { agentId: 'agent-writer', agentName: 'Writer', model: 'gpt-5.4', reasoningEffort: undefined, }, { agentId: 'agent-reviewer', agentName: 'Reviewer', model: 'claude-sonnet-4.5', reasoningEffort: undefined, }, ]); expect(run.events[0]).toMatchObject({ kind: 'run-started', status: 'completed', messageId: 'msg-user-1', }); }); test('records grouped message steps and settles the run on completion', () => { const baseRun = createSessionRunRecord({ requestId: 'turn-1', project: createProject(), workspaceKind: 'project', pattern: createPattern(), triggerMessageId: 'msg-user-1', startedAt: '2026-03-23T00:00:01.000Z', }); const streamingRun = upsertRunMessageEvent(baseRun, { messageId: 'msg-assistant-1', authorName: 'Writer', content: 'Draft', occurredAt: '2026-03-23T00:00:02.000Z', status: 'running', }); const completedRun = completeSessionRunRecord( upsertRunMessageEvent(streamingRun, { messageId: 'msg-assistant-1', authorName: 'Writer', content: 'Draft polished into the final answer.', occurredAt: '2026-03-23T00:00:03.000Z', status: 'completed', }), '2026-03-23T00:00:04.000Z', ); const messageEvent = completedRun.events.find((event) => event.kind === 'message'); expect(messageEvent).toMatchObject({ messageId: 'msg-assistant-1', agentId: 'agent-writer', agentName: 'Writer', content: 'Draft polished into the final answer.', status: 'completed', updatedAt: '2026-03-23T00:00:03.000Z', }); expect(completedRun.status).toBe('completed'); expect(completedRun.completedAt).toBe('2026-03-23T00:00:04.000Z'); expect(completedRun.events.at(-1)).toMatchObject({ kind: 'run-completed', status: 'completed', }); }); test('captures handoffs with explicit source and target agents', () => { const run = appendRunActivityEvent( createSessionRunRecord({ requestId: 'turn-1', project: createProject(), workspaceKind: 'project', pattern: createPattern(), triggerMessageId: 'msg-user-1', startedAt: '2026-03-23T00:00:01.000Z', }), { activityType: 'handoff', occurredAt: '2026-03-23T00:00:02.000Z', sourceAgentId: 'agent-writer', agentId: 'agent-reviewer', }, ); expect(run.events.at(-1)).toMatchObject({ kind: 'handoff', agentId: 'agent-writer', agentName: 'Writer', sourceAgentId: 'agent-writer', sourceAgentName: 'Writer', targetAgentId: 'agent-reviewer', targetAgentName: 'Reviewer', }); }); test('normalizes missing run collections to an empty array', () => { expect(normalizeSessionRunRecords(undefined)).toEqual([]); }); test('tracks approval checkpoints as a single timeline event that can be resolved later', () => { const baseRun = createSessionRunRecord({ requestId: 'turn-1', project: createProject(), workspaceKind: 'project', pattern: createPattern(), triggerMessageId: 'msg-user-1', startedAt: '2026-03-23T00:00:01.000Z', }); const pendingApproval: PendingApprovalRecord = { id: 'approval-1', kind: 'tool-call', status: 'pending', requestedAt: '2026-03-23T00:00:02.000Z', agentId: 'agent-writer', agentName: 'Writer', title: 'Approve tool access', permissionKind: 'tool access', }; const pendingRun = upsertRunApprovalEvent(baseRun, pendingApproval); const resolvedRun = upsertRunApprovalEvent(pendingRun, { ...pendingApproval, status: 'approved', resolvedAt: '2026-03-23T00:00:03.000Z', }); const approvalEvent = resolvedRun.events.find((event) => event.kind === 'approval'); expect(approvalEvent).toMatchObject({ approvalId: 'approval-1', approvalKind: 'tool-call', approvalTitle: 'Approve tool access', permissionKind: 'tool access', status: 'completed', decision: 'approved', updatedAt: '2026-03-23T00:00:03.000Z', }); }); });