import { describe, expect, it } from 'vitest'; import { applyStreamingPartDeltaToMessage, normalizeOpencodeSessionMessages } from '@/lib/opencode-message'; describe('opencode message normalization', () => { it('keeps reasoning deltas as thinking content when the full part has not arrived yet', () => { const message = applyStreamingPartDeltaToMessage(null, { sessionID: 'ses_1', messageID: 'msg_assistant', partID: 'part_reasoning', field: 'text', delta: 'Reviewing the project flow.', part: { id: 'part_reasoning', type: 'reasoning', }, }); expect(message).toEqual(expect.objectContaining({ id: 'msg_assistant', role: 'assistant', content: [ { type: 'thinking', id: 'part_reasoning', thinking: 'Reviewing the project flow.', }, ], })); }); it('normalizes provider reasoning, reasoning_content, and reasoning_text aliases', () => { const messages = normalizeOpencodeSessionMessages([{ id: 'msg_reasoning_aliases', role: 'assistant', reasoning: 'first', reasoning_content: 'second', content: [ { type: 'reasoning', reasoning_text: 'third' }, { type: 'text', text: 'answer' }, ], }]); expect(messages[0]?.content).toEqual([ { type: 'thinking', thinking: 'first' }, { type: 'thinking', thinking: 'third' }, { type: 'text', text: 'answer' }, ]); }); it('surfaces a completed assistant message with no visible content as an error', () => { const messages = normalizeOpencodeSessionMessages([ { info: { id: 'msg_empty_assistant', role: 'assistant', sessionID: 'ses_1', time: { created: 1747040000000, completed: 1747040001000 }, }, parts: [ { id: 'step_start', type: 'step-start' }, { id: 'step_finish', type: 'step-finish' }, ], }, ]); expect(messages).toEqual([ expect.objectContaining({ id: 'msg_empty_assistant', role: 'assistant', isError: true, errorMessage: '模型没有返回任何内容。请重试,或切换到支持图片输入的模型。', content: [{ type: 'text', text: '模型没有返回任何内容。请重试,或切换到支持图片输入的模型。', }], }), ]); }); it('retains bounded runtime tool evidence without exposing raw tool state', () => { const messages = normalizeOpencodeSessionMessages([ { info: { id: 'msg_tool_evidence', role: 'assistant', time: { created: 1747040000000, completed: 1747040001000 }, }, parts: [ { id: 'tool_build', callID: 'call_build', type: 'tool', tool: 'bash', state: { status: 'completed', input: { command: 'pnpm build' }, output: 'build completed', metadata: { exit: 0 }, time: { start: 1747040000000, end: 1747040000500 }, }, }, ], }, ]); expect(messages[0]?._toolEvidence).toEqual([ expect.objectContaining({ id: 'tool_build', toolCallId: 'call_build', name: 'bash', status: 'completed', inputSummary: expect.stringContaining('pnpm build'), exitCode: 0, }), ]); }); it('preserves bounded user runtime context and only the true synthetic bit', () => { const [message] = normalizeOpencodeSessionMessages([{ info: { id: 'msg_user', role: 'user', time: { created: 1_700_000_000_000 }, agent: 'game-development', model: { providerID: 'niancode-user-models', modelID: 'qwen3.7-plus', variant: 'high', }, system: 'SECRET SYSTEM PROMPT', }, parts: [ { id: 'part_visible', type: 'text', text: 'Visible request', synthetic: false }, { id: 'part_hidden', type: 'text', text: 'Injected context', synthetic: true }, ], }]); expect(message?._runtimeContext).toEqual({ agent: 'game-development', model: 'niancode-user-models/qwen3.7-plus', variant: 'high', }); expect(message?.content).toEqual([ { id: 'part_visible', type: 'text', text: 'Visible request' }, { id: 'part_hidden', type: 'text', text: 'Injected context', synthetic: true }, ]); expect(JSON.stringify(message)).not.toContain('SECRET SYSTEM PROMPT'); }); it('drops oversized runtime identifiers instead of retaining arbitrary metadata', () => { const [message] = normalizeOpencodeSessionMessages([{ info: { id: 'msg_user', role: 'user', time: { created: 1_700_000_000_000 }, agent: 'a'.repeat(200), model: { providerID: 'provider', modelID: 'model', variant: 'v'.repeat(200), }, }, parts: [{ type: 'text', text: 'Hello' }], }]); expect(message?._runtimeContext).toEqual({ model: 'provider/model' }); }); it('renders only a persisted command invocation and its whitelisted context', () => { const [message] = normalizeOpencodeSessionMessages([{ info: { id: 'msg_command', role: 'user', time: { created: 1_700_000_000_000 }, agent: 'game-development', model: { providerID: 'niancode-user-models', modelID: 'qwen3.7-plus', }, }, parts: [ { id: 'prt_invocation', type: 'command-invocation', command: 'Review', arguments: ' raw arguments ', contextPartIDs: ['prt_context', 'prt_image'], }, { id: 'prt_template', type: 'text', text: 'PRIVATE EXPANDED TEMPLATE', synthetic: true, }, { id: 'prt_context', type: 'text', text: 'Bounded selected path context', synthetic: true, }, { id: 'prt_image', type: 'file', mime: 'image/webp', filename: 'screen.webp', url: 'data:image/webp;base64,QUFB', }, { id: 'prt_plugin', type: 'text', text: 'PRIVATE PLUGIN TEXT', synthetic: true, }, ], }]); expect(message?.content?.[0]).toEqual({ id: 'prt_invocation', type: 'text', text: '/Review raw arguments ', }); expect(JSON.stringify(message)).toContain('Bounded selected path context'); expect(message?._attachedFiles ?? []).toHaveLength(1); expect(JSON.stringify(message)).not.toContain('PRIVATE EXPANDED TEMPLATE'); expect(JSON.stringify(message)).not.toContain('PRIVATE PLUGIN TEXT'); expect(JSON.stringify(message)).not.toContain('contextPartIDs'); }); it('fails closed when a command invocation marker is malformed or duplicated', () => { const messages = normalizeOpencodeSessionMessages([ { info: { id: 'msg_malformed', role: 'user', time: { created: 1_700_000_000_000 }, agent: 'game-development', model: { providerID: 'provider', modelID: 'model' }, }, parts: [ { id: 'prt_bad', type: 'command-invocation', command: '', arguments: '', contextPartIDs: ['not-a-part-id'], }, { id: 'prt_template', type: 'text', text: 'PRIVATE MALFORMED TEMPLATE', synthetic: true }, ], }, { info: { id: 'msg_duplicated', role: 'user', time: { created: 1_700_000_000_001 }, }, parts: [ { id: 'prt_first', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_context'], }, { id: 'prt_second', type: 'command-invocation', command: 'Explain', arguments: '', contextPartIDs: [], }, { id: 'prt_context', type: 'text', text: 'PRIVATE DUPLICATED TEMPLATE', synthetic: true, }, ], }, { info: { id: 'msg_control', role: 'user', time: { created: 1_700_000_000_002 }, }, parts: [ { id: 'prt_control', type: 'command-invocation', command: 'Review\u0000hidden', arguments: '', contextPartIDs: ['prt_control_context'], }, { id: 'prt_control_context', type: 'text', text: 'PRIVATE CONTROL TEMPLATE', synthetic: true, }, ], }, { info: { id: 'msg_invalid_marker_id', role: 'user', time: { created: 1_700_000_000_003 }, }, parts: [ { id: 'arbitrary-renderer-id', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_invalid_id_context'], }, { id: 'prt_invalid_id_context', type: 'text', text: 'PRIVATE INVALID ID TEMPLATE', synthetic: true, }, ], }, ]); expect(messages).toHaveLength(4); for (const message of messages) { expect(message.content).toEqual([{ type: 'text', text: '[Project command metadata unavailable]', }]); expect(message._attachedFiles ?? []).toEqual([]); expect(JSON.stringify(message)).not.toContain('PRIVATE'); } }); it('fails closed for command markers on every non-user role', () => { const messages = normalizeOpencodeSessionMessages( ['assistant', 'system', 'unknown-runtime-role'].map((role, index) => ({ info: { id: `msg_non_user_${index}`, role, time: { created: 1_700_000_001_000 + index }, }, parts: [ { id: `prt_marker_${index}`, type: 'command-invocation', command: 'Review', arguments: '--private', contextPartIDs: [`prt_private_${index}`, `prt_file_${index}`, `prt_tool_${index}`], }, { id: `prt_private_${index}`, type: 'text', text: `PRIVATE_ROLE_${index}`, synthetic: true, }, { id: `prt_file_${index}`, type: 'file', mime: 'image/png', filename: 'private.png', url: 'data:image/png;base64,QUFB', }, { id: `prt_tool_${index}`, type: 'tool', tool: 'bash', state: { input: { command: 'print-private-data' } }, }, ], })), ); expect(messages).toHaveLength(3); for (const message of messages) { expect(message.content).toEqual([{ type: 'text', text: '[Project command metadata unavailable]', }]); expect(message._attachedFiles ?? []).toEqual([]); expect(message._toolEvidence ?? []).toEqual([]); expect(JSON.stringify(message)).not.toContain('PRIVATE_ROLE'); expect(JSON.stringify(message)).not.toContain('print-private-data'); expect(JSON.stringify(message)).not.toContain('--private'); } }); it('never renders tool or reasoning parts referenced by a valid user command marker', () => { const [message] = normalizeOpencodeSessionMessages([{ info: { id: 'msg_user_unsafe_context', role: 'user', time: { created: 1_700_000_001_500 }, }, parts: [ { id: 'prt_user_unsafe_marker', type: 'command-invocation', command: 'Review', arguments: 'safe arguments', contextPartIDs: [ 'prt_user_safe_text', 'prt_user_private_reasoning', 'prt_user_private_tool', ], }, { id: 'prt_user_safe_text', type: 'text', text: 'Visible selected context', synthetic: true, }, { id: 'prt_user_private_reasoning', type: 'reasoning', text: 'PRIVATE_REASONING', }, { id: 'prt_user_private_tool', callID: 'call_user_private_tool', type: 'tool', tool: 'bash', state: { status: 'completed', input: { command: 'PRIVATE_TOOL_INPUT' }, }, }, ], }]); expect(message?.content).toEqual([ { id: 'prt_user_unsafe_marker', type: 'text', text: '/Review safe arguments', }, { id: 'prt_user_safe_text', type: 'text', text: 'Visible selected context', synthetic: true, }, ]); expect(message?._toolEvidence ?? []).toEqual([]); expect(JSON.stringify(message)).not.toContain('PRIVATE_REASONING'); expect(JSON.stringify(message)).not.toContain('PRIVATE_TOOL_INPUT'); }); it('fails closed for malformed structured-like and legacy command marker shapes', () => { const messages = normalizeOpencodeSessionMessages([ { id: 'msg_missing_info', role: 'user', parts: [ { id: 'prt_missing_info', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_missing_info_private'], }, { id: 'prt_missing_info_private', type: 'text', text: 'PRIVATE_MISSING_INFO', synthetic: true, }, ], }, { info: { id: 'msg_non_array_parts', role: 'user', time: { created: 1_700_000_002_000 }, }, parts: { marker: { id: 'prt_non_array', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_non_array_private'], }, privatePart: { id: 'prt_non_array_private', type: 'text', text: 'PRIVATE_NON_ARRAY_PARTS', synthetic: true, }, }, }, { id: 'msg_legacy_array', role: 'user', content: [ { id: 'prt_legacy_marker', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_legacy_private'], }, { id: 'prt_legacy_private', type: 'text', text: 'PRIVATE_LEGACY_ARRAY', synthetic: true, }, ], }, { id: 'msg_array_info', info: [], parts: [ { id: 'prt_array_info_marker', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_array_info_private'], }, { id: 'prt_array_info_private', type: 'text', text: 'PRIVATE_ARRAY_INFO', synthetic: true, }, ], }, { id: 'msg_ordinary_legacy', role: 'assistant', content: 'Ordinary legacy content', }, ]); expect(messages).toHaveLength(5); for (const message of messages.slice(0, 4)) { expect(message.content).toEqual([{ type: 'text', text: '[Project command metadata unavailable]', }]); expect(message._attachedFiles ?? []).toEqual([]); expect(message._toolEvidence ?? []).toEqual([]); expect(JSON.stringify(message)).not.toContain('PRIVATE'); expect(JSON.stringify(message)).not.toContain('command-invocation'); } expect(messages[4]?.content).toEqual([{ type: 'text', text: 'Ordinary legacy content', }]); }); it('preserves legacy normalization for structured-like records without a command marker', () => { const messages = normalizeOpencodeSessionMessages([ { id: 'msg_legacy_info', role: 'assistant', text: 'Legacy info content', info: { source: 'legacy-adapter' }, }, { id: 'msg_legacy_parts', role: 'assistant', text: 'Legacy parts content', parts: { source: 'legacy-adapter' }, }, ]); expect(messages).toEqual([ { id: 'msg_legacy_info', role: 'assistant', timestamp: undefined, content: [{ type: 'text', text: 'Legacy info content' }], }, { id: 'msg_legacy_parts', role: 'assistant', timestamp: undefined, content: [{ type: 'text', text: 'Legacy parts content' }], }, ]); }); it('requires string arguments and enforces command selector bounds', () => { const invalidArguments: unknown[] = [undefined, null, 42, { private: true }]; const messages = normalizeOpencodeSessionMessages([ ...invalidArguments.map((argumentsValue, index) => ({ info: { id: `msg_bad_arguments_${index}`, role: 'user', time: { created: 1_700_000_003_000 + index }, }, parts: [{ id: `prt_bad_arguments_${index}`, type: 'command-invocation', command: 'Review', ...(argumentsValue === undefined ? {} : { arguments: argumentsValue }), contextPartIDs: [], }], })), { info: { id: 'msg_whitespace_command', role: 'user' }, parts: [{ id: 'prt_whitespace_command', type: 'command-invocation', command: ' Review', arguments: '', contextPartIDs: [], }], }, { info: { id: 'msg_leading_slash_command', role: 'user' }, parts: [{ id: 'prt_leading_slash_command', type: 'command-invocation', command: '/Review', arguments: '', contextPartIDs: [], }], }, { info: { id: 'msg_oversized_arguments', role: 'user' }, parts: [{ id: 'prt_oversized_arguments', type: 'command-invocation', command: 'Review', arguments: 'a'.repeat(8_193), contextPartIDs: [], }], }, { info: { id: 'msg_too_many_context_ids', role: 'user' }, parts: [{ id: 'prt_too_many_context_ids', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: Array.from({ length: 65 }, (_, index) => `prt_context_${index}`), }], }, ]); expect(messages).toHaveLength(8); for (const message of messages) { expect(message.content).toEqual([{ type: 'text', text: '[Project command metadata unavailable]', }]); expect(message._attachedFiles ?? []).toEqual([]); } }); it('does not mistake a large ordinary tool payload for command metadata', () => { const [message] = normalizeOpencodeSessionMessages([{ info: { id: 'msg_large_tool_payload', role: 'assistant', time: { created: 1_700_000_005_000 }, }, parts: [ { id: 'prt_visible_result', type: 'text', text: 'Visible assistant result', }, { id: 'prt_large_tool', callID: 'call_large_tool', type: 'tool', tool: 'inspect', state: { status: 'completed', input: Array.from({ length: 4_100 }, (_, index) => ({ index })), output: 'Inspection complete', }, }, ], }]); expect(message?.content).toEqual([ { id: 'prt_visible_result', type: 'text', text: 'Visible assistant result', }, expect.objectContaining({ id: 'call_large_tool', type: 'tool_use', name: 'inspect', }), ]); expect(message?._toolEvidence).toEqual([ expect.objectContaining({ id: 'prt_large_tool', toolCallId: 'call_large_tool', name: 'inspect', status: 'completed', }), ]); }); });