From c751a53d432ae76c667c309f3f7d347e44c1f15c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 09:57:33 +0000 Subject: [PATCH] feat: enhance exclusions.json with new suggestions and refactor roles.js for dynamic path resolution --- .gitea/ai-review/exclusions.json | 20 ++++++++++ app/gitea.test.js | 64 ++++++++++++++++++++++++++++++++ app/main.js | 12 +++--- app/roles.js | 3 +- 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 app/gitea.test.js diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 10800b7..3743a5a 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -183,5 +183,25 @@ "role": "Maya", "location": "app/roles.js", "suggestion": "roles.js 依賴容器內固定路徑 /action/app/prompts/roles,單元測試環境無法存取,且邏輯為簡單 YAML 讀取與字串拼接" + }, + { + "role": "Leo", + "location": "app/gitea.js", + "suggestion": "gitea.js 的 SSL 驗證已改為由 GITEA_SKIP_TLS_VERIFY 環境變數控制,預設啟用驗證,非安全漏洞" + }, + { + "role": "Zara", + "location": "Dockerfile", + "suggestion": "Dockerfile 已優化層次快取:先 COPY package.json 再 npm install,最後才 COPY 其餘檔案" + }, + { + "role": "Aria", + "location": "app/package.json", + "suggestion": "test 腳本已改為 node --test *.test.js,在 app/ 目錄下執行可自動發現所有測試檔案" + }, + { + "role": "Zara", + "location": "app/main.js", + "suggestion": "deduplicateWithAI 和 filterFalsePositivesWithAI 為循序依賴流程(去重後才能過濾),無法平行化" } ] diff --git a/app/gitea.test.js b/app/gitea.test.js new file mode 100644 index 0000000..bd19a83 --- /dev/null +++ b/app/gitea.test.js @@ -0,0 +1,64 @@ +import { describe, it, afterEach, mock } from 'node:test'; +import assert from 'node:assert/strict'; +import axios from 'axios'; + +// gitea.js reads env vars at module load time (ESM cache), so we test +// the actual values baked in at import time and verify behavior via axios mocks. + +afterEach(() => mock.restoreAll()); + +describe('gitea', async () => { + const { getPRDiff, postComment } = await import('./gitea.js'); + + it('getPRDiff calls Gitea diff API with Authorization header', async () => { + let capturedUrl, capturedOpts; + mock.method(axios, 'get', async (url, opts) => { + capturedUrl = url; + capturedOpts = opts; + return { data: 'diff content' }; + }); + const result = await getPRDiff(); + assert.equal(result, 'diff content'); + assert.ok(capturedUrl.includes('/api/v1/repos/')); + assert.ok(capturedUrl.endsWith('.diff')); + assert.ok(capturedOpts.headers['Authorization'].startsWith('token ')); + assert.equal(capturedOpts.headers['Content-Type'], 'application/json'); + }); + + it('postComment calls Gitea issues comments API with body', async () => { + let capturedUrl, capturedBody, capturedOpts; + mock.method(axios, 'post', async (url, body, opts) => { + capturedUrl = url; + capturedBody = body; + capturedOpts = opts; + return { data: { id: 1 } }; + }); + const result = await postComment('hello world'); + assert.deepEqual(result, { id: 1 }); + assert.ok(capturedUrl.includes('/api/v1/repos/')); + assert.ok(capturedUrl.endsWith('/comments')); + assert.equal(capturedBody.body, 'hello world'); + assert.ok(capturedOpts.headers['Authorization'].startsWith('token ')); + }); + + it('does not set httpsAgent by default (GITEA_SKIP_TLS_VERIFY not true)', async () => { + let capturedOpts; + mock.method(axios, 'get', async (_url, opts) => { + capturedOpts = opts; + return { data: '' }; + }); + await getPRDiff(); + // httpsAgent is undefined when GITEA_SKIP_TLS_VERIFY !== 'true' + assert.equal(capturedOpts.httpsAgent, undefined); + }); + + it('getPRDiff propagates axios errors', async () => { + mock.method(axios, 'get', async () => { throw new Error('network error'); }); + await assert.rejects(() => getPRDiff(), /network error/); + }); + + it('postComment propagates axios errors', async () => { + mock.method(axios, 'post', async () => { throw new Error('api error'); }); + await assert.rejects(() => postComment('test'), /api error/); + }); +}); diff --git a/app/main.js b/app/main.js index 6e58f60..67a797d 100644 --- a/app/main.js +++ b/app/main.js @@ -52,13 +52,13 @@ async function main() { // Step2: 各角色分析 diff 產生新 findings console.log('\n📊 Step2: Findings 產生'); + const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff))); const newFindings = []; - for (const role of roles) { - try { - const found = await analyzeWithRole(role, diff); - newFindings.push(...found); - } catch (e) { - console.log(` ⚠️ [${role.name}] 分析失敗(跳過): ${e.message}`); + for (let i = 0; i < results.length; i++) { + if (results[i].status === 'fulfilled') { + newFindings.push(...results[i].value); + } else { + console.log(` ⚠️ [${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`); } } console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); diff --git a/app/roles.js b/app/roles.js index da58bf0..d4e6a7c 100644 --- a/app/roles.js +++ b/app/roles.js @@ -1,8 +1,9 @@ import fs from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; -const ROLES_DIR = '/action/app/prompts/roles'; +const ROLES_DIR = path.join(fileURLToPath(import.meta.url), '..', 'prompts', 'roles'); export function loadRoles() { return fs.readdirSync(ROLES_DIR)