import { describe, expect, it, vi } from 'vitest'; import { logger } from '@/ui/logger'; import { AppServerEventConverter } from './appServerEventConverter'; describe('AppServerEventConverter', () => { it('maps thread/started', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('thread/started', { thread: { id: 'thread-1' } }); expect(events).toEqual([{ type: 'thread_started', thread_id: 'thread-1' }]); }); it('maps thread/resumed', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('thread/resumed', { thread: { id: 'thread-2' } }); expect(events).toEqual([{ type: 'thread_started', thread_id: 'thread-2' }]); }); it('maps thread goal updates and clears', () => { const converter = new AppServerEventConverter(); const goal = { threadId: 'thread-1', objective: 'ship goal support', status: 'active' }; expect(converter.handleNotification('thread/goal/updated', { threadId: 'thread-1', turnId: 'turn-1', goal })).toEqual([{ type: 'thread_goal_updated', thread_id: 'thread-1', turn_id: 'turn-1', goal }]); expect(converter.handleNotification('thread/goal/cleared', { threadId: 'thread-1' })).toEqual([{ type: 'thread_goal_cleared', thread_id: 'thread-1' }]); }); it('maps thread systemError to a task failure', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('thread/status/changed', { thread: { id: 'thread-1' }, status: { type: 'systemError' } }); expect(events).toEqual([{ type: 'task_failed', thread_id: 'thread-1', terminal_source: 'thread_status', error: 'Codex thread entered systemError' }]); }); it('maps turn/started and completed statuses', () => { const converter = new AppServerEventConverter(); const started = converter.handleNotification('turn/started', { turn: { id: 'turn-1' } }); expect(started).toEqual([{ type: 'task_started', turn_id: 'turn-1' }]); const completed = converter.handleNotification('turn/completed', { turn: { id: 'turn-1' }, status: 'Completed' }); expect(completed).toEqual([{ type: 'task_complete', turn_id: 'turn-1' }]); const interrupted = converter.handleNotification('turn/completed', { turn: { id: 'turn-1' }, status: 'Interrupted' }); expect(interrupted).toEqual([{ type: 'turn_aborted', turn_id: 'turn-1' }]); const failed = converter.handleNotification('turn/completed', { turn: { id: 'turn-1' }, status: 'Failed', message: 'boom' }); expect(failed).toEqual([{ type: 'task_failed', turn_id: 'turn-1', terminal_source: 'turn_completed', error: 'boom' }]); }); it('accumulates agent message deltas', () => { const converter = new AppServerEventConverter(); converter.handleNotification('item/agentMessage/delta', { itemId: 'msg-1', delta: 'Hello' }); converter.handleNotification('item/agentMessage/delta', { itemId: 'msg-1', delta: ' world' }); const completed = converter.handleNotification('item/completed', { item: { id: 'msg-1', type: 'agentMessage' } }); expect(completed).toEqual([{ type: 'agent_message', message: 'Hello world' }]); }); it('preserves thread and turn scope on item events', () => { const converter = new AppServerEventConverter(); converter.handleNotification('item/agentMessage/delta', { itemId: 'msg-1', delta: 'child output', thread_id: 'child-thread', turn_id: 'child-turn' }); const completed = converter.handleNotification('item/completed', { item: { id: 'msg-1', type: 'agentMessage' }, threadId: 'child-thread', turnId: 'child-turn' }); expect(completed).toEqual([{ type: 'agent_message', thread_id: 'child-thread', turn_id: 'child-turn', message: 'child output' }]); }); it('deduplicates repeated agent message completions for the same item', () => { const converter = new AppServerEventConverter(); converter.handleNotification('item/agentMessage/delta', { itemId: 'msg-1', delta: 'Hello' }); const first = converter.handleNotification('item/completed', { item: { id: 'msg-1', type: 'AgentMessage' } }); const second = converter.handleNotification('item/completed', { item: { id: 'msg-1', type: 'agentMessage' } }); expect(first).toEqual([{ type: 'agent_message', message: 'Hello' }]); expect(second).toEqual([]); }); it('maps command execution items and output deltas', () => { const converter = new AppServerEventConverter(); const started = converter.handleNotification('item/started', { item: { id: 'cmd-1', type: 'commandExecution', command: 'ls' } }); expect(started).toEqual([{ type: 'exec_command_begin', call_id: 'cmd-1', command: 'ls' }]); converter.handleNotification('item/commandExecution/outputDelta', { itemId: 'cmd-1', delta: 'ok' }); const completed = converter.handleNotification('item/completed', { item: { id: 'cmd-1', type: 'commandExecution', exitCode: 0 } }); expect(completed).toEqual([{ type: 'exec_command_end', call_id: 'cmd-1', command: 'ls', output: 'ok', exit_code: 0 }]); }); it('maps MCP tool call items', () => { const converter = new AppServerEventConverter(); const started = converter.handleNotification('item/started', { item: { id: 'call-1', type: 'mcpToolCall', server: 'hapi', tool: 'change_title', arguments: { title: 'MCP Title' } } }); expect(started).toEqual([{ type: 'mcp_tool_call_begin', call_id: 'call-1', server: 'hapi', tool: 'change_title', invocation: { server: 'hapi', tool: 'change_title', arguments: { title: 'MCP Title' } } }]); const completed = converter.handleNotification('item/completed', { item: { id: 'call-1', type: 'mcpToolCall', server: 'hapi', tool: 'change_title', result: { content: [{ type: 'text', text: 'done' }] } } }); expect(completed).toEqual([{ type: 'mcp_tool_call_end', call_id: 'call-1', server: 'hapi', tool: 'change_title', result: { content: [{ type: 'text', text: 'done' }] } }]); }); it('maps MCP tool call item errors', () => { const converter = new AppServerEventConverter(); const completed = converter.handleNotification('item/completed', { item: { id: 'call-1', type: 'mcpToolCall', server: 'hapi', tool: 'change_title', error: 'boom' } }); expect(completed).toEqual([{ type: 'mcp_tool_call_end', call_id: 'call-1', server: 'hapi', tool: 'change_title', result: { Err: 'boom' } }]); }); it('maps Codex collab spawn agent calls', () => { const converter = new AppServerEventConverter(); const started = converter.handleNotification('item/started', { item: { id: 'call-spawn', type: 'collabAgentToolCall', tool: 'spawnAgent', prompt: 'Do side work', agentType: 'explorer', forkContext: true, model: 'gpt-5.5', reasoningEffort: 'low', senderThreadId: 'parent-thread', receiverThreadIds: [] } }); expect(started).toEqual([{ type: 'codex_tool_call_begin', call_id: 'call-spawn', name: 'spawn_agent', input: { message: 'Do side work', agent_type: 'explorer', fork_context: true, model: 'gpt-5.5', reasoning_effort: 'low', sender_thread_id: 'parent-thread' } }]); const completed = converter.handleNotification('item/completed', { item: { id: 'call-spawn', type: 'collabAgentToolCall', tool: 'spawnAgent', status: 'completed', receiverThreadIds: ['agent-1'], agentsStates: { 'agent-1': { status: 'pendingInit', message: null } } } }); expect(completed).toEqual([{ type: 'codex_tool_call_end', call_id: 'call-spawn', name: 'spawn_agent', output: { agent_id: 'agent-1', agentId: 'agent-1', status: 'completed', agentsStates: { 'agent-1': { status: 'pendingInit', message: null } } }, is_error: false }]); }); it('maps Codex collab wait and close outputs for web agent views', () => { const converter = new AppServerEventConverter(); const waitStarted = converter.handleNotification('item/started', { item: { id: 'call-wait', type: 'collabAgentToolCall', tool: 'wait', receiverThreadIds: ['agent-1', 'agent-2'] } }); expect(waitStarted).toEqual([{ type: 'codex_tool_call_begin', call_id: 'call-wait', name: 'wait_agent', input: { targets: ['agent-1', 'agent-2'] } }]); const waitCompleted = converter.handleNotification('item/completed', { item: { id: 'call-wait', type: 'collabAgentToolCall', tool: 'wait', status: 'completed', receiverThreadIds: ['agent-1'], agentsStates: { 'agent-1': { status: 'completed', message: '42' }, 'agent-2': { status: 'done', message: null }, 'agent-3': { status: 'done', result: { text: 'structured result' } }, 'agent-4': { status: 'completed', message: '', output: { value: 42 } } } } }); expect(waitCompleted).toEqual([{ type: 'codex_tool_call_end', call_id: 'call-wait', name: 'wait_agent', output: { status: { 'agent-1': { completed: '42' }, 'agent-2': { status: 'completed', message: null }, 'agent-3': { status: 'completed', result: { text: 'structured result' } }, 'agent-4': { status: 'completed', message: '', output: { value: 42 } } }, timed_out: false }, is_error: false }]); const closeCompleted = converter.handleNotification('item/completed', { item: { id: 'call-close', type: 'collabAgentToolCall', tool: 'closeAgent', status: 'completed', receiverThreadIds: ['agent-1'], agentsStates: { 'agent-1': { status: 'completed', message: 'done' } } } }); expect(closeCompleted).toEqual([{ type: 'codex_tool_call_end', call_id: 'call-close', name: 'close_agent', output: { previous_status: { completed: 'done' }, agent_id: 'agent-1' }, is_error: false }]); }); it('maps raw MultiAgent V2 calls without inventing collab tool variants', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('rawResponseItem/completed', { threadId: 'thread-1', turnId: 'turn-1', item: { type: 'function_call', namespace: 'collaboration', name: 'followup_task', arguments: JSON.stringify({ target: '/root/review', message: 'Continue with tests' }), call_id: 'call-followup' } })).toEqual([{ thread_id: 'thread-1', turn_id: 'turn-1', type: 'codex_tool_call_begin', call_id: 'call-followup', name: 'followup_task', input: { message: 'Continue with tests', target: '/root/review' } }]); expect(converter.handleNotification('rawResponseItem/completed', { threadId: 'thread-1', turnId: 'turn-1', item: { type: 'function_call_output', call_id: 'call-followup', output: '' } })).toEqual([{ thread_id: 'thread-1', turn_id: 'turn-1', type: 'codex_tool_call_end', call_id: 'call-followup', name: 'followup_task', output: '', is_error: false }]); expect(converter.handleNotification('item/started', { item: { id: 'call-followup', type: 'collabAgentToolCall', tool: 'followupTask' } })).toEqual([]); }); it('keeps MultiAgent V1 calls on the collab item stream', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('rawResponseItem/completed', { threadId: 'thread-1', turnId: 'turn-1', item: { type: 'function_call', namespace: 'multi_agent_v1', name: 'spawn_agent', arguments: JSON.stringify({ message: 'Do side work' }), call_id: 'call-v1-spawn' } })).toEqual([]); expect(converter.handleNotification('rawResponseItem/completed', { threadId: 'thread-1', turnId: 'turn-1', item: { type: 'function_call_output', call_id: 'call-v1-spawn', output: '{"agent_id":"agent-1","nickname":null}' } })).toEqual([]); expect(converter.handleNotification('item/started', { item: { id: 'call-v1-spawn', type: 'collabAgentToolCall', tool: 'spawnAgent', prompt: 'Do side work' } })).toEqual([expect.objectContaining({ type: 'codex_tool_call_begin', call_id: 'call-v1-spawn', name: 'spawn_agent' })]); }); it('maps shared MultiAgent V2 names only when their V2 argument shape is present', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('rawResponseItem/completed', { item: { type: 'function_call', namespace: 'collaboration', name: 'spawn_agent', arguments: JSON.stringify({ task_name: 'review', message: 'Review this' }), call_id: 'call-v2-spawn' } })).toEqual([expect.objectContaining({ type: 'codex_tool_call_begin', call_id: 'call-v2-spawn', name: 'spawn_agent', input: { task_name: 'review', message: 'Review this' } })]); expect(converter.handleNotification('rawResponseItem/completed', { item: { type: 'function_call', namespace: 'collaboration', name: 'wait_agent', arguments: JSON.stringify({ timeout_ms: 10_000 }), call_id: 'call-v2-wait' } })).toEqual([expect.objectContaining({ type: 'codex_tool_call_begin', call_id: 'call-v2-wait', name: 'wait_agent', input: { timeout_ms: 10_000 } })]); expect(converter.handleNotification('rawResponseItem/completed', { item: { type: 'function_call', namespace: 'agents', name: 'list_agents', arguments: '{}', call_id: 'call-v2-custom-namespace' } })).toEqual([expect.objectContaining({ type: 'codex_tool_call_begin', call_id: 'call-v2-custom-namespace', name: 'list_agents' })]); }); it('maps reasoning deltas', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('item/reasoning/textDelta', { itemId: 'r1', delta: 'step' }); expect(events).toEqual([{ type: 'agent_reasoning_delta', delta: 'step' }]); }); it('dedupes duplicate reasoning deltas', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('item/reasoning/textDelta', { itemId: 'r1', delta: 'Hello ' })) .toEqual([{ type: 'agent_reasoning_delta', delta: 'Hello ' }]); expect(converter.handleNotification('item/reasoning/textDelta', { itemId: 'r1', delta: 'Hello ' })) .toEqual([]); converter.handleNotification('item/reasoning/textDelta', { itemId: 'r1', delta: 'world' }); const completed = converter.handleNotification('item/completed', { item: { id: 'r1', type: 'reasoning' } }); expect(completed).toEqual([{ type: 'agent_reasoning', text: 'Hello world' }]); }); it('maps reasoning summary deltas', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('item/reasoning/summaryTextDelta', { itemId: 'r1', delta: 'step' }); expect(events).toEqual([{ type: 'agent_reasoning_delta', delta: 'step' }]); }); it('deduplicates repeated reasoning completions for the same item', () => { const converter = new AppServerEventConverter(); const first = converter.handleNotification('item/completed', { item: { id: 'r1', type: 'Reasoning', summary_text: ['Plan'] } }); const second = converter.handleNotification('item/completed', { item: { id: 'r1', type: 'reasoning', summary_text: ['Plan'] } }); expect(first).toEqual([{ type: 'agent_reasoning', text: 'Plan' }]); expect(second).toEqual([]); }); it('maps turn plan updates into update_plan events', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('turn/plan/updated', { plan: [ { step: 'Inspect Codex events', status: 'completed' }, { content: 'Render plan state', status: 'in_progress' }, { title: 'Verify web DOM', status: 'pending' } ] }); expect(events).toEqual([{ type: 'plan_update', plan: [ { step: 'Inspect Codex events', status: 'completed' }, { step: 'Render plan state', status: 'in_progress' }, { step: 'Verify web DOM', status: 'pending' } ] }]); }); it('unwraps wrapped codex plan updates', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('codex/event/plan_update', { msg: { type: 'plan_update', update: { items: [ { text: 'Plan from wrapped event', status: 'completed' } ] } } }); expect(events).toEqual([{ type: 'plan_update', plan: [ { step: 'Plan from wrapped event', status: 'completed' } ] }]); }); it('maps diff updates', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('turn/diff/updated', { diff: 'diff --git a b' }); expect(events).toEqual([{ type: 'turn_diff', unified_diff: 'diff --git a b' }]); }); it('preserves scope on diff and token usage updates', () => { const converter = new AppServerEventConverter(); const diffEvents = converter.handleNotification('turn/diff/updated', { threadId: 'child-thread', turnId: 'child-turn', diff: 'diff --git a b' }); expect(diffEvents).toEqual([{ type: 'turn_diff', thread_id: 'child-thread', turn_id: 'child-turn', unified_diff: 'diff --git a b' }]); const tokenEvents = converter.handleNotification('thread/tokenUsage/updated', { tokenUsage: { thread_id: 'child-thread', turn_id: 'child-turn', last_token_usage: { input_tokens: 10, output_tokens: 2 } } }); expect(tokenEvents).toEqual([{ type: 'token_count', thread_id: 'child-thread', turn_id: 'child-turn', info: { thread_id: 'child-thread', turn_id: 'child-turn', last_token_usage: { input_tokens: 10, output_tokens: 2 } } }]); }); it('maps compact notifications with scope', () => { const converter = new AppServerEventConverter(); const direct = converter.handleNotification('thread/compacted', { thread: { id: 'thread-1' } }); expect(direct).toEqual([ { type: 'thread_compacted', thread_id: 'thread-1' }, { type: 'context_compacted', thread_id: 'thread-1' } ]); const wrapped = converter.handleNotification('codex/event/context_compacted', { msg: { type: 'context_compacted', thread_id: 'thread-2', turn_id: 'turn-2' } }); expect(wrapped).toEqual([ { type: 'thread_compacted', thread_id: 'thread-2', turn_id: 'turn-2' }, { type: 'context_compacted', thread_id: 'thread-2', turn_id: 'turn-2' } ]); }); it('unwraps codex/event task lifecycle', () => { const converter = new AppServerEventConverter(); const started = converter.handleNotification('codex/event/task_started', { msg: { type: 'task_started', turn_id: 'turn-1' } }); expect(started).toEqual([{ type: 'task_started', turn_id: 'turn-1' }]); const completed = converter.handleNotification('codex/event/task_complete', { msg: { type: 'task_complete', turn_id: 'turn-1' } }); expect(completed).toEqual([{ type: 'task_complete', turn_id: 'turn-1' }]); }); it('preserves nested scope on wrapped terminal lifecycle events', () => { const converter = new AppServerEventConverter(); const completed = converter.handleNotification('codex/event/task_complete', { msg: { type: 'task_complete', thread: { id: 'child-thread' }, turn: { id: 'child-turn' } } }); expect(completed).toEqual([{ type: 'task_complete', thread_id: 'child-thread', turn_id: 'child-turn' }]); }); it('ignores wrapped terminal lifecycle events without turn_id', () => { const converter = new AppServerEventConverter(); const completed = converter.handleNotification('codex/event/task_complete', { msg: { type: 'task_complete' } }); expect(completed).toEqual([]); }); it('unwraps codex/event agent deltas and item completion', () => { const converter = new AppServerEventConverter(); converter.handleNotification('codex/event/agent_message_delta', { msg: { type: 'agent_message_delta', item_id: 'msg-1', delta: 'Hello' } }); converter.handleNotification('codex/event/agent_message_content_delta', { msg: { type: 'agent_message_content_delta', item_id: 'msg-1', delta: ' world' } }); const completed = converter.handleNotification('codex/event/item_completed', { msg: { type: 'item_completed', item_id: 'msg-1', item: { id: 'msg-1', type: 'AgentMessage' } } }); expect(completed).toEqual([{ type: 'agent_message', message: 'Hello world' }]); }); it('preserves nested scope on wrapped item lifecycle events', () => { const converter = new AppServerEventConverter(); const started = converter.handleNotification('codex/event/item_started', { msg: { type: 'item_started', thread: { id: 'child-thread' }, turn: { id: 'child-turn' }, item: { id: 'cmd-1', type: 'commandExecution', command: 'pwd' } } }); expect(started).toEqual([{ type: 'exec_command_begin', thread_id: 'child-thread', turn_id: 'child-turn', call_id: 'cmd-1', command: 'pwd' }]); const completed = converter.handleNotification('codex/event/item_completed', { msg: { type: 'item_completed', item_id: 'msg-1', item: { id: 'msg-1', type: 'AgentMessage', message: 'child output', thread: { id: 'child-thread' }, turn: { id: 'child-turn' } } } }); expect(completed).toEqual([{ type: 'agent_message', thread_id: 'child-thread', turn_id: 'child-turn', message: 'child output' }]); }); it('unwraps codex/event reasoning completion from summary text', () => { const converter = new AppServerEventConverter(); converter.handleNotification('codex/event/reasoning_content_delta', { msg: { type: 'reasoning_content_delta', item_id: 'r1', delta: 'Plan' } }); const completed = converter.handleNotification('codex/event/item_completed', { msg: { type: 'item_completed', item_id: 'r1', item: { id: 'r1', type: 'Reasoning', summary_text: ['Plan done'] } } }); expect(completed).toEqual([{ type: 'agent_reasoning', text: 'Plan done' }]); }); it('prefers canonical reasoning stream over wrapped agent_reasoning events', () => { const converter = new AppServerEventConverter(); const section = converter.handleNotification('codex/event/agent_reasoning_section_break', { msg: { type: 'agent_reasoning_section_break', item_id: 'r1' } }); const delta = converter.handleNotification('codex/event/agent_reasoning_delta', { msg: { type: 'agent_reasoning_delta', item_id: 'r1', delta: 'step' } }); const reasoning = converter.handleNotification('codex/event/agent_reasoning', { msg: { type: 'agent_reasoning', item_id: 'r1', text: 'Plan' } }); expect(section).toEqual([{ type: 'agent_reasoning_section_break' }]); expect(delta).toEqual([]); expect(reasoning).toEqual([]); }); it('deduplicates section break when wrapped and direct summary part events share the same index', () => { const converter = new AppServerEventConverter(); const wrapped = converter.handleNotification('codex/event/agent_reasoning_section_break', { msg: { type: 'agent_reasoning_section_break', item_id: 'r1', summary_index: 0 } }); const direct = converter.handleNotification('item/reasoning/summaryPartAdded', { itemId: 'r1', summaryIndex: 0 }); expect(wrapped).toEqual([{ type: 'agent_reasoning_section_break' }]); expect(direct).toEqual([]); }); it('ignores wrapped final agent message and relies on item completion', () => { const converter = new AppServerEventConverter(); const wrapped = converter.handleNotification('codex/event/agent_message', { msg: { type: 'agent_message', item_id: 'msg-1', message: 'Hello' } }); expect(wrapped).toEqual([]); }); it('ignores wrapped retryable errors', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('codex/event/error', { msg: { type: 'error', message: 'temporary', will_retry: true } }); expect(events).toEqual([]); }); it('maps wrapped non-retryable errors to task_failed', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('codex/event/error', { msg: { type: 'error', message: 'fatal' } }); expect(events).toEqual([{ type: 'task_failed', error: 'fatal' }]); }); it('preserves typed non-retryable cyber-policy errors', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('error', { threadId: 'thread-1', turnId: 'turn-1', error: { message: 'This content was flagged for possible cybersecurity risk.', codexErrorInfo: 'cyberPolicy' }, willRetry: false }); expect(events).toEqual([{ type: 'task_failed', thread_id: 'thread-1', turn_id: 'turn-1', terminal_source: 'error', retryable: false, codex_error_info: 'cyberPolicy', error: 'This content was flagged for possible cybersecurity risk.' }]); }); it('preserves cyber-policy metadata from wrapped and completed-turn errors', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('codex/event/error', { msg: { type: 'error', thread_id: 'thread-1', turn_id: 'turn-1', message: 'wrapped policy failure', codex_error_info: 'cyber_policy', will_retry: false } })).toEqual([{ type: 'task_failed', thread_id: 'thread-1', turn_id: 'turn-1', retryable: false, codex_error_info: 'cyber_policy', error: 'wrapped policy failure' }]); expect(converter.handleNotification('turn/completed', { threadId: 'thread-1', turn: { id: 'turn-1', status: 'failed', error: { message: 'completed policy failure', codexErrorInfo: 'CyberPolicy' } } })).toEqual([{ type: 'task_failed', thread_id: 'thread-1', turn_id: 'turn-1', terminal_source: 'turn_completed', codex_error_info: 'CyberPolicy', error: 'completed policy failure' }]); }); it('maps Codex model safety notifications', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('model/safetyBuffering/updated', { threadId: 'thread-1', turnId: 'turn-1', model: 'gpt-5.4', useCases: ['cyber'], reasons: ['review'], showBufferingUi: true, fasterModel: 'gpt-5.4-mini' })).toEqual([{ type: 'model_safety_buffering', thread_id: 'thread-1', turn_id: 'turn-1', model: 'gpt-5.4', use_cases: ['cyber'], reasons: ['review'], show_buffering_ui: true, faster_model: 'gpt-5.4-mini' }]); expect(converter.handleNotification('model/rerouted', { threadId: 'thread-1', turnId: 'turn-1', fromModel: 'gpt-5.4', toModel: 'gpt-5.4-codex', reason: 'highRiskCyberActivity' })).toEqual([{ type: 'model_rerouted', thread_id: 'thread-1', turn_id: 'turn-1', from_model: 'gpt-5.4', to_model: 'gpt-5.4-codex', reason: 'highRiskCyberActivity' }]); expect(converter.handleNotification('model/verification', { threadId: 'thread-1', turnId: 'turn-1', verifications: ['trustedAccessForCyber'] })).toEqual([{ type: 'model_verification', thread_id: 'thread-1', turn_id: 'turn-1', verifications: ['trustedAccessForCyber'] }]); }); it('maps thread/compacted notifications', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('thread/compacted', { threadId: 'thread-1', turnId: 'turn-compact' }); expect(events).toEqual([ { type: 'thread_compacted', thread_id: 'thread-1', turn_id: 'turn-compact' }, { type: 'context_compacted', thread_id: 'thread-1', turn_id: 'turn-compact' } ]); }); it('maps completed contextCompaction items and preserves the turn boundary', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('item/completed', { threadId: 'thread-1', turnId: 'turn-compact', item: { id: 'compact-item-1', type: 'contextCompaction' } }); expect(events).toEqual([ { type: 'thread_compacted', thread_id: 'thread-1', turn_id: 'turn-compact', await_turn_completion: true }, { type: 'context_compacted', thread_id: 'thread-1', turn_id: 'turn-compact' } ]); }); it('ignores compacted notifications without thread ids', () => { const converter = new AppServerEventConverter(); expect(converter.handleNotification('thread/compacted', { turnId: 'turn-compact' })).toEqual([]); expect(converter.handleNotification('codex/event/context_compacted', { msg: { type: 'context_compacted', turn_id: 'turn-compact' } })).toEqual([]); }); it('unwraps context_compacted events', () => { const converter = new AppServerEventConverter(); const events = converter.handleNotification('codex/event/context_compacted', { msg: { type: 'context_compacted', thread_id: 'thread-1', turn_id: 'turn-compact' } }); expect(events).toEqual([ { type: 'thread_compacted', thread_id: 'thread-1', turn_id: 'turn-compact' }, { type: 'context_compacted', thread_id: 'thread-1', turn_id: 'turn-compact' } ]); }); it('converts completed image generation items without including large result payloads', () => { const converter = new AppServerEventConverter(); const largeImageResult = 'a'.repeat(4096); const events = converter.handleNotification('item/completed', { item: { id: 'image-1', type: 'imageGeneration', result: largeImageResult, savedPath: '/tmp/image.png', mimeType: 'image/png' } }); expect(events).toEqual([{ type: 'generated_image', image_id: 'image-1', saved_path: '/tmp/image.png', file_name: 'image.png', mime_type: 'image/png' }]); expect(JSON.stringify(events)).not.toContain(largeImageResult); }); it('truncates large unhandled notification payloads before logging', () => { const converter = new AppServerEventConverter(); const debug = vi.spyOn(logger, 'debug').mockImplementation(() => undefined); const largeResult = 'a'.repeat(4096); const events = converter.handleNotification('item/completed', { item: { id: 'unknown-1', type: 'unknownLargePayload', result: largeResult, savedPath: '/tmp/image.png' } }); expect(events).toEqual([]); expect(debug).toHaveBeenCalledTimes(1); const logged = debug.mock.calls[0]?.[1] as { params?: { item?: { result?: string; savedPath?: string } } }; expect(logged.params?.item?.result).not.toBe(largeResult); expect(logged.params?.item?.result).toContain('[truncated 3584 chars for logs]'); expect(logged.params?.item?.savedPath).toBe('/tmp/image.png'); debug.mockRestore(); }); });