import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mergeModels, prefixLlamaSwapModels, clearProviderSnapshotCache, getProviderSnapshot, } from '../provider-snapshot.js'; vi.mock('../acp-probe.js', () => ({ probeAcpProvider: vi.fn(), })); import { probeAcpProvider } from '../acp-probe.js'; const mockProbe = vi.mocked(probeAcpProvider); function mockSql(agents: Array<{ name: string; install_path: string | null; supports_acp: boolean; models: Array<{ id: string; label: string }> | null; label: string | null; transport: string | null; }>) { return vi.fn((strings: TemplateStringsArray) => { const query = strings.join(''); if (query.includes('FROM available_agents')) { return Promise.resolve(agents); } if (query.includes('UPDATE available_agents')) { return Promise.resolve([]); } return Promise.resolve([]); }) as unknown as import('../db.js').Sql; } const config = { LLAMA_SWAP_URL: 'http://llama-swap.test', } as import('../config.js').Config; describe('prefixLlamaSwapModels', () => { it('prefixes bare ids', () => { expect(prefixLlamaSwapModels([{ id: 'qwen3', label: 'qwen3' }])).toEqual([ { id: 'llama-swap/qwen3', label: 'qwen3' }, ]); }); it('leaves already-prefixed ids unchanged', () => { expect(prefixLlamaSwapModels([{ id: 'llama-swap/qwen3', label: 'qwen3' }])).toEqual([ { id: 'llama-swap/qwen3', label: 'qwen3' }, ]); }); }); describe('mergeModels', () => { it('dedupes by id preserving first occurrence', () => { const merged = mergeModels( [{ id: 'a', label: 'A' }], [{ id: 'a', label: 'A2' }, { id: 'b', label: 'B' }], ); expect(merged).toEqual([ { id: 'a', label: 'A' }, { id: 'b', label: 'B' }, ]); }); }); describe('getProviderSnapshot', () => { beforeEach(() => { clearProviderSnapshotCache(); vi.restoreAllMocks(); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: 'local-model' }, { id: 'llama-swap/existing' }], }), }), ); }); it('merges opencode ACP models with prefixed llama-swap models', async () => { mockProbe.mockResolvedValue({ ok: true, models: [{ id: 'opencode/big-pickle', label: 'Big Pickle', isDefault: true }], modes: [{ id: 'build', label: 'Build' }], defaultModeId: 'build', commands: [{ name: 'custom', description: 'From ACP probe' }], }); const sql = mockSql([ { name: 'opencode', install_path: '/usr/bin/opencode', supports_acp: true, models: null, label: 'OpenCode', transport: 'acp', }, ]); const entries = await getProviderSnapshot(sql, config, '/tmp/project', true); const opencode = entries.find((e) => e.name === 'opencode'); expect(opencode?.models.map((m) => m.id)).toEqual([ 'opencode/big-pickle', 'llama-swap/local-model', 'llama-swap/existing', ]); expect(opencode?.commands.some((c) => c.name === 'help')).toBe(true); expect(opencode?.commands.some((c) => c.name === 'custom')).toBe(true); }); it('combines qwen-shaped probe and settings model lists via mergeModels', () => { const merged = mergeModels( [{ id: 'qwen-probed', label: 'Qwen Probed' }], [{ id: 'from-settings', label: 'from-settings' }], ); expect(merged.map((m) => m.id)).toEqual(['qwen-probed', 'from-settings']); }); it('returns cached entries on second call within TTL', async () => { mockProbe.mockResolvedValue({ ok: true, models: [{ id: 'm1', label: 'M1' }], modes: [], defaultModeId: null, commands: [], }); const sql = mockSql([ { name: 'goose', install_path: '/usr/bin/goose', supports_acp: true, models: null, label: 'Goose', transport: 'acp', }, ]); await getProviderSnapshot(sql, config, '/tmp/cwd', true); await getProviderSnapshot(sql, config, '/tmp/cwd', false); expect(mockProbe).toHaveBeenCalledTimes(1); }); it('attaches claude thinking options', async () => { const sql = mockSql([ { name: 'claude', install_path: '/usr/bin/claude', supports_acp: false, models: [{ id: 'claude-sonnet', label: 'Sonnet' }], label: 'Claude Code', transport: 'pty', }, ]); const entries = await getProviderSnapshot(sql, config, '/tmp/project', true); const claude = entries.find((e) => e.name === 'claude'); expect(claude?.models[0]?.thinkingOptions?.length).toBeGreaterThan(0); expect(claude?.modes.length).toBeGreaterThan(0); expect(claude?.commands.some((c) => c.name === 'help')).toBe(true); }); });