Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
5.1 KiB
JavaScript
146 lines
5.1 KiB
JavaScript
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, []);
|
|
});
|
|
});
|