import { getAvailableTeamProviderModelOptions, getAvailableTeamProviderModels, getTeamModelSelectionError, GPT_5_1_CODEX_MINI_UI_DISABLED_REASON, GPT_5_2_CODEX_UI_DISABLED_REASON, GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON, normalizeTeamModelForUi, type TeamModelRuntimeProviderStatus, } from '@renderer/utils/teamModelAvailability'; import { describe, expect, it } from 'vitest'; function createCodexProviderStatus( models: string[], overrides: Partial = {} ): TeamModelRuntimeProviderStatus { return { providerId: 'codex', models, authMethod: 'api_key', backend: { kind: 'codex-native', label: 'Codex native', endpointLabel: 'codex exec --json', }, authenticated: true, supported: true, modelVerificationState: 'idle', modelAvailability: [], ...overrides, }; } function createOpenCodeProviderStatus( models: string[], overrides: Partial = {} ): TeamModelRuntimeProviderStatus { return { providerId: 'opencode', models, authMethod: 'opencode_managed', backend: { kind: 'opencode-cli', label: 'OpenCode CLI', }, authenticated: true, supported: true, modelVerificationState: 'idle', modelAvailability: [], ...overrides, }; } function createAnthropicCompatibleProviderStatus( overrides: Partial = {} ): TeamModelRuntimeProviderStatus { return { providerId: 'anthropic', models: [], authMethod: 'api_key', backend: null, authenticated: true, supported: true, modelVerificationState: 'idle', modelAvailability: [], connection: { supportsOAuth: true, supportsApiKey: true, configurableAuthModes: ['auto', 'oauth', 'api_key'], configuredAuthMode: 'auto', apiKeyConfigured: false, apiKeySource: null, apiKeySourceLabel: null, compatibleEndpoint: { enabled: true, baseUrl: 'http://localhost:1234', tokenConfigured: true, tokenSource: 'stored', tokenSourceLabel: 'Stored in app', }, }, ...overrides, }; } describe('teamModelAvailability', () => { it('uses runtime-reported Codex models as the source of truth', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.3-codex']); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual([ 'gpt-5.4', 'gpt-5.3-codex', ]); }); it('filters only the Codex models that remain UI-disabled on the native runtime path', () => { const providerStatus = createCodexProviderStatus([ 'gpt-5.4', 'gpt-5.3-codex-spark', 'gpt-5.2-codex', 'gpt-5.1-codex-mini', 'gpt-5.1-codex-max', ]); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual([ 'gpt-5.4', 'gpt-5.1-codex-max', ]); }); it('keeps 5.1 Codex Max available on the native runtime path', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.1-codex-max'], { authMethod: 'api_key', backend: { kind: 'codex-native', label: 'Codex native', endpointLabel: 'codex exec --json', }, }); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual([ 'gpt-5.4', 'gpt-5.1-codex-max', ]); }); it('hides 5.1 Codex Max on the ChatGPT subscription-backed path', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.1-codex-max'], { authMethod: 'chatgpt', backend: { kind: 'codex-native', label: 'Codex native', endpointLabel: 'codex exec --json', authMethodDetail: 'chatgpt', }, }); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']); expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-max', providerStatus)).toBe(''); expect(getTeamModelSelectionError('codex', 'gpt-5.1-codex-max', providerStatus)).toContain( 'Temporarily disabled for team agents - this model is not currently available on the Codex native runtime.' ); }); it('builds Codex model options from the runtime list plus disabled safety entries', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.3-codex']); expect(getAvailableTeamProviderModelOptions('codex', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'gpt-5.4', label: '5.4', badgeLabel: undefined, availabilityStatus: 'available', availabilityReason: null, }, { value: 'gpt-5.3-codex', label: '5.3 Codex', badgeLabel: undefined, 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, }, ]); }); it('treats runtime-reported unavailable models as non-selectable', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4'], { modelAvailability: [ { modelId: 'gpt-5.4', status: 'unavailable', reason: 'No access for this account', checkedAt: null, }, ], }); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual([]); expect(normalizeTeamModelForUi('codex', 'gpt-5.4', providerStatus)).toBe(''); expect(getTeamModelSelectionError('codex', 'gpt-5.4', providerStatus)).toContain( 'No access for this account' ); expect(getAvailableTeamProviderModelOptions('codex', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'gpt-5.4', label: '5.4', badgeLabel: undefined, availabilityStatus: 'unavailable', availabilityReason: 'No access for this account', }, { 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, }, ]); }); it('keeps OpenCode raw ids intact while exposing readable labels and source badges', () => { const providerStatus = createOpenCodeProviderStatus([ 'openai/gpt-5.4', 'openrouter/moonshotai/kimi-k2', 'opencode/big-pickle', ]); expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([ 'opencode/big-pickle', 'openai/gpt-5.4', 'openrouter/moonshotai/kimi-k2', ]); expect(getAvailableTeamProviderModelOptions('opencode', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'opencode/big-pickle', label: 'big-pickle', badgeLabel: 'OpenCode', availabilityStatus: 'available', availabilityReason: null, }, { value: 'openai/gpt-5.4', label: 'GPT-5.4', badgeLabel: 'OpenAI', availabilityStatus: 'available', availabilityReason: null, }, { value: 'openrouter/moonshotai/kimi-k2', label: 'moonshotai/kimi-k2', badgeLabel: 'OpenRouter', availabilityStatus: 'available', availabilityReason: null, }, ]); expect( normalizeTeamModelForUi('opencode', 'openrouter/moonshotai/kimi-k2', providerStatus) ).toBe('openrouter/moonshotai/kimi-k2'); }); it('uses the OpenCode model catalog when runtime models are summary-only', () => { const providerStatus = createOpenCodeProviderStatus(['opencode/big-pickle'], { modelCatalog: { schemaVersion: 1, providerId: 'opencode', source: 'app-server', status: 'ready', fetchedAt: '2026-05-12T00:00:00.000Z', staleAt: '2026-05-12T00:10:00.000Z', defaultModelId: 'opencode/big-pickle', defaultLaunchModel: 'opencode/big-pickle', models: [ { id: 'openai/gpt-5.4', launchModel: 'openai/gpt-5.4', displayName: 'openai/gpt-5.4', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: false, upgrade: false, source: 'app-server', badgeLabel: null, }, { id: 'opencode/big-pickle', launchModel: 'opencode/big-pickle', displayName: 'opencode/big-pickle', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: true, upgrade: false, source: 'app-server', badgeLabel: 'Free', }, { id: 'openrouter/hidden-model', launchModel: 'openrouter/hidden-model', displayName: 'openrouter/hidden-model', hidden: true, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: false, upgrade: false, source: 'app-server', badgeLabel: null, }, ], diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }, }); expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([ 'opencode/big-pickle', 'openai/gpt-5.4', ]); expect(getAvailableTeamProviderModelOptions('opencode', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'opencode/big-pickle', label: 'big-pickle', badgeLabel: 'OpenCode', availabilityStatus: 'available', availabilityReason: null, }, { value: 'openai/gpt-5.4', label: 'GPT-5.4', badgeLabel: 'OpenAI', availabilityStatus: 'available', availabilityReason: null, }, ]); expect(normalizeTeamModelForUi('opencode', 'openai/gpt-5.4', providerStatus)).toBe( 'openai/gpt-5.4' ); expect(getTeamModelSelectionError('opencode', 'openai/gpt-5.4', providerStatus)).toBeNull(); }); it('uses the OpenCode model catalog when runtime models are empty', () => { const providerStatus = createOpenCodeProviderStatus([], { modelCatalog: { schemaVersion: 1, providerId: 'opencode', source: 'app-server', status: 'ready', fetchedAt: '2026-05-12T00:00:00.000Z', staleAt: '2026-05-12T00:10:00.000Z', defaultModelId: 'opencode/big-pickle', defaultLaunchModel: 'opencode/big-pickle', models: [ { id: 'opencode/big-pickle', launchModel: 'opencode/big-pickle', displayName: 'opencode/big-pickle', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: true, upgrade: false, source: 'app-server', badgeLabel: 'Free', }, { id: 'openai/gpt-5.4', launchModel: 'openai/gpt-5.4', displayName: 'openai/gpt-5.4', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: false, upgrade: false, source: 'app-server', badgeLabel: null, }, ], diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }, }); expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([ 'opencode/big-pickle', 'openai/gpt-5.4', ]); expect( getAvailableTeamProviderModelOptions('opencode', providerStatus).map((option) => option.value) ).toEqual(['', 'opencode/big-pickle', 'openai/gpt-5.4']); }); it('reports OpenCode openai routes unavailable when OpenAI auth is invalid', () => { const providerStatus = createOpenCodeProviderStatus(['openai/gpt-5.4', 'opencode/big-pickle'], { statusMessage: 'OpenAI token invalid', detailMessage: 'OpenAI token refresh failed: 401', availableBackends: [ { id: 'openai', label: 'OpenAI', description: 'OpenAI route', selectable: false, recommended: false, available: false, state: 'authentication-required', statusMessage: 'Authentication required', detailMessage: 'Token refresh failed: 401', }, ], }); expect(getTeamModelSelectionError('opencode', 'openai/gpt-5.4', providerStatus)).toContain( 'OpenCode OpenAI provider authentication failed' ); expect( getTeamModelSelectionError('opencode', 'opencode/big-pickle', providerStatus) ).toBeNull(); }); it('clears stale Codex selections when runtime no longer reports that model', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.3-codex']); expect(normalizeTeamModelForUi('codex', 'gpt-5.2-codex', providerStatus)).toBe(''); expect(normalizeTeamModelForUi('codex', 'gpt-5.4', providerStatus)).toBe('gpt-5.4'); }); it('reports an explicit error when a Codex model is unsupported by the current runtime', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4', 'gpt-5.3-codex']); expect(getTeamModelSelectionError('codex', 'gpt-5.2-codex', providerStatus)).toContain( 'Temporarily disabled for team agents' ); expect(getTeamModelSelectionError('codex', 'gpt-5.4', providerStatus)).toBeNull(); }); it('does not raise a hard validation error while explicit Codex models are still loading', () => { expect(getTeamModelSelectionError('codex', 'gpt-5.4')).toBeNull(); expect(getTeamModelSelectionError('codex', '')).toBeNull(); }); it('keeps known Codex selections stable while the runtime is still on placeholder checking state', () => { const providerStatus = createCodexProviderStatus([], { authMethod: null, backend: null, authenticated: false, supported: false, verificationState: 'unknown', modelVerificationState: 'idle', statusMessage: 'Checking...', }); expect(normalizeTeamModelForUi('codex', 'gpt-5.4', providerStatus)).toBe('gpt-5.4'); expect(getTeamModelSelectionError('codex', 'gpt-5.4', providerStatus)).toBeNull(); expect(getAvailableTeamProviderModelOptions('codex', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'gpt-5.5', label: '5.5', badgeLabel: '5.5' }, { value: 'gpt-5.4', label: '5.4', badgeLabel: '5.4' }, { value: 'gpt-5.4-mini', label: '5.4 Mini', badgeLabel: '5.4-mini' }, { value: 'gpt-5.3-codex', label: '5.3 Codex', badgeLabel: '5.3-codex' }, { value: 'gpt-5.3-codex-spark', label: '5.3 Codex Spark', badgeLabel: '5.3-codex-spark', uiDisabledReason: GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON, }, { value: 'gpt-5.2', label: '5.2', badgeLabel: '5.2' }, { value: 'gpt-5.2-codex', label: '5.2 Codex', badgeLabel: '5.2-codex', uiDisabledReason: GPT_5_2_CODEX_UI_DISABLED_REASON, }, { value: 'gpt-5.1-codex-mini', label: '5.1 Codex Mini', badgeLabel: '5.1-codex-mini', uiDisabledReason: GPT_5_1_CODEX_MINI_UI_DISABLED_REASON, }, { value: 'gpt-5.1-codex-max', label: '5.1 Codex Max', badgeLabel: '5.1-codex-max' }, ]); }); it('keeps known Codex selections stable while Codex native account truth is loaded before the runtime model catalog', () => { const providerStatus = createCodexProviderStatus([], { authMethod: 'chatgpt', backend: { kind: 'codex-native', label: 'Codex native', endpointLabel: 'codex exec --json', }, authenticated: true, supported: true, verificationState: 'verified', modelVerificationState: 'idle', statusMessage: 'ChatGPT account ready', }); expect(normalizeTeamModelForUi('codex', 'gpt-5.4', providerStatus)).toBe('gpt-5.4'); expect(getTeamModelSelectionError('codex', 'gpt-5.4', providerStatus)).toBeNull(); }); it('keeps runtime models selectable without per-model verification state', () => { const providerStatus = createCodexProviderStatus(['gpt-5.4']); expect(normalizeTeamModelForUi('codex', 'gpt-5.4', providerStatus)).toBe('gpt-5.4'); expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']); expect(getTeamModelSelectionError('codex', 'gpt-5.4', providerStatus)).toBeNull(); }); it('does not require runtime verification for Anthropic curated models', () => { expect(normalizeTeamModelForUi('anthropic', 'opus')).toBe('opus'); expect(getTeamModelSelectionError('anthropic', 'opus')).toBeNull(); }); it('keeps Anthropic Opus 4.8, explicit 4.7, and explicit 4.6 in the fallback selector options', () => { expect(getAvailableTeamProviderModelOptions('anthropic')).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: 'available', availabilityReason: null, }, { value: 'claude-opus-4-6', label: 'Opus 4.6', badgeLabel: 'Opus 4.6', availabilityStatus: 'available', availabilityReason: null, }, { value: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6', availabilityStatus: 'available', availabilityReason: null, }, { value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5', availabilityStatus: 'available', availabilityReason: null, }, ]); }); it('does not let stale first-party Anthropic runtime labels downgrade the Opus alias', () => { const providerStatus: TeamModelRuntimeProviderStatus = { providerId: 'anthropic', models: ['opus', 'claude-opus-4-7'], authMethod: 'oauth', backend: null, authenticated: true, supported: true, modelVerificationState: 'idle', modelAvailability: [], modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-models-api', status: 'ready', fetchedAt: '2026-05-31T00:00:00.000Z', staleAt: '2026-05-31T00:10:00.000Z', defaultModelId: 'opus', defaultLaunchModel: 'opus', diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, models: [ { id: 'opus', launchModel: 'opus', displayName: 'Opus 4.7', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high', 'max'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'anthropic-models-api', }, ], }, }; const options = getAvailableTeamProviderModelOptions('anthropic', providerStatus); expect(options.find((option) => option.value === 'opus')).toMatchObject({ label: 'Opus 4.8', badgeLabel: 'Opus 4.8', }); expect(options.find((option) => option.value === 'claude-opus-4-7')).toMatchObject({ label: 'Opus 4.7', badgeLabel: 'Opus 4.7', }); }); it('merges first-party Anthropic catalog models with curated safety fallbacks', () => { const providerStatus: TeamModelRuntimeProviderStatus = { providerId: 'anthropic', models: ['opus', 'claude-sonnet-4-7'], authMethod: 'oauth', backend: null, authenticated: true, supported: true, modelVerificationState: 'idle', modelAvailability: [], modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-models-api', status: 'ready', fetchedAt: '2026-06-20T00:00:00.000Z', staleAt: '2026-06-20T00:10:00.000Z', defaultModelId: 'opus', defaultLaunchModel: 'opus', diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, models: [ { id: 'opus', launchModel: 'opus', displayName: 'Opus 4.9', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high', 'max'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: true, upgrade: false, source: 'anthropic-models-api', }, { id: 'claude-sonnet-4-7', launchModel: 'claude-sonnet-4-7', displayName: 'Sonnet 4.7', hidden: false, supportedReasoningEfforts: ['low', 'medium', 'high', 'max'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'anthropic-models-api', }, { id: 'claude-sonnet-4-7[1m]', launchModel: 'claude-sonnet-4-7[1m]', displayName: 'Sonnet 4.7 (1M)', hidden: true, supportedReasoningEfforts: ['low', 'medium', 'high', 'max'], defaultReasoningEffort: 'high', inputModalities: ['text', 'image'], supportsPersonality: false, isDefault: false, upgrade: false, source: 'anthropic-models-api', }, ], }, }; const options = getAvailableTeamProviderModelOptions('anthropic', providerStatus); const values = options.map((option) => option.value); expect(options.find((option) => option.value === 'opus')).toMatchObject({ label: 'Opus 4.9', badgeLabel: 'Opus 4.9', }); expect(values).toContain('claude-sonnet-4-7'); expect(values).toContain('claude-opus-4-7'); expect(values).not.toContain('claude-sonnet-4-7[1m]'); expect(normalizeTeamModelForUi('anthropic', 'claude-sonnet-4-7', providerStatus)).toBe( 'claude-sonnet-4-7' ); expect( getTeamModelSelectionError('anthropic', 'claude-sonnet-4-7', providerStatus) ).toBeNull(); }); it('keeps known Anthropic full model ids selectable without runtime verification', () => { expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8')).toBe('claude-opus-4-8'); expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8[1m]')).toBe('claude-opus-4-8[1m]'); expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7')).toBe('claude-opus-4-7'); expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-7[1m]')).toBe('claude-opus-4-7[1m]'); expect(normalizeTeamModelForUi('anthropic', 'claude-haiku-4-5-20251001')).toBe( 'claude-haiku-4-5-20251001' ); expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-8')).toBeNull(); expect(getTeamModelSelectionError('anthropic', 'claude-opus-4-7')).toBeNull(); expect(getTeamModelSelectionError('anthropic', 'claude-haiku-4-5-20251001')).toBeNull(); }); it('uses Anthropic-compatible runtime catalog models instead of curated Claude aliases', () => { const providerStatus = createAnthropicCompatibleProviderStatus({ modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-compatible-api', status: 'ready', fetchedAt: '2026-05-21T00:00:00.000Z', staleAt: '2026-05-21T00:10:00.000Z', defaultModelId: 'openai/gpt-oss-20b', defaultLaunchModel: 'openai/gpt-oss-20b', models: [ { id: 'openai/gpt-oss-20b', launchModel: 'openai/gpt-oss-20b', displayName: 'GPT OSS 20B', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: true, upgrade: false, source: 'anthropic-compatible-api', badgeLabel: 'Local', }, { id: 'hidden-local', launchModel: 'hidden-local', displayName: 'Hidden', hidden: true, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: false, upgrade: false, source: 'anthropic-compatible-api', badgeLabel: null, }, ], diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }, }); expect(getAvailableTeamProviderModels('anthropic', providerStatus)).toEqual([ 'openai/gpt-oss-20b', ]); expect(getAvailableTeamProviderModelOptions('anthropic', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'openai/gpt-oss-20b', label: 'GPT OSS 20B', badgeLabel: 'Local', availabilityStatus: 'available', availabilityReason: null, }, ]); expect(normalizeTeamModelForUi('anthropic', 'openai/gpt-oss-20b', providerStatus)).toBe( 'openai/gpt-oss-20b' ); expect(normalizeTeamModelForUi('anthropic', 'opus', providerStatus)).toBe(''); }); it('keeps custom Anthropic-compatible model ids selectable when the catalog is degraded', () => { const providerStatus = createAnthropicCompatibleProviderStatus({ modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-compatible-api', status: 'degraded', fetchedAt: '2026-05-21T00:00:00.000Z', staleAt: '2026-05-21T00:10:00.000Z', defaultModelId: null, defaultLaunchModel: null, models: [], diagnostics: { configReadState: 'failed', appServerState: 'degraded', message: 'Local catalog unavailable', }, }, }); expect(normalizeTeamModelForUi('anthropic', 'openai/gpt-oss-20b', providerStatus)).toBe( 'openai/gpt-oss-20b' ); expect( getTeamModelSelectionError('anthropic', 'openai/gpt-oss-20b', providerStatus) ).toBeNull(); expect(getAvailableTeamProviderModelOptions('anthropic', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, ]); }); it('allows custom Anthropic-compatible model ids before a runtime catalog is available', () => { const providerStatus = createAnthropicCompatibleProviderStatus({ modelCatalog: null, runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'anthropic-compatible-api', }, reasoningEffort: { supported: false, values: [], configPassthrough: true, }, }, }); expect(getAvailableTeamProviderModelOptions('anthropic', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, ]); expect(normalizeTeamModelForUi('anthropic', 'qwen/qwen3-coder', providerStatus)).toBe( 'qwen/qwen3-coder' ); expect(getTeamModelSelectionError('anthropic', 'qwen/qwen3-coder', providerStatus)).toBeNull(); }); it('keeps stale Anthropic-compatible catalog models visible while allowing custom ids', () => { const providerStatus = createAnthropicCompatibleProviderStatus({ modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-compatible-api', status: 'stale', fetchedAt: '2026-05-21T00:00:00.000Z', staleAt: '2026-05-21T00:10:00.000Z', defaultModelId: 'local-default', defaultLaunchModel: 'local-default', models: [ { id: 'local-default', launchModel: 'local-default', displayName: 'Local Default', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: true, upgrade: false, source: 'anthropic-compatible-api', badgeLabel: 'Stale', }, ], diagnostics: { configReadState: 'ready', appServerState: 'degraded', message: 'Using stale local catalog', }, }, }); expect(getAvailableTeamProviderModelOptions('anthropic', providerStatus)).toEqual([ { value: '', label: 'Default', badgeLabel: 'Default' }, { value: 'local-default', label: 'Local Default', badgeLabel: 'Stale', availabilityStatus: 'available', availabilityReason: null, }, ]); expect(normalizeTeamModelForUi('anthropic', 'openai/gpt-oss-20b', providerStatus)).toBe( 'openai/gpt-oss-20b' ); }); it('rejects custom Anthropic-compatible ids when a ready compatible catalog has visible models', () => { const providerStatus = createAnthropicCompatibleProviderStatus({ modelCatalog: { schemaVersion: 1, providerId: 'anthropic', source: 'anthropic-compatible-api', status: 'ready', fetchedAt: '2026-05-21T00:00:00.000Z', staleAt: '2026-05-21T00:10:00.000Z', defaultModelId: 'local-default', defaultLaunchModel: 'local-default', models: [ { id: 'local-default', launchModel: 'local-default', displayName: 'Local Default', hidden: false, supportedReasoningEfforts: [], defaultReasoningEffort: null, inputModalities: ['text'], supportsPersonality: true, isDefault: true, upgrade: false, source: 'anthropic-compatible-api', badgeLabel: 'Local', }, ], diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }, }); expect(normalizeTeamModelForUi('anthropic', 'openai/gpt-oss-20b', providerStatus)).toBe(''); expect( getTeamModelSelectionError('anthropic', 'openai/gpt-oss-20b', providerStatus) ).toContain('not available'); }); });