import { describe, expect, it, vi } from 'vitest' import type { CodexAppServerConnection } from './codex-app-server-connection' import { CodexAcquisitionWindow } from './codex-structured-acquisition-window' import { applyCodexStructuredSessionOption, readCodexStructuredSessionOptions, reportedCodexThreadOptions, restoredCodexSessionOptions } from './codex-structured-session-options' import type { CodexSession } from './codex-structured-session-state' function optionSession(request: CodexAppServerConnection['request']): CodexSession { return { connection: { pid: 1, closed: false, request, notify: () => {}, respond: () => {}, respondWithError: () => {}, close: async () => true }, ended: false, requestedClose: false, fence: 1, acquisitionGeneration: 'generation-1', threadId: 'thread-1', historyPath: null, prompts: new CodexAcquisitionWindow().prompts, options: new Map(), reportedOptions: { model: 'gpt-live', effort: 'high' }, turnIdWaiters: [], translator: null } } describe('structured Codex session options', () => { it('filters restored records to recognized turn options', () => { expect( Object.fromEntries( restoredCodexSessionOptions({ model: 'gpt-live', effort: 'high', threadId: 'thread-injected', input: 'input-injected' }) ) ).toEqual({ model: 'gpt-live', effort: 'high' }) }) it('hydrates paged provider models and their supported efforts', async () => { const request = vi.fn(async (_method: string, params?: Record) => params?.cursor ? { data: [ { model: 'gpt-second', displayName: 'GPT Second', description: 'Fast', hidden: false, supportedReasoningEfforts: [ { reasoningEffort: 'low', description: 'Quick reasoning' } ], defaultReasoningEffort: 'low', isDefault: false } ], nextCursor: null } : { data: [ { model: 'gpt-live', displayName: 'GPT Live', hidden: false, supportedReasoningEfforts: [ { reasoningEffort: 'medium', description: 'Balanced' }, { reasoningEffort: 'high', description: 'Deep reasoning' } ], defaultReasoningEffort: 'medium', isDefault: true } ], nextCursor: 'page-2' } ) await expect( readCodexStructuredSessionOptions({ connection: { request } as never, current: { model: 'gpt-live', effort: 'medium' } }) ).resolves.toEqual({ models: [ { id: 'gpt-live', label: 'GPT Live', isDefault: true, defaultEffort: 'medium', efforts: [ { value: 'medium', label: 'Medium', description: 'Balanced' }, { value: 'high', label: 'High', description: 'Deep reasoning' } ] }, { id: 'gpt-second', label: 'GPT Second', description: 'Fast', isDefault: false, defaultEffort: 'low', efforts: [{ value: 'low', label: 'Low', description: 'Quick reasoning' }] } ], current: { model: 'gpt-live', effort: 'medium' } }) expect(request).toHaveBeenNthCalledWith( 2, 'model/list', { limit: 100, includeHidden: false, cursor: 'page-2' }, { timeoutMs: undefined } ) }) it('hydrates current values from thread start or resume', () => { expect( reportedCodexThreadOptions({ threadId: 'thread-1', historyPath: null, model: 'gpt-live', effort: 'high' }) ).toEqual({ model: 'gpt-live', effort: 'high' }) }) it('reconciles an incompatible effort when only the model changes', async () => { const session = optionSession( vi.fn(async () => ({ data: [ { model: 'gpt-live', supportedReasoningEfforts: [{ reasoningEffort: 'high' }], defaultReasoningEffort: 'high' }, { model: 'gpt-fast', supportedReasoningEfforts: [{ reasoningEffort: 'low' }], defaultReasoningEffort: 'low' } ], nextCursor: null })) ) await expect( applyCodexStructuredSessionOption(session, 'model', 'gpt-fast', undefined) ).resolves.toEqual({ model: 'gpt-fast', effort: 'low' }) }) it('rejects values absent from the provider catalog', async () => { const session = optionSession( vi.fn(async () => ({ data: [{ model: 'gpt-live', supportedReasoningEfforts: [] }], nextCursor: null })) ) await expect( applyCodexStructuredSessionOption(session, 'model', 'not-entitled', undefined) ).rejects.toThrow('does not offer model not-entitled') await expect( applyCodexStructuredSessionOption(session, 'effort', 'high', undefined) ).rejects.toThrow('does not support high') }) })