// @vitest-environment node import { beforeEach, describe, expect, it, vi } from 'vitest'; const execCliMock = vi.fn(); const buildProviderAwareCliEnvMock = vi.fn(); vi.mock('@main/utils/childProcess', () => ({ execCli: (...args: Parameters) => execCliMock(...args), })); vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({ buildProviderAwareCliEnv: (...args: Parameters) => buildProviderAwareCliEnvMock(...args), })); import { CliProviderModelAvailabilityService, type ProviderModelAvailabilityContext, } from '@main/services/runtime/CliProviderModelAvailabilityService'; function createContext(models: string[]): ProviderModelAvailabilityContext { return { binaryPath: '/usr/local/bin/claude', installedVersion: '2.3.4', provider: { providerId: 'codex', models, supported: true, authenticated: true, authMethod: 'oauth_token', selectedBackendId: 'chatgpt', resolvedBackendId: 'chatgpt', capabilities: { teamLaunch: true, oneShot: true, }, backend: { kind: 'openai', label: 'OpenAI', endpointLabel: 'chatgpt.com/backend-api/codex/responses', }, }, }; } describe('CliProviderModelAvailabilityService', () => { beforeEach(() => { vi.clearAllMocks(); }); it('reuses probe cache for the same provider signature', async () => { buildProviderAwareCliEnvMock.mockResolvedValue({ env: { HOME: '/Users/tester' }, connectionIssues: {}, }); execCliMock.mockResolvedValue({ stdout: 'PONG', stderr: '' }); const service = new CliProviderModelAvailabilityService(); const context = createContext(['gpt-5.4', 'gpt-5.3-codex']); expect(service.getSnapshot(context).modelVerificationState).toBe('verifying'); expect(service.getSnapshot(context).modelVerificationState).toBe('verifying'); await vi.waitFor(() => { expect(execCliMock).toHaveBeenCalledTimes(2); }); expect(service.getSnapshot(context).modelAvailability).toEqual([ expect.objectContaining({ modelId: 'gpt-5.4', status: 'available' }), expect.objectContaining({ modelId: 'gpt-5.3-codex', status: 'available' }), ]); expect(execCliMock).toHaveBeenCalledTimes(2); }); it('marks visible unsupported models as unavailable with the runtime reason', async () => { buildProviderAwareCliEnvMock.mockResolvedValue({ env: { HOME: '/Users/tester' }, connectionIssues: {}, }); execCliMock.mockRejectedValue( new Error( "The 'gpt-5.1-codex-max' model is not supported when using Codex with a ChatGPT account." ) ); const onUpdate = vi.fn(); const service = new CliProviderModelAvailabilityService(onUpdate); service.getSnapshot(createContext(['gpt-5.1-codex-max'])); await vi.waitFor(() => { expect(onUpdate).toHaveBeenCalledWith( 'codex', expect.any(String), expect.objectContaining({ modelAvailability: [ expect.objectContaining({ modelId: 'gpt-5.1-codex-max', status: 'unavailable', reason: 'Not available with Codex ChatGPT subscription', }), ], }) ); }); }); it('marks timeout-like probe failures as unknown instead of unavailable', async () => { buildProviderAwareCliEnvMock.mockResolvedValue({ env: { HOME: '/Users/tester' }, connectionIssues: {}, }); execCliMock.mockRejectedValue(new Error('Command timed out after 45000ms')); const onUpdate = vi.fn(); const service = new CliProviderModelAvailabilityService(onUpdate); service.getSnapshot(createContext(['gpt-5.4'])); await vi.waitFor(() => { expect(onUpdate).toHaveBeenCalledWith( 'codex', expect.any(String), expect.objectContaining({ modelAvailability: [ expect.objectContaining({ modelId: 'gpt-5.4', status: 'unknown', reason: 'Model verification timed out', }), ], }) ); }); }); it('invalidates the cache when the provider signature changes', async () => { buildProviderAwareCliEnvMock.mockResolvedValue({ env: { HOME: '/Users/tester' }, connectionIssues: {}, }); execCliMock.mockResolvedValue({ stdout: 'PONG', stderr: '' }); const service = new CliProviderModelAvailabilityService(); service.getSnapshot(createContext(['gpt-5.4'])); await vi.waitFor(() => { expect(execCliMock).toHaveBeenCalledTimes(1); }); service.getSnapshot(createContext(['gpt-5.4', 'gpt-5.2'])); await vi.waitFor(() => { expect(execCliMock).toHaveBeenCalledTimes(3); }); }); });