test(test 目錄): 將 12 個單元測試移至 app/test 並更新 npm test 指令
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
51fd223b48
commit
303104bb20
@@ -0,0 +1,145 @@
|
||||
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, []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user