import { describe, it, beforeEach, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import axios from 'axios'; const ENV_KEYS = [ 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', ]; let saved = {}; beforeEach(() => { saved = {}; for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; } }); afterEach(() => { for (const k of ENV_KEYS) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } mock.restoreAll(); }); function mockOpenCodeResponse(content) { let calls = 0; mock.method(axios, 'post', async () => { calls += 1; if (calls === 1) return { data: { id: 'ses_test' } }; return { data: { parts: [{ type: 'text', text: content }] } }; }); } describe('chat - OpenCode', async () => { const { chat } = await import('../llm.js'); it('uses OpenCode server session API', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; process.env.OPENCODE_PROVIDER = 'google'; process.env.OPENCODE_MODEL = 'gemini-2.5-flash'; const calls = []; mock.method(axios, 'post', async (url, payload, opts) => { calls.push({ url, payload, headers: opts.headers }); if (url.endsWith('/session')) return { data: { id: 'ses_test' } }; return { data: { parts: [{ type: 'text', text: 'opencode response' }] } }; }); const result = await chat('sys', 'user'); assert.equal(result, 'opencode response'); assert.equal(calls[0].url, 'http://opencode.local:4096/session'); assert.deepEqual(calls[0].payload.model, { providerID: 'google', id: 'gemini-2.5-flash' }); assert.equal(calls[1].url, 'http://opencode.local:4096/session/ses_test/message'); assert.deepEqual(calls[1].payload.model, { providerID: 'google', modelID: 'gemini-2.5-flash' }); assert.equal(calls[1].payload.system, 'sys'); assert.deepEqual(calls[1].payload.parts, [{ type: 'text', text: 'user' }]); assert.equal(calls[1].headers['Authorization'], undefined); }); it('passes an insecure https agent to OpenCode by default', async () => { process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096'; const agents = []; mock.method(axios, 'post', async (url, _payload, opts) => { agents.push(opts.httpsAgent); if (url.endsWith('/session')) return { data: { id: 'ses_test' } }; return { data: { parts: [{ type: 'text', text: 'ok' }] } }; }); await chat('sys', 'user'); assert.equal(agents.length, 2); assert.equal(agents[0].options.rejectUnauthorized, false); assert.equal(agents[1].options.rejectUnauthorized, false); }); it('extracts text from OpenCode message parts', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; let calls = 0; mock.method(axios, 'post', async () => { calls += 1; if (calls === 1) return { data: { id: 'ses_test' } }; return { data: { parts: [{ type: 'text', text: 'hello' }, { type: 'text', text: ' world' }] } }; }); const result = await chat('sys', 'user'); assert.equal(result, 'hello world'); }); it('calls process.exit(1) when OpenCode fails', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mock.method(axios, 'post', async () => { throw new Error('fail'); }); const exitMock = mock.method(process, 'exit', () => { throw new Error('exit:1'); }); await assert.rejects(() => chat('sys', 'user'), /exit:1/); assert.equal(exitMock.mock.calls[0].arguments[0], 1); }); }); describe('chatJSON', async () => { const { chatJSON } = await import('../llm.js'); it('parses plain JSON response', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mockOpenCodeResponse('[{"level":"critical"}]'); const result = await chatJSON('sys', 'user'); assert.deepEqual(result, [{ level: 'critical' }]); }); it('strips markdown code block before parsing', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mockOpenCodeResponse('```json\n[{"level":"info"}]\n```'); const result = await chatJSON('sys', 'user'); assert.deepEqual(result, [{ level: 'info' }]); }); it('extracts JSON array from surrounding prose', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mockOpenCodeResponse('**Reviewing findings**\n\n[{"level":"warning","suggestion":"x"}]\n\nDone.'); const result = await chatJSON('sys', 'user'); assert.deepEqual(result, [{ level: 'warning', suggestion: 'x' }]); }); it('extracts JSON object from surrounding prose', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mockOpenCodeResponse('**Begin Combine**\n{"merged_text":"repo block\\n\\nsource block"}'); const result = await chatJSON('sys', 'user'); assert.deepEqual(result, { merged_text: 'repo block\n\nsource block' }); }); it('returns [] when JSON is invalid', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mockOpenCodeResponse('not json'); const result = await chatJSON('sys', 'user'); assert.deepEqual(result, []); }); });