import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { DEFAULT_AI_ENTITLEMENT, DEFAULT_AI_POLICY, type AiPolicy } from '../types'; import { loadAiSettings } from '../local-settings'; const { listOpencodeModels } = vi.hoisted(() => ({ listOpencodeModels: vi.fn() })); vi.mock('../local-client', () => ({ listOpencodeModels })); const { discoverLocalOllama, recommendDefaultModel } = vi.hoisted(() => ({ discoverLocalOllama: vi.fn(), recommendDefaultModel: vi.fn(), })); vi.mock('../local-discovery', () => ({ discoverLocalOllama, recommendDefaultModel })); const { supportsLocalLlm } = vi.hoisted(() => ({ supportsLocalLlm: vi.fn(() => true) })); vi.mock('../../platform-capabilities', () => ({ supportsLocalLlm })); // Imported after the mocks so it picks up the mocked modules. const { ensureDefaultProvider, _resetAutoProvisionForTests } = await import('../auto-provision'); function policyWith(classes: AiPolicy['entitlement']['classes']): AiPolicy { return { ...DEFAULT_AI_POLICY, entitlement: { ...DEFAULT_AI_ENTITLEMENT, classes } }; } describe('ensureDefaultProvider', () => { beforeEach(() => { window.localStorage.clear(); _resetAutoProvisionForTests(); listOpencodeModels.mockReset(); discoverLocalOllama.mockReset(); recommendDefaultModel.mockReset(); supportsLocalLlm.mockReturnValue(true); }); afterEach(() => { vi.unstubAllGlobals(); }); it('prefers OpenCode when it has a usable model', async () => { listOpencodeModels.mockResolvedValue([{ ref: 'opencode/deepseek-v4-flash-free', label: 'DeepSeek V4 Flash Free' }]); const next = await ensureDefaultProvider(policyWith(['opencode', 'local'])); expect(next.provider).toBe('opencode'); expect(next.opencodeModel).toBe('opencode/deepseek-v4-flash-free'); expect(discoverLocalOllama).not.toHaveBeenCalled(); expect(loadAiSettings().provider).toBe('opencode'); // persisted, not just returned }); it('falls back to Ollama when OpenCode is unreachable', async () => { listOpencodeModels.mockRejectedValue(new Error('No local OpenCode server is running')); discoverLocalOllama.mockResolvedValue({ baseUrl: 'http://127.0.0.1:11434', models: [{ name: 'qwen2.5:32b' }] }); recommendDefaultModel.mockReturnValue('qwen2.5:32b'); const next = await ensureDefaultProvider(policyWith(['opencode', 'local'])); expect(next.provider).toBe('local'); expect(next.localModel).toBe('qwen2.5:32b'); }); it('leaves provider unset when neither is available', async () => { listOpencodeModels.mockResolvedValue([]); discoverLocalOllama.mockResolvedValue(null); const next = await ensureDefaultProvider(policyWith(['opencode', 'local'])); expect(next.provider).toBeNull(); }); it('never overrides an explicit choice already saved', async () => { const { saveAiSettings, DEFAULT_AI_SETTINGS } = await import('../local-settings'); saveAiSettings({ ...DEFAULT_AI_SETTINGS, provider: 'server', serverModel: 'qwen2.5:32b' }); const next = await ensureDefaultProvider(policyWith(['opencode', 'local', 'server'])); expect(next.provider).toBe('server'); expect(listOpencodeModels).not.toHaveBeenCalled(); }); it('only probes once per module lifetime even if called again', async () => { listOpencodeModels.mockResolvedValue([]); discoverLocalOllama.mockResolvedValue(null); await ensureDefaultProvider(policyWith(['opencode', 'local'])); await ensureDefaultProvider(policyWith(['opencode', 'local'])); expect(listOpencodeModels).toHaveBeenCalledTimes(1); }); });