import { describe, it, expect } from 'vitest'; import { partsFromAssistantMessage, partsFromToolMessage } from '../inference/parts.js'; import type { ToolCall, ToolResult } from '../../types/api.js'; describe('partsFromAssistantMessage', () => { it('emits one text part for content-only assistant', () => { const parts = partsFromAssistantMessage({ content: 'hello world', tool_calls: null }); expect(parts).toHaveLength(1); expect(parts[0]).toEqual({ sequence: 0, kind: 'text', payload: { text: 'hello world' }, }); }); it('emits one tool_call part for empty-content + single tool_call', () => { const tc: ToolCall = { id: 'call_1', name: 'view_file', args: { path: 'src/a.ts' } }; const parts = partsFromAssistantMessage({ content: '', tool_calls: [tc] }); expect(parts).toHaveLength(1); expect(parts[0]).toEqual({ sequence: 0, kind: 'tool_call', payload: { id: 'call_1', name: 'view_file', args: { path: 'src/a.ts' } }, }); }); it('emits text then tool_call parts in order when both present', () => { const tc: ToolCall = { id: 'call_2', name: 'grep', args: { pattern: 'foo' } }; const parts = partsFromAssistantMessage({ content: 'let me search', tool_calls: [tc] }); expect(parts.map((p) => [p.sequence, p.kind])).toEqual([ [0, 'text'], [1, 'tool_call'], ]); }); it('preserves tool_call order with multiple calls', () => { const calls: ToolCall[] = [ { id: 'a', name: 'list_dir', args: { path: '.' } }, { id: 'b', name: 'view_file', args: { path: 'x.ts' } }, { id: 'c', name: 'grep', args: { pattern: 'y' } }, ]; const parts = partsFromAssistantMessage({ content: '', tool_calls: calls }); expect(parts).toHaveLength(3); expect(parts.map((p) => p.payload)).toEqual([ { id: 'a', name: 'list_dir', args: { path: '.' } }, { id: 'b', name: 'view_file', args: { path: 'x.ts' } }, { id: 'c', name: 'grep', args: { pattern: 'y' } }, ]); expect(parts.map((p) => p.sequence)).toEqual([0, 1, 2]); }); it('returns empty array for empty content + null tool_calls', () => { expect(partsFromAssistantMessage({ content: '', tool_calls: null })).toEqual([]); }); it('v1.13.1-C: reasoning lands at sequence 0 before text + tool_calls', () => { const tc: ToolCall = { id: 'call_r', name: 'view_file', args: { path: 'x.ts' } }; const parts = partsFromAssistantMessage({ content: 'inspecting now', tool_calls: [tc], reasoning: 'user asked about x.ts; I should view it', }); expect(parts.map((p) => [p.sequence, p.kind])).toEqual([ [0, 'reasoning'], [1, 'text'], [2, 'tool_call'], ]); expect(parts[0]!.payload).toEqual({ text: 'user asked about x.ts; I should view it', }); }); it('v1.13.1-C: reasoning + empty content + tool_calls preserves seq 0 reasoning', () => { const tc: ToolCall = { id: 'call_r2', name: 'grep', args: { pattern: 'foo' } }; const parts = partsFromAssistantMessage({ content: '', tool_calls: [tc], reasoning: 'jumping straight to grep', }); expect(parts.map((p) => [p.sequence, p.kind])).toEqual([ [0, 'reasoning'], [1, 'tool_call'], ]); }); }); describe('partsFromToolMessage', () => { it('emits a single tool_result part at sequence 0', () => { const tr: ToolResult = { tool_call_id: 'call_1', output: { contents: 'console.log(1)' }, truncated: false, }; const parts = partsFromToolMessage({ tool_results: tr }); expect(parts).toHaveLength(1); expect(parts[0]).toEqual({ sequence: 0, kind: 'tool_result', payload: { tool_call_id: 'call_1', output: { contents: 'console.log(1)' }, truncated: false, }, }); }); it('includes error in payload when present', () => { const tr: ToolResult = { tool_call_id: 'call_2', output: null, truncated: false, error: 'permission denied', }; const parts = partsFromToolMessage({ tool_results: tr }); expect(parts[0]!.payload).toMatchObject({ error: 'permission denied' }); }); it('returns empty array when tool_results is null', () => { expect(partsFromToolMessage({ tool_results: null })).toEqual([]); }); });