import { describe, expect, it, vi, afterEach } from 'vitest'; import { findOpencodeServer, parseModelRef, opencodeBaseUrls, opencodePrompt } from '../opencode'; /** * The single most important behaviour under test is the SPA-catch-all trap: * `opencode serve` answers 200 with the web UI's index.html for ANY unknown * path, so a probe that trusts `res.ok` "verifies" endpoints that do not * exist. That is not hypothetical — it is exactly how this integration was * first built wrong (against an assumed OpenAI-compatible `/v1/models` that * only ever returned HTML). */ const HTML_CATCHALL = { ok: true, headers: new Headers({ 'content-type': 'text/html; charset=utf-8' }), json: async () => { throw new Error('not json'); }, }; function jsonResponse(body: unknown) { return { ok: true, headers: new Headers({ 'content-type': 'application/json' }), json: async () => body, }; } describe('parseModelRef', () => { it('splits providerID/modelID, keeping slashes inside the model id', () => { expect(parseModelRef('opencode/deepseek-v4-flash-free')).toEqual({ providerID: 'opencode', modelID: 'deepseek-v4-flash-free', }); // Real provider ids do contain slashes (e.g. openrouter's // "anthropic/claude-..."), so only the FIRST slash separates. expect(parseModelRef('openrouter/anthropic/claude-sonnet-4.5')).toEqual({ providerID: 'openrouter', modelID: 'anthropic/claude-sonnet-4.5', }); }); it('rejects malformed refs rather than guessing', () => { expect(parseModelRef('noslash')).toBeNull(); expect(parseModelRef('/leading')).toBeNull(); expect(parseModelRef('trailing/')).toBeNull(); expect(parseModelRef('')).toBeNull(); }); }); describe('opencodeBaseUrls', () => { const original = process.env.OPENCODE_BASE_URL; afterEach(() => { if (original === undefined) delete process.env.OPENCODE_BASE_URL; else process.env.OPENCODE_BASE_URL = original; }); it('refuses a non-loopback override — this class must never reach off-machine', () => { process.env.OPENCODE_BASE_URL = 'https://evil.example.com'; const urls = opencodeBaseUrls(); expect(urls.some((u) => u.includes('evil.example.com'))).toBe(false); expect(urls[0]).toMatch(/127\.0\.0\.1|localhost/); }); it('honours a loopback override, trying it first', () => { process.env.OPENCODE_BASE_URL = 'http://127.0.0.1:9999/'; expect(opencodeBaseUrls()[0]).toBe('http://127.0.0.1:9999'); }); }); describe('findOpencodeServer', () => { const originalFetch = global.fetch; afterEach(() => { global.fetch = originalFetch; vi.restoreAllMocks(); }); it('does NOT accept the web UI catch-all as a working API (200 + HTML)', async () => { global.fetch = vi.fn().mockResolvedValue(HTML_CATCHALL) as unknown as typeof fetch; expect(await findOpencodeServer()).toBeNull(); }); it('parses the real /api/model shape into providerID/modelID refs', async () => { global.fetch = vi.fn().mockResolvedValue( jsonResponse({ data: [ { id: 'deepseek-v4-flash-free', providerID: 'opencode', name: 'DeepSeek V4 Flash Free' }, { id: 'deepseek-chat', providerID: 'deepseek' }, { id: '', providerID: 'broken' }, ], }), ) as unknown as typeof fetch; const found = await findOpencodeServer(); expect(found?.models.map((m) => m.ref)).toEqual([ 'opencode/deepseek-v4-flash-free', 'deepseek/deepseek-chat', ]); expect(found?.models[0].label).toBe('DeepSeek V4 Flash Free (opencode)'); }); it('returns null when the server answers JSON with no models', async () => { global.fetch = vi.fn().mockResolvedValue(jsonResponse({ data: [] })) as unknown as typeof fetch; expect(await findOpencodeServer()).toBeNull(); }); }); describe('opencodePrompt', () => { const originalFetch = global.fetch; afterEach(() => { global.fetch = originalFetch; vi.restoreAllMocks(); }); it('returns only the text parts — never the model\'s private reasoning', async () => { global.fetch = vi.fn() .mockResolvedValueOnce(jsonResponse({ id: 'ses_abc' })) .mockResolvedValueOnce( jsonResponse({ parts: [ { type: 'step-start' }, { type: 'reasoning', text: 'SECRET chain of thought that must not be shown' }, { type: 'text', text: 'The visible answer.' }, { type: 'step-finish' }, ], }), ) as unknown as typeof fetch; const result = await opencodePrompt('http://127.0.0.1:4096', { providerID: 'opencode', modelID: 'm' }, 'sys', 'q'); expect(result).toEqual({ ok: true, answer: 'The visible answer.' }); if (result.ok) expect(result.answer).not.toContain('SECRET'); }); it('fails cleanly when no session can be created', async () => { global.fetch = vi.fn().mockResolvedValue(HTML_CATCHALL) as unknown as typeof fetch; const result = await opencodePrompt('http://127.0.0.1:4096', { providerID: 'p', modelID: 'm' }, undefined, 'q'); expect(result.ok).toBe(false); }); it('fails cleanly when the reply carries no text part', async () => { global.fetch = vi.fn() .mockResolvedValueOnce(jsonResponse({ id: 'ses_abc' })) .mockResolvedValueOnce(jsonResponse({ parts: [{ type: 'step-start' }, { type: 'reasoning', text: 'only thinking' }] })) as unknown as typeof fetch; const result = await opencodePrompt('http://127.0.0.1:4096', { providerID: 'p', modelID: 'm' }, undefined, 'q'); expect(result).toEqual({ ok: false, error: 'OpenCode returned no message content' }); }); });