test(providers): add Codex and Ollama message adapter tests
This commit is contained in:
parent
3e34fda59a
commit
8309896e7d
2 changed files with 402 additions and 0 deletions
249
v2/src/lib/adapters/codex-messages.test.ts
Normal file
249
v2/src/lib/adapters/codex-messages.test.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { adaptCodexMessage } from './codex-messages';
|
||||
|
||||
describe('adaptCodexMessage', () => {
|
||||
describe('thread.started', () => {
|
||||
it('maps to init message with thread_id as sessionId', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'thread.started',
|
||||
thread_id: 'thread-abc-123',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('init');
|
||||
expect((result[0].content as any).sessionId).toBe('thread-abc-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('turn.started', () => {
|
||||
it('maps to status message', () => {
|
||||
const result = adaptCodexMessage({ type: 'turn.started' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('status');
|
||||
expect((result[0].content as any).subtype).toBe('turn_started');
|
||||
});
|
||||
});
|
||||
|
||||
describe('turn.completed', () => {
|
||||
it('maps to cost message with token usage', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'turn.completed',
|
||||
usage: { input_tokens: 1000, output_tokens: 200, cached_input_tokens: 800 },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('cost');
|
||||
const content = result[0].content as any;
|
||||
expect(content.inputTokens).toBe(1000);
|
||||
expect(content.outputTokens).toBe(200);
|
||||
expect(content.totalCostUsd).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('turn.failed', () => {
|
||||
it('maps to error message', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'turn.failed',
|
||||
error: { message: 'Rate limit exceeded' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('error');
|
||||
expect((result[0].content as any).message).toBe('Rate limit exceeded');
|
||||
});
|
||||
});
|
||||
|
||||
describe('item.completed — agent_message', () => {
|
||||
it('maps to text message', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: { id: 'item_3', type: 'agent_message', text: 'Done. I updated foo.ts.' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('text');
|
||||
expect((result[0].content as any).text).toBe('Done. I updated foo.ts.');
|
||||
});
|
||||
|
||||
it('ignores item.started for agent_message', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.started',
|
||||
item: { type: 'agent_message', text: '' },
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('item.completed — reasoning', () => {
|
||||
it('maps to thinking message', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: { type: 'reasoning', text: 'Let me think about this...' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('thinking');
|
||||
expect((result[0].content as any).text).toBe('Let me think about this...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('item — command_execution', () => {
|
||||
it('maps item.started to tool_call', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.started',
|
||||
item: { id: 'item_1', type: 'command_execution', command: 'ls -la', status: 'in_progress' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('tool_call');
|
||||
expect((result[0].content as any).name).toBe('Bash');
|
||||
expect((result[0].content as any).input.command).toBe('ls -la');
|
||||
});
|
||||
|
||||
it('maps item.completed to tool_call + tool_result pair', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: {
|
||||
id: 'item_1',
|
||||
type: 'command_execution',
|
||||
command: 'ls -la',
|
||||
aggregated_output: 'total 48\ndrwxr-xr-x',
|
||||
exit_code: 0,
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].type).toBe('tool_call');
|
||||
expect(result[1].type).toBe('tool_result');
|
||||
expect((result[1].content as any).output).toBe('total 48\ndrwxr-xr-x');
|
||||
});
|
||||
|
||||
it('ignores item.updated for command_execution', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.updated',
|
||||
item: { type: 'command_execution', command: 'ls', status: 'in_progress' },
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('item.completed — file_change', () => {
|
||||
it('maps file changes to tool_call + tool_result pairs', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: {
|
||||
type: 'file_change',
|
||||
changes: [
|
||||
{ path: 'src/foo.ts', kind: 'update' },
|
||||
{ path: 'src/bar.ts', kind: 'add' },
|
||||
],
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0].type).toBe('tool_call');
|
||||
expect((result[0].content as any).name).toBe('Edit');
|
||||
expect(result[1].type).toBe('tool_result');
|
||||
expect(result[2].type).toBe('tool_call');
|
||||
expect((result[2].content as any).name).toBe('Write');
|
||||
});
|
||||
|
||||
it('maps delete to Bash tool name', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: {
|
||||
type: 'file_change',
|
||||
changes: [{ path: 'old.ts', kind: 'delete' }],
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect((result[0].content as any).name).toBe('Bash');
|
||||
});
|
||||
|
||||
it('returns empty for no changes', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: { type: 'file_change', changes: [], status: 'completed' },
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('item.completed — mcp_tool_call', () => {
|
||||
it('maps to tool_call + tool_result with server:tool name', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: {
|
||||
id: 'mcp_1',
|
||||
type: 'mcp_tool_call',
|
||||
server: 'filesystem',
|
||||
tool: 'read_file',
|
||||
arguments: { path: '/tmp/test.txt' },
|
||||
result: { content: 'file contents' },
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect((result[0].content as any).name).toBe('filesystem:read_file');
|
||||
expect((result[0].content as any).input.path).toBe('/tmp/test.txt');
|
||||
});
|
||||
|
||||
it('maps error result to error message in tool_result', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: {
|
||||
id: 'mcp_2',
|
||||
type: 'mcp_tool_call',
|
||||
server: 'fs',
|
||||
tool: 'write',
|
||||
arguments: {},
|
||||
error: { message: 'Permission denied' },
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect((result[1].content as any).output).toBe('Permission denied');
|
||||
});
|
||||
});
|
||||
|
||||
describe('item.completed — web_search', () => {
|
||||
it('maps to WebSearch tool_call', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: { id: 'ws_1', type: 'web_search', query: 'ollama api docs' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('tool_call');
|
||||
expect((result[0].content as any).name).toBe('WebSearch');
|
||||
expect((result[0].content as any).input.query).toBe('ollama api docs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('item — error', () => {
|
||||
it('maps to error message', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'item.completed',
|
||||
item: { type: 'error', message: 'Sandbox violation' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('error');
|
||||
expect((result[0].content as any).message).toBe('Sandbox violation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('top-level error', () => {
|
||||
it('maps to error message', () => {
|
||||
const result = adaptCodexMessage({
|
||||
type: 'error',
|
||||
message: 'Connection lost',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('error');
|
||||
expect((result[0].content as any).message).toBe('Connection lost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown event type', () => {
|
||||
it('maps to unknown message preserving raw data', () => {
|
||||
const result = adaptCodexMessage({ type: 'custom.event', data: 42 });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('unknown');
|
||||
expect((result[0].content as any).data).toBe(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
153
v2/src/lib/adapters/ollama-messages.test.ts
Normal file
153
v2/src/lib/adapters/ollama-messages.test.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { adaptOllamaMessage } from './ollama-messages';
|
||||
|
||||
describe('adaptOllamaMessage', () => {
|
||||
describe('system init', () => {
|
||||
it('maps to init message', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
session_id: 'sess-123',
|
||||
model: 'qwen3:8b',
|
||||
cwd: '/home/user/project',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('init');
|
||||
const content = result[0].content as any;
|
||||
expect(content.sessionId).toBe('sess-123');
|
||||
expect(content.model).toBe('qwen3:8b');
|
||||
expect(content.cwd).toBe('/home/user/project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('system status', () => {
|
||||
it('maps non-init subtypes to status message', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'system',
|
||||
subtype: 'model_loaded',
|
||||
status: 'Model loaded successfully',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('status');
|
||||
expect((result[0].content as any).subtype).toBe('model_loaded');
|
||||
expect((result[0].content as any).message).toBe('Model loaded successfully');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk — text content', () => {
|
||||
it('maps streaming text to text message', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: 'Hello world' },
|
||||
done: false,
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('text');
|
||||
expect((result[0].content as any).text).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('ignores empty content', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: '' },
|
||||
done: false,
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk — thinking content', () => {
|
||||
it('maps thinking field to thinking message', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: '', thinking: 'Let me reason about this...' },
|
||||
done: false,
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('thinking');
|
||||
expect((result[0].content as any).text).toBe('Let me reason about this...');
|
||||
});
|
||||
|
||||
it('emits both thinking and text when both present', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: 'Answer', thinking: 'Reasoning' },
|
||||
done: false,
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].type).toBe('thinking');
|
||||
expect(result[1].type).toBe('text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk — done with token counts', () => {
|
||||
it('maps final chunk to cost message', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: '' },
|
||||
done: true,
|
||||
done_reason: 'stop',
|
||||
prompt_eval_count: 500,
|
||||
eval_count: 120,
|
||||
eval_duration: 2_000_000_000,
|
||||
total_duration: 3_000_000_000,
|
||||
});
|
||||
// Should have cost message (no text since content is empty)
|
||||
const costMsg = result.find(m => m.type === 'cost');
|
||||
expect(costMsg).toBeDefined();
|
||||
const content = costMsg!.content as any;
|
||||
expect(content.inputTokens).toBe(500);
|
||||
expect(content.outputTokens).toBe(120);
|
||||
expect(content.durationMs).toBe(2000);
|
||||
expect(content.totalCostUsd).toBe(0);
|
||||
expect(content.isError).toBe(false);
|
||||
});
|
||||
|
||||
it('marks error done_reason as isError', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: '' },
|
||||
done: true,
|
||||
done_reason: 'error',
|
||||
prompt_eval_count: 0,
|
||||
eval_count: 0,
|
||||
});
|
||||
const costMsg = result.find(m => m.type === 'cost');
|
||||
expect(costMsg).toBeDefined();
|
||||
expect((costMsg!.content as any).isError).toBe(true);
|
||||
});
|
||||
|
||||
it('includes text + cost when final chunk has content', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'chunk',
|
||||
message: { role: 'assistant', content: '.' },
|
||||
done: true,
|
||||
done_reason: 'stop',
|
||||
prompt_eval_count: 10,
|
||||
eval_count: 5,
|
||||
});
|
||||
expect(result.some(m => m.type === 'text')).toBe(true);
|
||||
expect(result.some(m => m.type === 'cost')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error event', () => {
|
||||
it('maps to error message', () => {
|
||||
const result = adaptOllamaMessage({
|
||||
type: 'error',
|
||||
message: 'model not found',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('error');
|
||||
expect((result[0].content as any).message).toBe('model not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown event type', () => {
|
||||
it('maps to unknown message preserving raw data', () => {
|
||||
const result = adaptOllamaMessage({ type: 'something_else', data: 'test' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('unknown');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue