From 3f78543159fe4e0851812a7cea0a48d5d45c535a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 17 Jun 2026 06:50:51 +0000 Subject: [PATCH] =?UTF-8?q?test(opencode):=20=E8=A3=9C=E4=B8=8A=20OpenCode?= =?UTF-8?q?=20server=20provider=20=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config.test.js | 20 ++++++++++++++ app/llm.test.js | 64 +++++++++++++++++++++++++++++++++++++++++++ app/preflight.test.js | 37 +++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/app/config.test.js b/app/config.test.js index c648520..869e811 100644 --- a/app/config.test.js +++ b/app/config.test.js @@ -8,6 +8,8 @@ const ENV_KEYS = [ 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', + 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', + 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', ]; let saved = {}; @@ -84,6 +86,24 @@ describe('getLLMConfig', () => { assert.equal(cfg.model, 'my-amazon-model'); }); + it('detects opencode server with gemini defaults', () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'opencode'); + assert.deepEqual(cfg.apiKeys, ['opencode']); + assert.equal(cfg.baseURL, 'http://opencode.local:4096'); + assert.equal(cfg.model, 'gemini-2.5-flash'); + }); + + it('detects opencode server with custom model', () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_MODEL = 'google/gemini-2.5-pro'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'opencode'); + assert.equal(cfg.baseURL, 'http://opencode.local:4096'); + assert.equal(cfg.model, 'google/gemini-2.5-pro'); + }); + it('openai takes priority over gemini when both set', () => { process.env.OPENAI_API_KEY = 'sk-test'; process.env.GEMINI_API_KEY = 'gemini-key'; diff --git a/app/llm.test.js b/app/llm.test.js index c4654ab..be2756a 100644 --- a/app/llm.test.js +++ b/app/llm.test.js @@ -10,6 +10,8 @@ const ENV_KEYS = [ 'CLAUDE_API_KEY', 'CLAUDE_BASE_URL', 'CLAUDE_MODEL', 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', + 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', + 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', ]; let saved = {}; @@ -125,6 +127,68 @@ describe('chat - key rotation', async () => { await chat('sys', 'user'); assert.equal(capturedHeaders['anthropic-version'], '2023-06-01'); }); + + it('uses OpenCode server session API for opencode', 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('uses Basic Auth for protected OpenCode server', async () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_SERVER_USERNAME = 'opencode'; + process.env.OPENCODE_SERVER_PASSWORD = 'secret'; + const headers = []; + mock.method(axios, 'post', async (url, _payload, opts) => { + headers.push(opts.headers); + if (url.endsWith('/session')) return { data: { id: 'ses_test' } }; + return { data: { parts: [{ type: 'text', text: 'ok' }] } }; + }); + await chat('sys', 'user'); + assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`); + }); + + it('uses Responses API for openai GPT-5.5', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_MODEL = 'GPT-5.5'; + let capturedUrl, capturedPayload; + mock.method(axios, 'post', async (url, payload) => { + capturedUrl = url; + capturedPayload = payload; + return { data: { output_text: 'gpt response' } }; + }); + const result = await chat('sys', 'user'); + assert.equal(result, 'gpt response'); + assert.equal(capturedUrl, 'https://api.openai.com/v1/responses'); + assert.deepEqual(capturedPayload, { model: 'GPT-5.5', instructions: 'sys', input: 'user', temperature: 0.2 }); + }); + + it('extracts opencode text from 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'); + }); }); describe('chatJSON', async () => { diff --git a/app/preflight.test.js b/app/preflight.test.js index d654bc2..7a49678 100644 --- a/app/preflight.test.js +++ b/app/preflight.test.js @@ -9,6 +9,8 @@ const LLM_ENV_KEYS = [ 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', + 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', + 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', ]; function clearLLMEnv() { @@ -163,6 +165,41 @@ describe('verifyLLM', () => { assert.equal(capturedHeaders['anthropic-version'], '2023-06-01'); }); + it('checks opencode server provider and model', async () => { + clearLLMEnv(); + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_PROVIDER = 'google'; + process.env.OPENCODE_MODEL = 'gemini-2.5-flash'; + const urls = []; + mock.method(axios, 'get', async (url) => { + urls.push(url); + if (url.endsWith('/global/health')) return { data: { healthy: true, version: '1.17.7' } }; + return { data: { providers: [{ id: 'google', models: { 'gemini-2.5-flash': { id: 'gemini-2.5-flash' } } }] } }; + }); + const result = await verifyLLM(); + assert.equal(result.ok, true); + assert.equal(result.provider, 'opencode'); + assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']); + }); + + it('checks openai GPT-5.5 with Responses API', async () => { + clearLLMEnv(); + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_MODEL = 'GPT-5.5'; + let capturedUrl, capturedPayload; + mock.method(axios, 'post', async (url, payload) => { + capturedUrl = url; + capturedPayload = payload; + return { data: { output_text: 'o' } }; + }); + const result = await verifyLLM(); + assert.equal(result.ok, true); + assert.equal(result.provider, 'openai'); + assert.equal(capturedUrl, 'https://api.openai.com/v1/responses'); + assert.equal(capturedPayload.model, 'GPT-5.5'); + assert.equal(capturedPayload.max_output_tokens, 1); + }); + it('checks base URL connectivity for ollama (no key)', async () => { clearLLMEnv(); process.env.OLLAMA_BASE_URL = 'http://ollama.local/v1';