From 228d5ceb4ab3ed661cc04411576c5ebb6647d782 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 22 Jun 2026 09:40:48 +0000 Subject: [PATCH] =?UTF-8?q?test(review=20=E8=88=87=20OpenCode=20TLS):=20?= =?UTF-8?q?=E8=A3=9C=E9=BD=8A=20Pull=20Review=20=E8=88=87=20TLS=20?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E8=A6=86=E8=93=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comments.test.js | 51 ++++++++++++++++++++++++++++++++++++++++++- app/config.test.js | 7 ++++++ app/gitea.test.js | 20 ++++++++++++++++- app/preflight.test.js | 17 +++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/app/comments.test.js b/app/comments.test.js index 78b0e0f..734722c 100644 --- a/app/comments.test.js +++ b/app/comments.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { saveFindings, parseLocation, postNewCriticalComments } from './comments.js'; +import { saveFindings, parseLocation, buildReviewPayload, postFindingsReview, postNewCriticalComments } from './comments.js'; import { FINDINGS_PATH } from './config.js'; describe('saveFindings', () => { @@ -185,3 +185,52 @@ describe('postNewCriticalComments', () => { assert.ok(issueCalls.every(b => criticalCommentPattern.test(b))); }); }); + +describe('review payload', () => { + const intro = '## Reviewers\n\nLeo / Maya'; + const findings = [ + { level: 'warning', role: 'Leo', location: 'app/a.js:1', suggestion: '改善命名', is_new: true }, + { level: 'critical', role: 'Rex', location: 'app/b.js:5', suggestion: '修正權限檢查', is_new: true }, + { level: 'critical', role: 'Maya', location: 'app/c.js', suggestion: '補上交易保護', is_new: true }, + { level: 'info', role: 'Bard', location: 'app/d.js:9', suggestion: '補註解', is_new: false }, + ]; + + it('builds one review body with role intro and inline comments for every located finding', () => { + const payload = buildReviewPayload(intro, findings); + assert.match(payload.body, /Reviewers/); + assert.doesNotMatch(payload.body, /改善命名/); + assert.match(payload.body, /無法行內標註的新嚴重問題/); + assert.equal(payload.comments.length, 3); + assert.deepEqual(payload.comments.map(c => c.path), ['app/a.js', 'app/b.js', 'app/d.js']); + assert.deepEqual(payload.comments.map(c => c.new_position), [1, 5, 9]); + assert.match(payload.comments[0].body, /新發現問題/); + assert.match(payload.comments[1].body, /新嚴重問題/); + assert.match(payload.comments[2].body, /舊有未解決問題/); + }); + + it('posts findings as a single pull review', async () => { + const calls = []; + await postFindingsReview(intro, findings, { + postReview: async (payload) => { calls.push(payload); }, + }); + assert.equal(calls.length, 1); + assert.match(calls[0].body, /Reviewers/); + assert.equal(calls[0].comments.length, 3); + }); + + it('falls back to one review body when inline comments are rejected', async () => { + const calls = []; + await postFindingsReview(intro, findings, { + postReview: async (payload) => { + calls.push(payload); + if (calls.length === 1) throw new Error('line not in diff'); + }, + }); + assert.equal(calls.length, 2); + assert.equal(calls[1].comments.length, 0); + assert.match(calls[1].body, /改善命名/); + assert.match(calls[1].body, /修正權限檢查/); + assert.match(calls[1].body, /補上交易保護/); + assert.match(calls[1].body, /補註解/); + }); +}); diff --git a/app/config.test.js b/app/config.test.js index dec24c9..0fe62f9 100644 --- a/app/config.test.js +++ b/app/config.test.js @@ -114,6 +114,13 @@ describe('getLLMConfig', () => { assert.equal(shouldSkipOpenCodeTLSVerify(), false); }); + it('skips OpenCode TLS verification for any value other than false', () => { + for (const value of ['', '0', 'true', 'yes']) { + process.env.OPENCODE_SKIP_TLS_VERIFY = value; + assert.equal(shouldSkipOpenCodeTLSVerify(), true); + } + }); + 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/gitea.test.js b/app/gitea.test.js index 89c5a3a..2673ce3 100644 --- a/app/gitea.test.js +++ b/app/gitea.test.js @@ -1,7 +1,7 @@ import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import axios from 'axios'; -import { getPRDiff, filterDiff, postComment, postPullReviewComment, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit, getBotReviewOutcome } from './gitea.js'; +import { getPRDiff, filterDiff, postComment, createPullReview, postPullReviewComment, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit, getBotReviewOutcome } from './gitea.js'; afterEach(() => mock.restoreAll()); @@ -77,6 +77,24 @@ describe('gitea', () => { assert.ok(capturedOpts.headers['Authorization'].startsWith('token ')); }); + it('createPullReview posts body and comments in one pull review', async () => { + let capturedUrl, capturedBody; + mock.method(axios, 'post', async (url, body) => { + capturedUrl = url; + capturedBody = body; + return { data: { id: 8 } }; + }); + const result = await createPullReview({ + body: 'review body', + comments: [{ path: 'app/a.js', body: 'inline', new_position: 3 }], + }); + assert.deepEqual(result, { id: 8 }); + assert.ok(capturedUrl.endsWith('/reviews')); + assert.equal(capturedBody.event, 'COMMENT'); + assert.equal(capturedBody.body, 'review body'); + assert.deepEqual(capturedBody.comments, [{ path: 'app/a.js', body: 'inline', new_position: 3 }]); + }); + it('postPullReviewComment propagates axios errors', async () => { mock.method(axios, 'post', async () => { throw new Error('not in diff'); }); await assert.rejects(() => postPullReviewComment({ path: 'a.js', line: 1, body: 'x' }), /not in diff/); diff --git a/app/preflight.test.js b/app/preflight.test.js index 0270c33..6cc0d33 100644 --- a/app/preflight.test.js +++ b/app/preflight.test.js @@ -199,6 +199,23 @@ describe('verifyLLM', () => { assert.equal(agents[1].options.rejectUnauthorized, false); }); + it('passes an insecure https agent for opencode when TLS skip is explicitly true', async () => { + clearLLMEnv(); + process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096'; + process.env.OPENCODE_SKIP_TLS_VERIFY = 'true'; + const agents = []; + mock.method(axios, 'get', async (url, opts) => { + agents.push(opts.httpsAgent); + if (url.endsWith('/global/health')) return { data: { healthy: true } }; + 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(agents.length, 2); + assert.equal(agents[0].options.rejectUnauthorized, false); + assert.equal(agents[1].options.rejectUnauthorized, false); + }); + it('does not pass an insecure https agent for opencode when TLS verification is enabled', async () => { clearLLMEnv(); process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';