agent-ecosystem/src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts
777genius 6eada589aa feat(team): add Opus 4.8 to model catalog and runtime profile
Wire claude-opus-4-8 (+ [1m] variant) through the team model picker
alongside 4.7 and 4.6, point the opus alias label at 4.8, update the
reasoning/long-context predicates, and switch the fast-mode UI
message to mention 4.8.
2026-05-29 00:54:33 +03:00

492 lines
15 KiB
TypeScript

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<CliProviderStatus['modelCatalog']>['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<CliProviderStatus['modelCatalog']>['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
);
});
});