import { afterEach, describe, expect, test } from 'bun:test' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { codexStreamToAnthropic, convertAnthropicMessagesToResponsesInput, convertCodexResponseToAnthropicMessage, convertToolsToResponsesTools, } from './codexShim.js' import { resolveCodexApiCredentials, resolveProviderRequest, } from './providerConfig.js' const tempDirs: string[] = [] afterEach(() => { while (tempDirs.length > 0) { const dir = tempDirs.pop() if (dir) rmSync(dir, { recursive: true, force: true }) } }) function createTempAuthJson(payload: Record): string { const dir = mkdtempSync(join(tmpdir(), 'openclaude-codex-')) tempDirs.push(dir) const authPath = join(dir, 'auth.json') writeFileSync(authPath, JSON.stringify(payload), 'utf8') return authPath } async function collectStreamEventTypes(responseText: string): Promise { const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(responseText)) controller.close() }, }) const events: string[] = [] for await (const event of codexStreamToAnthropic(new Response(stream), 'gpt-5.4')) { events.push(event.type) } return events } describe('Codex provider config', () => { test('resolves codexplan alias to Codex transport with reasoning', () => { const resolved = resolveProviderRequest({ model: 'codexplan' }) expect(resolved.transport).toBe('codex_responses') expect(resolved.resolvedModel).toBe('gpt-5.4') expect(resolved.reasoning).toEqual({ effort: 'high' }) }) test('loads Codex credentials from auth.json fallback', () => { const authPath = createTempAuthJson({ tokens: { access_token: 'header.payload.signature', account_id: 'acct_test', }, }) const credentials = resolveCodexApiCredentials({ CODEX_AUTH_JSON_PATH: authPath, } as NodeJS.ProcessEnv) expect(credentials.apiKey).toBe('header.payload.signature') expect(credentials.accountId).toBe('acct_test') expect(credentials.source).toBe('auth.json') }) }) describe('Codex request translation', () => { test('normalizes optional parameters into strict Responses schemas', () => { const tools = convertToolsToResponsesTools([ { name: 'Agent', description: 'Spawn a sub-agent', input_schema: { type: 'object', properties: { description: { type: 'string' }, prompt: { type: 'string' }, subagent_type: { type: 'string' }, }, required: ['description', 'prompt'], additionalProperties: false, }, }, ]) expect(tools).toEqual([ { type: 'function', name: 'Agent', description: 'Spawn a sub-agent', parameters: { type: 'object', properties: { description: { type: 'string' }, prompt: { type: 'string' }, subagent_type: { type: 'string' }, }, required: ['description', 'prompt', 'subagent_type'], additionalProperties: false, }, strict: true, }, ]) }) test('keeps strict mode for tools whose schema already matches Responses requirements', () => { const tools = convertToolsToResponsesTools([ { name: 'Ping', description: 'Ping tool', input_schema: { type: 'object', properties: { value: { type: 'string' }, }, required: ['value'], additionalProperties: false, }, }, ]) expect(tools).toEqual([ { type: 'function', name: 'Ping', description: 'Ping tool', parameters: { type: 'object', properties: { value: { type: 'string' }, }, required: ['value'], additionalProperties: false, }, strict: true, }, ]) }) test('converts assistant tool use and user tool result into Responses items', () => { const items = convertAnthropicMessagesToResponsesInput([ { role: 'assistant', content: [ { type: 'text', text: 'Working...' }, { type: 'tool_use', id: 'call_123', name: 'search', input: { q: 'x' } }, ], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_123', content: 'done' }, ], }, ]) expect(items).toEqual([ { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Working...' }], }, { type: 'function_call', id: 'fc_123', call_id: 'call_123', name: 'search', arguments: '{"q":"x"}', }, { type: 'function_call_output', call_id: 'call_123', output: 'done', }, ]) }) test('converts completed Codex tool response into Anthropic message', () => { const message = convertCodexResponseToAnthropicMessage( { id: 'resp_1', model: 'gpt-5.3-codex-spark', output: [ { type: 'function_call', id: 'fc_1', call_id: 'call_1', name: 'ping', arguments: '{"value":"ping"}', }, ], usage: { input_tokens: 12, output_tokens: 4 }, }, 'gpt-5.3-codex-spark', ) expect(message.stop_reason).toBe('tool_use') expect(message.content).toEqual([ { type: 'tool_use', id: 'call_1', name: 'ping', input: { value: 'ping' }, }, ]) }) test('translates Codex SSE text stream into Anthropic events', async () => { const responseText = [ 'event: response.output_item.added', 'data: {"type":"response.output_item.added","item":{"id":"msg_1","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":0}', '', 'event: response.content_part.added', 'data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_1","output_index":0,"part":{"type":"output_text","text":""},"sequence_number":1}', '', 'event: response.output_text.delta', 'data: {"type":"response.output_text.delta","content_index":0,"delta":"ok","item_id":"msg_1","output_index":0,"sequence_number":2}', '', 'event: response.output_item.done', 'data: {"type":"response.output_item.done","item":{"id":"msg_1","type":"message","status":"completed","content":[{"type":"output_text","text":"ok"}],"role":"assistant"},"output_index":0,"sequence_number":3}', '', 'event: response.completed', 'data: {"type":"response.completed","response":{"id":"resp_1","status":"completed","model":"gpt-5.4","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":2,"output_tokens":1}},"sequence_number":4}', '', ].join('\n') const eventTypes = await collectStreamEventTypes(responseText) expect(eventTypes).toEqual([ 'message_start', 'content_block_start', 'content_block_delta', 'content_block_stop', 'message_delta', 'message_stop', ]) }) })