import { describe, it, expect } from 'vitest'; import type { SessionNotification } from '@agentclientprotocol/sdk'; import { mapSessionUpdate } from '../acp-event-map.js'; import type { AcpToolSnapshot } from '../acp-tool-snapshot.js'; /** * Pure event-mapping shared by the one-shot ACP dispatch (AcpStreamContext) and * the warm ACP backend (Phase 2). Mirrors the original handleSessionUpdate switch * verbatim but returns normalized AgentEvents instead of publishing broker frames. */ describe('mapSessionUpdate (shared ACP event mapping)', () => { function note(update: SessionNotification['update']): SessionNotification { return { sessionId: 's1', update }; } it('maps an agent_message_chunk text → a text event', () => { const events = mapSessionUpdate( note({ sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'hello' } }), ); expect(events).toEqual([{ type: 'text', text: 'hello' }]); }); it('maps an agent_thought_chunk text → a reasoning event', () => { const events = mapSessionUpdate( note({ sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'thinking' } }), ); expect(events).toEqual([{ type: 'reasoning', text: 'thinking' }]); }); it('ignores non-text content on message/thought chunks', () => { const img = mapSessionUpdate( note({ sessionUpdate: 'agent_message_chunk', content: { type: 'image', data: 'x', mimeType: 'image/png' }, } as never), ); expect(img).toEqual([]); }); it('maps a tool_call → a tool_call event with a merged snapshot', () => { const events = mapSessionUpdate( note({ sessionUpdate: 'tool_call', toolCallId: 't1', title: 'read_file', status: 'pending', rawInput: { path: 'a.ts' }, } as never), ); expect(events).toHaveLength(1); expect(events[0]!.type).toBe('tool_call'); const snap = (events[0] as { type: 'tool_call'; toolCall: AcpToolSnapshot }).toolCall; expect(snap.toolCallId).toBe('t1'); expect(snap.title).toBe('read_file'); expect(snap.status).toBe('pending'); expect(snap.rawInput).toEqual({ path: 'a.ts' }); }); it('maps a tool_call_update → a tool_update event merged over the prior snapshot', () => { const prior = new Map([ ['t1', { toolCallId: 't1', title: 'read_file', status: 'pending', rawInput: { path: 'a.ts' } }], ]); const events = mapSessionUpdate( note({ sessionUpdate: 'tool_call_update', toolCallId: 't1', status: 'completed', rawOutput: 'file body', } as never), prior, ); expect(events).toHaveLength(1); expect(events[0]!.type).toBe('tool_update'); const snap = (events[0] as { type: 'tool_update'; toolCall: AcpToolSnapshot }).toolCall; expect(snap.toolCallId).toBe('t1'); // merged: title carried from prior, status updated, output added, input retained expect(snap.title).toBe('read_file'); expect(snap.status).toBe('completed'); expect(snap.rawOutput).toBe('file body'); expect(snap.rawInput).toEqual({ path: 'a.ts' }); }); it('maps available_commands_update → a commands event', () => { const events = mapSessionUpdate( note({ sessionUpdate: 'available_commands_update', availableCommands: [ { name: 'plan', description: 'make a plan' }, { name: 'review', description: null }, ], } as never), ); expect(events).toEqual([ { type: 'commands', commands: [ { name: 'plan', description: 'make a plan' }, { name: 'review', description: undefined }, ], }, ]); }); it('returns [] for unhandled update kinds (plan, mode change)', () => { expect(mapSessionUpdate(note({ sessionUpdate: 'plan', entries: [] } as never))).toEqual([]); expect( mapSessionUpdate(note({ sessionUpdate: 'current_mode_update', currentModeId: 'code' } as never)), ).toEqual([]); }); });