Compare commits

..

3 Commits

5 changed files with 60 additions and 56 deletions
+5
View File
@@ -208,5 +208,10 @@
"role": "Zara",
"location": "app/main.js",
"suggestion": "deduplicateWithAI 和 filterFalsePositivesWithAI 為循序依賴流程(去重後才能過濾),無法平行化"
},
{
"role": "Leo",
"location": "app/comments.js",
"suggestion": "buildTable 函式已在 comments.js 第 13 行定義,非未定義或未匯入,不會導致執行時錯誤"
}
]
+6 -27
View File
@@ -1,37 +1,16 @@
[
{
"level": "warning",
"role": "Aria",
"location": "app/main.js:60",
"suggestion": "已移除的註解 `// 載入舊 findings,用於 AI 誤報過濾參考` 提供了該程式碼區塊的上下文資訊。建議保留此類註解或以 JSDoc 形式補充,以提升程式碼可讀性與維護性。",
"is_new": true
},
{
"level": "warning",
"role": "Aria",
"location": "app/main.js:64",
"suggestion": "已移除的註解 `// Clone repo 以讀取舊 findings 與排除清單` 說明了呼叫 `cloneRepo` 的目的。建議保留此類註解或以 JSDoc 形式補充,以提升程式碼可讀性與維護性。",
"level": "critical",
"role": "Leo",
"location": "app/comments.js:66",
"suggestion": "`buildTable` 函式在此檔案中被呼叫,但未見其定義或匯入。這將導致執行時錯誤。請確保 `buildTable` 函式已被正確定義或從其他模組匯入,以確保程式碼的正確執行。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "app/gitea.js:14",
"suggestion": "更新 `filterDiff` 的測試。過濾邏輯從正表達式匹配改為 `startsWith`這是一個功能性變更。需要新增或修改測試案例,以確保新的 `startsWith` 邏輯能正確過濾或保留 diff 區塊,特別是針對邊界條件和不同前綴的匹配情況。",
"is_new": true
},
{
"level": "info",
"role": "Rex",
"location": "action.yaml",
"suggestion": "此 Action 需要 `contents: write`、`pull-requests: write` 和 `issues: write` 權限。這些權限對於 Action 的正常運作是必要的(例如寫入 findings.json、發布評論),但屬於較廣泛的權限。建議在文件或使用說明中明確指出這些權限的需求及其潛在影響,確保使用者了解並接受。",
"is_new": false
},
{
"level": "info",
"role": "Leo",
"location": "app/main.js:16",
"suggestion": "在 `main` 函式中,移除了多個高層次的註解,例如 `// 偵測 LLM`、`// 載入角色` 等。雖然這些註解描述了接下來的程式碼區塊,但對於理解整個 pipeline 的執行流程和各步驟的目標,它們提供了有用的指引。建議恢復這些高層次註解,以提升程式碼的整體可讀性和維護性,特別是對於新加入的開發者。",
"location": "app/gitea.js:11, app/main.js:42-45",
"suggestion": "`filterDiff` 函數的邏輯從正表達式比對改為 `startsWith`並將其呼叫從 `getPRDiff` 移至 `main.js`。雖然 `startsWith` 可能更高效精確,但這是一個行為變更與職責重分配。請確保為 `filterDiff` 函數撰寫足夠的單元測試,以驗證:\n1. 正確過濾 `.gitea/` 路徑下的檔案。\n2. 不會錯誤過濾非 `.gitea/` 路徑下的檔案。\n3. 處理空 diff 內容。\n4. 處理僅包含 `.gitea/` 檔案的 diff 內容(應返回空字串)。",
"is_new": true
}
]
+15 -20
View File
@@ -18,25 +18,31 @@ export async function analyzeWithRole(role, diff) {
}
/**
* 讀取舊 findings(從 workspace 的 FINDINGS_PATH
* 讀取 JSON 陣列檔案,失敗或不存在時回傳空陣列
*/
export function loadOldFindings(workspace) {
const fullPath = path.join(workspace, FINDINGS_PATH);
function readJSONArray(fullPath, label) {
if (!fs.existsSync(fullPath)) {
console.log(' 舊 findings 檔案不存在,視為空');
console.log(` ${label}檔案不存在,視為空`);
return [];
}
try {
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
const old = (Array.isArray(data) ? data : []).map(f => ({ ...f, is_new: false }));
console.log(` 讀取舊 findings: ${old.length}`);
return old;
return Array.isArray(data) ? data : [];
} catch (e) {
console.log(` ⚠️ 讀取舊 findings 失敗: ${e.message},視為空`);
console.log(` ⚠️ 讀取${label}失敗: ${e.message},視為空`);
return [];
}
}
/**
* 讀取舊 findings(從 workspace 的 FINDINGS_PATH
*/
export function loadOldFindings(workspace) {
const old = readJSONArray(path.join(workspace, FINDINGS_PATH), '舊 findings ').map(f => ({ ...f, is_new: false }));
console.log(` 讀取舊 findings: ${old.length}`);
return old;
}
/**
* 合併新舊 findings,以 (role + location + suggestion前50字) 為 key 去除重複
*/
@@ -97,20 +103,9 @@ export async function deduplicateWithAI(findings) {
* 讀取排除問題檔案(從 workspace 的 EXCLUSIONS_PATH
*/
export function loadExclusions(workspace) {
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
if (!fs.existsSync(fullPath)) {
console.log(' 排除問題檔案不存在,跳過過濾');
return [];
}
try {
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
const exclusions = Array.isArray(data) ? data : [];
const exclusions = readJSONArray(path.join(workspace, EXCLUSIONS_PATH), '排除問題');
console.log(` 讀取排除問題: ${exclusions.length}`);
return exclusions;
} catch (e) {
console.log(` ⚠️ 讀取排除問題失敗: ${e.message},跳過過濾`);
return [];
}
}
/**
+2 -2
View File
@@ -3,6 +3,8 @@ import fs from 'fs';
import path from 'path';
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
function makeRunner(spawn) {
return function run(args, cwd, env) {
const opts = { cwd, encoding: 'utf8' };
@@ -30,7 +32,6 @@ function withAskpass(workspace, fn) {
*/
export function cloneRepo(workspace, _spawnSync = spawnSync) {
const run = makeRunner(_spawnSync);
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
const repoDir = path.join(workspace, 'repo');
return withAskpass(workspace, credEnv => {
@@ -48,7 +49,6 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) {
export async function commitAndPush(workspace, _spawnSync = spawnSync) {
const run = makeRunner(_spawnSync);
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
try {
const repoDir = cloneRepo(workspace, _spawnSync);
+30 -5
View File
@@ -2,13 +2,10 @@ 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');
const { getPRDiff, filterDiff, postComment } = await import('./gitea.js');
it('getPRDiff calls Gitea diff API with Authorization header', async () => {
let capturedUrl, capturedOpts;
@@ -48,7 +45,6 @@ describe('gitea', async () => {
return { data: '' };
});
await getPRDiff();
// httpsAgent is undefined when GITEA_SKIP_TLS_VERIFY !== 'true'
assert.equal(capturedOpts.httpsAgent, undefined);
});
@@ -62,3 +58,32 @@ describe('gitea', async () => {
await assert.rejects(() => postComment('test'), /api error/);
});
});
describe('filterDiff', async () => {
const { filterDiff } = await import('./gitea.js');
const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`;
it('filters out .gitea/ blocks', () => {
const diff = block('.gitea/workflows/review.yaml') + block('src/index.js');
const result = filterDiff(diff, ['.gitea/']);
assert.ok(!result.includes('.gitea/'));
assert.ok(result.includes('src/index.js'));
});
it('does not filter non-.gitea/ blocks', () => {
const diff = block('src/index.js') + block('README.md');
const result = filterDiff(diff, ['.gitea/']);
assert.equal(result, diff);
});
it('returns empty string when all blocks are excluded', () => {
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json');
const result = filterDiff(diff, ['.gitea/']);
assert.equal(result, '');
});
it('returns empty string for empty diff', () => {
assert.equal(filterDiff('', ['.gitea/']), '');
});
});