import { describe, expect, it } from 'vitest'; import { getAvailableTeamProviderModelOptions, getAvailableTeamProviderModels, getTeamModelSelectionError, isTeamModelAvailableForUi, isTeamProviderModelVerificationPending, normalizeTeamModelForUi, } from '../teamModelAvailability'; import type { CliProviderStatus } from '@shared/types'; function createCodexProviderStatus( models: NonNullable['models'], options: { dynamicLaunch?: boolean } = {} ): CliProviderStatus { return { providerId: 'codex', displayName: 'Codex', supported: true, authenticated: true, authMethod: 'chatgpt', verificationState: 'verified', models: models.map((model) => model.launchModel), modelCatalog: { schemaVersion: 1, providerId: 'codex', source: 'app-server', status: 'ready', fetchedAt: '2026-04-21T00:00:00.000Z', staleAt: '2026-04-21T00:01:00.000Z', defaultModelId: models[0]?.id ?? null, defaultLaunchModel: models[0]?.launchModel ?? null, models, diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }, modelAvailability: [], runtimeCapabilities: { modelCatalog: { dynamic: options.dynamicLaunch === true, source: 'app-server', }, reasoningEffort: { supported: true, values: ['low', 'medium', 'high'], configPassthrough: false, }, }, canLoginFromUi: true, capabilities: { teamLaunch: true, oneShot: true, extensions: { plugins: { status: 'unsupported', ownership: 'shared', reason: null }, mcp: { status: 'supported', ownership: 'shared', reason: null }, skills: { status: 'supported', ownership: 'shared', reason: null }, apiKeys: { status: 'supported', ownership: 'shared', reason: null }, }, }, }; } function createAnthropicProviderStatus( models: NonNullable['models'] ): CliProviderStatus { return { providerId: 'anthropic', displayName: 'Anthropic', supported: true, authenticated: true, authMethod: 'claude.ai', verificationState: 'verified', models: ['opus', 'claude-opus-4-6', 'sonnet', 'haiku'], modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-models-api', status: 'ready', fetchedAt: '2026-04-21T00:00:00.000Z', staleAt: '2026-04-21T00:10:00.000Z', defaultModelId: 'opus[1m]', defaultLaunchModel: 'opus[1m]', models, diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }, modelAvailability: [], runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'anthropic-models-api', }, reasoningEffort: { supported: true, values: ['low', 'medium', 'high'], configPassthrough: false, }, }, canLoginFromUi: true, capabilities: { teamLaunch: true, oneShot: true, extensions: { plugins: { status: 'supported', ownership: 'shared', reason: null }, mcp: { status: 'supported', ownership: 'shared', reason: null }, skills: { status: 'supported', ownership: 'shared', reason: null }, apiKeys: { status: 'supported', ownership: 'shared', reason: null }, }, }, }; } describe('team model availability Codex catalog integration', () => { it('uses app-server catalog models with runtime-backed labels', () => { const providerStatus = createCodexProviderStatus( [ { id: 'gpt-5.5', launchModel: 'gpt-5.5', displayName: 'GPT-5.5', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'app-server', badgeLabel: '5.5', }, ], { dynamicLaunch: true } ); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.5']); expect(getAvailableTeamProviderModelOptions('codex', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'gpt-5.5', label: '5.5', badgeLabel: '5.5', availabilityStatus: 'available', availabilityReason: null, }, { value: 'gpt-5.3-codex-spark', label: '5.3 Codex Spark', badgeLabel: undefined, availabilityStatus: null, availabilityReason: null, }, { value: 'gpt-5.2-codex', label: '5.2 Codex', badgeLabel: undefined, availabilityStatus: null, availabilityReason: null, }, { value: 'gpt-5.1-codex-mini', label: '5.1 Codex Mini', badgeLabel: undefined, availabilityStatus: null, availabilityReason: null, }, { value: 'gpt-5.1-codex-max', label: '5.1 Codex Max', badgeLabel: undefined, availabilityStatus: null, availabilityReason: null, }, ]); }); it('allows app-server catalog models even when the runtime does not declare dynamic model launch', () => { const providerStatus = createCodexProviderStatus([ { id: 'gpt-5.5', launchModel: 'gpt-5.5', displayName: 'GPT-5.5', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'app-server', }, ]); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.5']); expect(getAvailableTeamProviderModelOptions('codex', providerStatus)[1]).toMatchObject({ value: 'gpt-5.5', label: '5.5', availabilityStatus: 'available', }); expect(getTeamModelSelectionError('codex', 'gpt-5.5', providerStatus)).toBeNull(); }); it('does not reject a selected Codex model while provider status is still loading', () => { const providerStatus = { ...createCodexProviderStatus([ { id: 'gpt-5.1-codex', launchModel: 'gpt-5.1-codex', displayName: 'GPT-5.1 Codex', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'static-fallback', }, ]), verificationState: 'unknown' as const, statusMessage: 'Checking...', }; expect(isTeamProviderModelVerificationPending('codex', providerStatus)).toBe(true); expect(getTeamModelSelectionError('codex', 'gpt-5.5', providerStatus)).toBeNull(); expect(normalizeTeamModelForUi('codex', 'gpt-5.5', providerStatus)).toBe('gpt-5.5'); }); it('orders GPT-5.5 first after the virtual default option', () => { const providerStatus = createCodexProviderStatus([ { id: 'gpt-5.4', launchModel: 'gpt-5.4', displayName: 'GPT-5.4', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'app-server', badgeLabel: '5.4', }, { id: 'gpt-5.5', launchModel: 'gpt-5.5', displayName: 'GPT-5.5', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'app-server', badgeLabel: '5.5', }, { id: 'gpt-5.2', launchModel: 'gpt-5.2', displayName: 'GPT-5.2', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'app-server', badgeLabel: '5.2', }, ]); expect( getAvailableTeamProviderModelOptions('codex', providerStatus).map((model) => model.value) ).toEqual([ '', 'gpt-5.5', 'gpt-5.4', 'gpt-5.3-codex-spark', 'gpt-5.2', 'gpt-5.2-codex', 'gpt-5.1-codex-mini', 'gpt-5.1-codex-max', ]); }); it('keeps existing disabled model policy on top of the dynamic catalog', () => { const providerStatus = createCodexProviderStatus([ { id: 'gpt-5.3-codex-spark', launchModel: 'gpt-5.3-codex-spark', displayName: 'GPT-5.3 Codex Spark', hidden: false, supportedReasoningEfforts: ['high'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'app-server', }, { id: 'gpt-5.4', launchModel: 'gpt-5.4', displayName: 'GPT-5.4', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'app-server', }, ]); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']); }); it('keeps the curated Anthropic picker surface while using runtime-backed labels', () => { const providerStatus = createAnthropicProviderStatus([ { id: 'opus', launchModel: 'opus', displayName: 'Opus 4.8', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'anthropic-models-api', badgeLabel: 'Opus 4.8', }, { id: 'opus[1m]', launchModel: 'opus[1m]', displayName: 'Opus 4.8 (1M)', hidden: true, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'anthropic-models-api', }, { id: 'claude-opus-4-6', launchModel: 'claude-opus-4-6', displayName: 'Opus 4.6', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'anthropic-models-api', badgeLabel: 'Opus 4.6', }, { id: 'sonnet', launchModel: 'sonnet', displayName: 'Sonnet 4.7', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'anthropic-models-api', badgeLabel: 'Sonnet 4.7', }, { id: 'haiku', launchModel: 'haiku', displayName: 'Haiku 4.6', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'anthropic-models-api', badgeLabel: 'Haiku 4.6', }, { id: 'claude-sonnet-4-6[1m]', launchModel: 'claude-sonnet-4-6[1m]', displayName: 'Sonnet 4.6 (1M)', hidden: true, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'static-fallback', }, ]); expect(getAvailableTeamProviderModels('anthropic', providerStatus)).toEqual([ 'haiku', 'opus', 'claude-opus-4-6', 'sonnet', ]); expect(getAvailableTeamProviderModelOptions('anthropic', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default', availabilityStatus: undefined, availabilityReason: undefined, }, { value: 'opus', label: 'Opus 4.8', badgeLabel: 'Opus 4.8', availabilityStatus: 'available', availabilityReason: null, }, { value: 'claude-opus-4-7', label: 'Opus 4.7', badgeLabel: 'Opus 4.7', availabilityStatus: null, availabilityReason: null, }, { value: 'claude-opus-4-6', label: 'Opus 4.6', badgeLabel: 'Opus 4.6', availabilityStatus: 'available', availabilityReason: null, }, { value: 'sonnet', label: 'Sonnet 4.7', badgeLabel: 'Sonnet 4.7', availabilityStatus: 'available', availabilityReason: null, }, { value: 'haiku', label: 'Haiku 4.6', badgeLabel: 'Haiku 4.6', availabilityStatus: 'available', availabilityReason: null, }, ]); }); it('keeps persisted hidden Anthropic compatibility values valid when runtime catalog supplies them', () => { const providerStatus = createAnthropicProviderStatus([ { id: 'claude-sonnet-4-6[1m]', launchModel: 'claude-sonnet-4-6[1m]', displayName: 'Sonnet 4.6 (1M)', hidden: true, supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: null, inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'static-fallback', }, ]); expect(isTeamModelAvailableForUi('anthropic', 'claude-sonnet-4-6[1m]', providerStatus)).toBe( true ); expect(normalizeTeamModelForUi('anthropic', 'claude-sonnet-4-6[1m]', providerStatus)).toBe( 'claude-sonnet-4-6[1m]' ); expect(getTeamModelSelectionError('anthropic', 'claude-sonnet-4-6[1m]', providerStatus)).toBe( null ); }); });