import { describe, expect, it } from 'bun:test' import { createCurrentTurnLocalCommandForwarder, translateCliMessage, } from '../ws/handler.js' import { parseSlashCommand } from '../../utils/slashCommandParsing.js' describe('WebSocket memory events', () => { it('forwards CLI memory_saved system messages to the desktop client', () => { const messages = translateCliMessage( { type: 'system', subtype: 'memory_saved', writtenPaths: [ '/Users/test/.claude/projects/example/memory/preferences.md', '/Users/test/.claude/projects/example/memory/team/MEMORY.md', ], teamCount: 1, verb: 'Saved', }, 'session-1', ) expect(messages).toEqual([ { type: 'system_notification', subtype: 'memory_saved', message: undefined, data: { writtenPaths: [ '/Users/test/.claude/projects/example/memory/preferences.md', '/Users/test/.claude/projects/example/memory/team/MEMORY.md', ], teamCount: 1, verb: 'Saved', }, }, ]) }) }) describe('WebSocket compact events', () => { it('forwards CLI compacting status to the desktop client', () => { expect(translateCliMessage({ type: 'system', subtype: 'status', status: 'compacting', }, 'session-1')).toEqual([ { type: 'status', state: 'compacting', verb: 'Compacting conversation', }, ]) expect(translateCliMessage({ type: 'system', subtype: 'status', status: null, }, 'session-1')).toEqual([ { type: 'status', state: 'thinking', verb: 'Thinking', }, ]) expect(translateCliMessage({ type: 'system', subtype: 'status', status: 'warming', }, 'session-1')).toEqual([]) }) it('forwards compact summaries as system notifications instead of user chat bubbles', () => { const summary = [ 'This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.', '', 'Built the compact UI and verified the WebSocket event path.', ].join('\n') expect(translateCliMessage({ type: 'user', message: { role: 'user', content: summary, }, isSynthetic: true, }, 'session-1')).toEqual([ { type: 'system_notification', subtype: 'compact_summary', message: summary, data: { isSynthetic: true }, }, ]) }) it('suppresses compact local command output after the compact summary', () => { expect(translateCliMessage({ type: 'user', message: { role: 'user', content: 'Compacted ', }, }, 'session-1')).toEqual([]) }) }) describe('WebSocket API retry events', () => { it('forwards CLI api_retry messages as structured retry status', () => { expect(translateCliMessage({ type: 'system', subtype: 'api_retry', attempt: 2, max_retries: 10, retry_delay_ms: 1500, error_status: 503, error: 'server_error', }, 'session-1')).toEqual([ { type: 'api_retry', attempt: 2, maxRetries: 10, retryDelayMs: 1500, errorStatus: 503, errorType: 'server_error', }, ]) }) }) describe('WebSocket background task events', () => { it('forwards task start and progress as structured desktop notifications', () => { const started = { type: 'system', subtype: 'task_started', task_id: 'agent-task-1', tool_use_id: 'agent-tool-1', description: 'Verify the todo app', task_type: 'local_agent', prompt: 'Run E2E checks', } expect(translateCliMessage(started, 'session-1')).toEqual([ { type: 'system_notification', subtype: 'task_started', message: 'Verify the todo app', data: started, }, { type: 'status', state: 'tool_executing', verb: 'Verify the todo app', }, ]) const progress = { type: 'system', subtype: 'task_progress', task_id: 'agent-task-1', tool_use_id: 'agent-tool-1', description: 'Verify the todo app', summary: 'Running Playwright checks', last_tool_name: 'Bash', usage: { total_tokens: 1200, tool_uses: 4, duration_ms: 45000, }, } expect(translateCliMessage(progress, 'session-1')).toEqual([ { type: 'system_notification', subtype: 'task_progress', message: 'Running Playwright checks', data: progress, }, { type: 'status', state: 'tool_executing', verb: 'Running Playwright checks', }, ]) }) }) describe('WebSocket goal command events', () => { const goalStatusOutput = 'Goal set: ship the smoke test' const runGoalCommand = (sessionId: string, args: string, output: string, type: 'system' | 'user' = 'system') => { expect(translateCliMessage({ type: 'system', subtype: 'local_command', content: [ { text: '/goal' }, { text: `${args}` }, ], }, sessionId)).toEqual([]) if (type === 'user') { return translateCliMessage({ type: 'user', message: { content: [{ type: 'text', text: `${output}`, }], }, }, sessionId) } return translateCliMessage({ type: 'system', subtype: 'local_command_output', content: `${output}`, }, sessionId) } it('turns confirmed /goal local command output into a desktop goal event', () => { const sessionId = `goal-event-${crypto.randomUUID()}` expect(translateCliMessage({ type: 'system', subtype: 'local_command', content: '/goal\nship the smoke test', }, sessionId)).toEqual([]) expect(translateCliMessage({ type: 'system', subtype: 'local_command', content: [ '', goalStatusOutput, '', ].join('\n'), }, sessionId)).toEqual([ { type: 'system_notification', subtype: 'goal_event', message: goalStatusOutput, data: { action: 'created', status: 'active', objective: 'ship the smoke test', message: goalStatusOutput, }, }, ]) }) it('classifies /goal clear and completion output for the desktop client', () => { expect(runGoalCommand(`goal-complete-${crypto.randomUUID()}`, 'complete', 'Goal marked complete.')).toEqual([ expect.objectContaining({ type: 'system_notification', subtype: 'goal_event', data: { action: 'completed', message: 'Goal marked complete.' }, }), ]) expect(runGoalCommand(`goal-clear-${crypto.randomUUID()}`, 'clear', 'Goal cleared: ship docs')).toEqual([ expect.objectContaining({ type: 'system_notification', subtype: 'goal_event', data: { action: 'cleared', message: 'Goal cleared: ship docs' }, }), ]) }) it('allows direct /goal local command output through the pre-turn mute gate', () => { const shouldForward = createCurrentTurnLocalCommandForwarder( parseSlashCommand('/goal ship the smoke test'), ) expect(shouldForward({ type: 'system', subtype: 'local_command_output', content: 'Goal set: ship the smoke test', })).toBe(true) }) it('keeps negative /goal command output visible as a goal message event', () => { expect(runGoalCommand(`goal-empty-${crypto.randomUUID()}`, '', 'No active goal.', 'user')).toEqual([ { type: 'system_notification', subtype: 'goal_event', message: 'No active goal.', data: { action: 'message', message: 'No active goal.' }, }, ]) }) it('does not turn unrelated local command output into a goal event', () => { const sessionId = `goal-unrelated-${crypto.randomUUID()}` expect(translateCliMessage({ type: 'system', subtype: 'local_command', content: '/status', }, sessionId)).toEqual([]) expect(translateCliMessage({ type: 'system', subtype: 'local_command_output', content: 'Goal: active', }, sessionId)).toEqual([ { type: 'content_start', blockType: 'text' }, { type: 'content_delta', text: 'Goal: active' }, ]) }) it('allows the current slash command lifecycle through the pre-turn mute gate', () => { const shouldForward = createCurrentTurnLocalCommandForwarder( parseSlashCommand('/goal ship the smoke test'), ) expect(shouldForward({ type: 'system', subtype: 'init', })).toBe(false) expect(shouldForward({ type: 'system', subtype: 'local_command', content: '/cost\n', })).toBe(false) expect(shouldForward({ type: 'system', subtype: 'local_command', content: '/goal\nship the smoke test', })).toBe(true) expect(shouldForward({ type: 'system', subtype: 'local_command', content: 'Goal set: ship the smoke test', })).toBe(true) expect(shouldForward({ type: 'system', subtype: 'local_command_output', content: 'late unrelated output', })).toBe(false) }) }) describe('WebSocket stream event translation', () => { it('keeps subagent parent linkage when later stream events omit the parent id', () => { const sessionId = `subagent-parent-${crypto.randomUUID()}` expect(translateCliMessage({ type: 'stream_event', parent_tool_use_id: 'agent-1', event: { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'read-1', name: 'Read' }, }, }, sessionId)).toEqual([ { type: 'content_start', blockType: 'tool_use', toolName: 'Read', toolUseId: 'read-1', parentToolUseId: 'agent-1', }, ]) expect(translateCliMessage({ type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"file_path":"src/App.tsx"}' }, }, }, sessionId)).toEqual([ { type: 'content_delta', toolInput: '{"file_path":"src/App.tsx"}' }, ]) expect(translateCliMessage({ type: 'stream_event', event: { type: 'content_block_stop', index: 0 }, }, sessionId)).toEqual([ { type: 'tool_use_complete', toolName: 'Read', toolUseId: 'read-1', input: { file_path: 'src/App.tsx' }, parentToolUseId: 'agent-1', }, ]) expect(translateCliMessage({ type: 'user', message: { content: [ { type: 'tool_result', tool_use_id: 'read-1', content: 'ok' }, ], }, }, sessionId)).toEqual([ { type: 'tool_result', toolUseId: 'read-1', content: 'ok', isError: false, parentToolUseId: 'agent-1', }, ]) }) it('keeps DeepSeek-style thinking blocks in thinking state until text starts', () => { const sessionId = `deepseek-thinking-${crypto.randomUUID()}` expect(translateCliMessage({ type: 'stream_event', event: { type: 'message_start' }, }, sessionId)).toEqual([ { type: 'status', state: 'thinking' }, ]) expect(translateCliMessage({ type: 'stream_event', event: { type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' }, }, }, sessionId)).toEqual([ { type: 'status', state: 'thinking', verb: 'Thinking' }, ]) expect(translateCliMessage({ type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'Let me think' }, }, }, sessionId)).toEqual([ { type: 'thinking', text: 'Let me think' }, ]) expect(translateCliMessage({ type: 'stream_event', event: { type: 'content_block_stop', index: 0 }, }, sessionId)).toEqual([]) expect(translateCliMessage({ type: 'stream_event', event: { type: 'content_block_start', index: 1, content_block: { type: 'text', text: '' }, }, }, sessionId)).toEqual([ { type: 'content_start', blockType: 'text' }, ]) }) })