Author SHA1 Message Date
Jeffery 9e7f8a2a0a chore(ai-review 狀態): 回寫本輪已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 3s
CI / TEST (Claude) (pull_request) Successful in 27s
CI / TEST (Antigravity) (pull_request) Successful in 48s
CI / TEST (Codex) (pull_request) Successful in 3m18s
2026-07-21 14:24:14 +08:00
Jeffery 8b50a0d643 test(ai-review): 補上建問題模式結果檔測試 2026-07-21 14:24:10 +08:00
Jeffery 555611db07 fix(ai-review): 避免建問題模式嚴重結果 fail-open 2026-07-21 14:24:04 +08:00
ai-review-bot 24b248884f chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Failing after 26s
CI / TEST (Codex) (pull_request) Failing after 32s
CI / TEST (Antigravity) (pull_request) Failing after 46s
2026-07-21 05:50:00 +00:00
Jeffery e0ae6f886f chore(ai-review 狀態): 回寫已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 5s
CI / TEST (Claude) (pull_request) Successful in 28s
CI / TEST (Antigravity) (pull_request) Successful in 52s
CI / TEST (Codex) (pull_request) Successful in 2m58s
2026-07-21 13:46:48 +08:00
Jeffery 55b349da07 test(ai-review): 補上安全與 Gitea 契約測試 2026-07-21 13:46:43 +08:00
Jeffery 6a26984bee fix(ai-review): 強化安全防護與建問題留言處理 2026-07-21 13:46:39 +08:00
15 changed files with 466 additions and 530 deletions
+110
View File
@@ -1582,5 +1582,115 @@
"endLine": 370,
"problem": "建問題模式下把 `others` 交給 `review.postOthersToIssue` 逐條發 issue 留言,這是在拿遠端 API latency 燒時間。警告/建議通常可能比嚴重問題多很多,若有 50 條、每次 Gitea API 往返 200ms,光留言就可能多花 10 秒以上,還會增加 rate limit 壓力。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出建問題模式將警告/建議逐條發成 issue 留言,會造成 O(n) 遠端 POST、增加 CI 時間與 rate limit 壓力。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "action.yml",
"startLine": 19,
"endLine": 24,
"problem": "文件鼓勵呼叫端傳入「能觸發 CI 的 PAT」。這把原本自動 token 的防遞迴與範圍限制換成長效、較高權限的秘密。若 workflow 會在不受信任的 PR 程式碼或可被 PR 修改的 action 版本上執行,攻擊者可以在 CI 中讀取或外送 PAT,接著用它 push commit、改 issue、或觸發更多 workflow。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 action.yml 建議使用可觸發 CI 的 PAT 所帶來的長效高權限憑證風險提出相同問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 2,
"endLine": 3,
"problem": "檔頭 `更新時間` 仍寫著 `2026/07/17 18:49:58`,但本次變更描述顯示此檔最後更新為 `2026/07/20 17:24:18`。文件時間戳若成了舊拍子,讀者會開始懷疑整份 manifest 的註解是否也同樣過期。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手動更新時間容易過期且與實際內容不同。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 7,
"endLine": 7,
"problem": "啟動 banner 的 `更新時間` 被改成 `2026/07/17 18:49:58`,但這次主流程明顯新增了建 issue、延後解決舊留言等大量段落。這個時間戳像留在舊譜上的小節號,和目前程式內容不再合拍。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動 banner 的手動更新時間與程式實際變更不同,屬同一問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 390,
"problem": "`main()` 這次把建問題模式的暫存留言、issue 建立、標籤挑選、PR 回貼、相依設定、一般模式舊留言清理全部塞進同一個流程函式。半年後要改任何一個輸出通道時,維護者必須重新推演 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds` 與步驟 2 延後執行之間的狀態關係,這會讓主流程變成難以安全修改的編排巨石。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。main() 內建問題模式狀態、留言路由、issue 建立與發布職責耦合,已由既有排除事項與歷史 findings 涵蓋。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 199,
"endLine": 232,
"problem": "`queueOrPostComment` 透過閉包同時讀寫 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds`,回傳型別也會依狀態變成 `Object|null`。這種隱性狀態機短期能跑,但未來要追「這則留言到底會發到 PR、issue,還是只進 buffer」時,必須靠讀整個 `main()` 的執行順序才能理解。",
"reason": "Paladin:可排除(重複)。queueOrPostComment 的閉包狀態與留言目的地隱性分流,與既有 main() 留言路由、issue 狀態與緩衝佇列耦合問題相同。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "readme.md",
"startLine": 76,
"endLine": 131,
"problem": "README 的功能列表繼續維護大量硬編碼到 `develop` 分支與精確行號的連結。這次 diff 已經為了行號漂移更新一整片表格;之後任何新增註解、移動函式或插入測試都會讓文件連結過期,維護成本會隨每次重構累積。",
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 develop 分支與精確行號連結,會隨程式碼移動失準。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 409,
"endLine": 418,
"problem": "建問題模式下若有嚴重問題但 `exclusions.json` 沒變更,`filesToCommit` 會是空陣列,流程會走到「略過 commit/push」後直接 `return 0`。最小重現:`create-issue=true`、攻擊方保留 1 條 `severity: '嚴重'`、防守方沒有排除任何 finding,因此 `exclusionsChanged=false`。此時本輪檢查成功,且沒有推出 `[ai-review-bot][failure]` commit 觸發下一輪步驟 1 回報失敗;若 issue dependency API 未啟用或設定失敗,嚴重問題不會阻擋合併。",
"reason": "Paladin:可排除(重複)。與 F001 指涉同一個建問題模式下嚴重 finding 可能未產生可阻擋失敗訊號、最後回傳成功的問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 323,
"endLine": 390,
"problem": "建問題模式的核心流程新增了多個可觀察行為,但目前測試沒有驗證:有保留問題才建立 issue、無保留問題靜默通過、暫存的工具/diff/角色留言會 flush 到 issue、PR 只回貼 issue 連結、且只有嚴重問題才呼叫 `addIssueDependency`。這些都是本次 PR 新增/改動的主要行為,沒有試煉過就等於還沒完成。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式有問題才建 issue、無問題靜默通過、暫存留言 flush、PR 回貼與嚴重問題相依設定等核心分支缺少測試。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 190,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每步後重試 merge-base、淺層 repo 才 `--unshallow`、以及失敗診斷彙整,但新增測試只驗證不安全 `baseRef` 會被拒絕,沒有覆蓋這些成功與失敗路徑。這裡最怕的是淺層 checkout 或 PR head 歷史不足時,實際 CI 才發現 fetch 順序或 refspec 錯了。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、淺層/非淺層分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 303,
"endLine": 358,
"problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindingspushWithCredential 的 token 認證推送路徑、推送目標、錯誤遮蔽與安全行為缺少測試。"
}
]
@@ -8,19 +8,6 @@
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 210,
"endLine": 224,
"problem": "`ensureIssueCreated` 同時建立 issue、修改外層 `issue` 狀態、逐筆清空 `issueBuffer`,但整段流程沒有可重入或冪等機制。若 issue 建立成功後,寫入其中一則暫存留言時失敗,主流程會中止;重跑後又會建立另一個 issue,留下內容不完整的孤兒 issue。這種依賴閉包可變狀態的半完成狀態,半年後要加入重試、續傳或測試失敗情境都會很痛苦。",
"suggestion": "把「建立追蹤 issue 並沖刷留言」抽成獨立、可注入 Gitea client 的服務函式,明確回傳 issue 與已寫入進度;建立前以 PR 編號或隱藏識別標記查找既有追蹤 issue,讓重跑能接續而非重複建立。至少也應保留已建立的 issue 編號並在錯誤訊息中回報,避免留下無法追蹤的半成品。",
"suggestedCode": ""
},
{
"id": "F002",
"reviewer": "Maya",
@@ -34,19 +21,6 @@
"suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 188,
"endLine": 191,
"problem": "新增的 `addLabelsToIssue` 沒有對應測試,尚未驗證空值捷徑與實際 API 請求格式。這個函式位於新建問題流程的收尾路徑;若 endpoint、HTTP method 或 `{ labels }` payload 不符預期,追蹤 issue 將無法取得標籤,而空陣列是否真的不發出請求也未被保護。",
"suggestion": "新增單元測試,分別傳入 `undefined`、`null`、空陣列及多個 label id;斷言前三者回傳 `null` 且完全不呼叫 API,多個 id 時以 POST 呼叫正確的 ownerrepoissue endpoint 並傳送 `{ labels: [...] }`,另驗證 API 拋錯會原樣往上傳遞。",
"suggestedCode": ""
},
{
"id": "F004",
"reviewer": "Maya",
@@ -59,19 +33,6 @@
"problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。",
"suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 17,
"endLine": 45,
"problem": "新增的 agent 失敗診斷涵蓋逾時、數字或字串 exit code、signal、空輸出及 500 字截斷等多個邊界,但沒有測試鎖定輸出。這些資訊只在失敗路徑出現,正常審查不會自然覆蓋;日後修改時可能悄悄遺失真正的 CLI 錯誤內容,或破壞單行與截斷約束。",
"suggestion": "將摘要邏輯匯出供測試,或透過失敗的 `runAttackers``runDefenders``fillPurposes` 測試間接斷言 log。至少覆蓋 killed、數字 exit code、字串 code、signal、stderr 與 stdout 同時存在、超過 500 字、完全無資訊,以及 `res` 為 nullundefined 的案例。",
"suggestedCode": ""
}
],
"excluded": []
@@ -8,32 +8,6 @@
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 72,
"endLine": 81,
"problem": "開啟 ACTIONS_STEP_DEBUG=true 後,會把攻擊者可間接操控的 AI CLI stderrstdout(經黑名單式 redactSecrets 遮罩)寫入長期保存的 CI log。短 token、JWT、含標點或空白的密碼、非典型金鑰及 PII 仍可能繞過遮罩。建議即使除錯模式也只記錄退出碼/signal/逾時/診斷 ID,若需原文則寫入受限、短期保存、需授權取得的安全 artifact。",
"suggestion": "CI log 不輸出 AI CLI 原文,即使 debug 模式;需要原文時改寫入存取受限的安全 artifact,並套用允許清單式結構化診斷與 PII/機密掃描。此為安全性與可除錯性的政策取捨:現行已刻意保留 debug-gated 遮罩輸出,是否完全移除需維護者裁示。",
"suggestedCode": "function agentFailureDetail(res) {\n const err = res && res.error;\n if (err && err.killed) return '已逾時終止';\n if (err && typeof err.code === 'number') return `exit ${err.code}`;\n if (err && err.signal) return `signal ${err.signal}`;\n return 'AI CLI 執行失敗(原始輸出已隱藏)';\n}"
},
{
"id": "F002",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 176,
"endLine": 218,
"problem": "main() 內新增 issueBuffer、可變 issue、postComment 與 ensureIssueCreated 閉包,後續多處依 ctx.createIssue 分支。留言路由、issue 生命週期、標籤、相依關係與審查編排共享同一批可變狀態;未來增加發布目的地或重試策略時須同步理解並修改整個超長主流程,測試也只能透過 main() 間接覆蓋。",
"suggestion": "抽出具明確介面的發布器(如 PrReviewPublisher 與 IssueReviewPublisher),封裝留言暫存、issue 建立、沖刷、問題明細與收束關聯;main() 只呼叫一致的 publishContextpublishFindingsfinalize,便於分別注入假的 Gitea client 測試兩種模式並移除散落的模式判斷。屬大範圍重構+設計取捨,需維護者確認方向。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Maya",
@@ -60,19 +34,6 @@
"suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabelsselectLabelscreateIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 215,
"problem": "新增的問題相依 API 包裝沒有測試驗證 endpoint、HTTP method 與 payload。特別是 addIssueDependency 容易顛倒的「PR 相依於 issue」方向未被斷言;若 index 或 body 欄位放反,合併阻擋語意會相反。(原併列的 addLabelsToIssue 已於本次移除。)",
"suggestion": "mock 底層 API,對相依關係使用不同的 PR/issue 編號,精確斷言 URL 指向 PR、body.index 指向阻擋來源 issue,並覆蓋 API 非 2xx 時錯誤原樣往上拋出的案例。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F006",
"reviewer": "Maya",
@@ -99,19 +60,6 @@
"suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F008",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 80,
"problem": "redactSecretsagentFailureDetail 直接決定 CI 日誌是否洩漏內容及失敗診斷是否可用,但沒有測試覆蓋。空值、控制字元、Authorization/Bearer、URL 帳密、各種 token 樣式、截斷,以及 ACTIONS_STEP_DEBUG 大小寫與未啟用時隱藏 stdout/stderr 等邊界尚未驗證。",
"suggestion": "為這兩個純函式補單元測試(必要時受控匯出或抽獨立模組),以假憑證逐一測試遮罩規則與換行注入並斷言輸出不含原始秘密;保存還原 ACTIONS_STEP_DEBUG 驗證預設/true/混合大小寫/逾時/exit codesignal/無 error/超長輸出。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F009",
"reviewer": "Bard",
@@ -124,19 +72,6 @@
"problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。",
"suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。",
"suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token'\n required: false\n default: ''"
},
{
"id": "F010",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 203,
"endLine": 226,
"problem": "ensureIssueCreated 之名帶有「已存在便沿用」的冪等語意,實際卻無條件建立新 issue 並悄悄改寫外層 issue;名稱、行為與副作用不一致,閱讀呼叫處易形成錯誤預期。",
"suggestion": "若此函式只允許呼叫一次,改用直接表達「建立並沖刷暫存留言」的名稱並回傳建立結果,由呼叫端明確指派 issue,讓資料流一眼可見。註:本項與 F002(抽出 publisher 大重構)指向同一段核心流程、維護者正審視中,宜與該重構一併處理,避免重複改動。",
"suggestedCode": "const createIssueAndFlushBuffer = async (labelIds = []) => {\n const createdIssue = await gitea.createIssue(ctx, {\n title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,\n body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),\n labels: labelIds,\n });\n for (const body of issueBuffer) {\n await gitea.createCommentOnIssue(ctx, createdIssue.number, body);\n }\n issueBuffer.length = 0;\n return createdIssue;\n};\nissue = await createIssueAndFlushBuffer(labelIds);"
}
],
"excluded": []
@@ -21,19 +21,6 @@
"suggestion": "共用函式 JSDoc 改以語意階段名稱描述、不引用易變動的數字;日誌集中定義階段名稱或由單一流程描述產生編號;README 流程圖也以語意名稱為主。屬跨檔重構+設計取捨。",
"suggestedCode": "const STAGE = Object.freeze({\n TOOL_DETECTION: '偵測工具',\n DIFF_COLLECTION: '整理差異',\n ATTACK_REVIEW: '攻擊方審查',\n DEFENSE_REVIEW: '防守方裁決',\n});\nlog(STAGE.DIFF_COLLECTION, 'INF', message);"
},
{
"id": "F002",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 172,
"endLine": 215,
"problem": "建立 issue 相依關係的 API 封裝(addIssueDependency)沒有對應測試:尚未驗證 URL 的 issue 編號與 {index, owner, repo} payload 正確、以及非 2xx 錯誤是否原樣往上傳遞。(原併提的 addLabelsToIssue 已於先前 commit 移除。)",
"suggestion": "mock 底層 API,驗證 addIssueDependency 的 URL issue 編號與 payload,加入 4xx/5xx 拋錯案例,並搭配主流程測試確認相依失敗會被降級而不阻斷審查。屬測試架構決策(專案無測試框架)。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Leo",
@@ -33,32 +33,6 @@
"problem": "commitAndPushFindings 的推送行為(有無變更、認證方式、空 commit 防護、是否觸發 CI)缺少測試證明。此為本次變更的核心行為,卻沒有任何測試覆蓋。",
"suggestion": "mock git 命令,斷言:無 staged diff 時回傳 false 且不執行 commit/push;有變更時以認證方式推送到正確 refspec 與分支。注意:原 finding 描述的 pushToken 對比 origin 雙軌邏輯已於重構後移除(現行一律以 token 經 pushWithCredential 認證推送),撰寫測試前需依現行程式碼重新界定情境。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "🗡️ Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 64,
"endLine": 80,
"problem": "agentFailureDetail 於 AI CLI 失敗時會把(經 redactSecrets 盡力遮罩的)stderrstdout 片段寫入 CI log。redactSecrets 屬盡力遮罩,無法可靠辨識 PII、短密碼或私鑰片段;長期保存且多人可讀的 CI log 有洩漏風險。",
"suggestion": "屬安全(避免洩漏)與可除錯性的設計取捨:現行程式碼已於註解明確權衡並選擇「附上遮罩後輸出以利除錯」。是否改為只記錄退出碼/訊號/逾時狀態+隨機診斷 ID(內容級診斷改寫入有存取控制與短保存期的獨立 artifact)需由維護者裁示,故保留現行行為、標為待人工處理。",
"suggestedCode": "if (verbose) {\n parts.push('已啟用除錯;為避免洩漏原始碼、PII 或憑證,CLI 輸出仍不寫入日誌');\n}"
},
{
"id": "F004",
"reviewer": "🧪 Maya",
"focus": "testing",
"badge": "🧪",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 215,
"problem": "addLabelsToIssue 與 addIssueDependency 缺少契約測試驗證 endpoint、HTTP method 與 request body;相依關係方向由 URL 與 body 決定,參數次序寫反時粗略 mock 的主流程測試不易察覺。",
"suggestion": "補 Gitea client 單元測試:labels 為空或缺少時不呼叫 API 並回傳 null;有 labels 時送出正確陣列;相依 API 以 PR 編號置於 URL、追蹤 issue 編號置於 index,並帶入正確 ownerrepo。",
"suggestedCode": ""
}
],
"excluded": []
@@ -36,48 +36,6 @@
"suggestedCode": "```\n| 功能名稱 | 功能描述 |\n| --- | --- |\n| [gitrepo.resolveMergeBase](src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |\n```",
"sourceIssue": 12
},
{
"id": "F003",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 82,
"problem": "`agentFailureDetail()` 同時負責解析程序失敗、決定 debug 政策、讀取全域環境變數、截斷輸出及遮罩機密。尤其直接讀取 `process.env.ACTIONS_STEP_DEBUG` 形成隱藏相依,測試不同輸出政策時必須修改程序全域狀態;日後若其他呼叫端需要不同診斷層級,也只能繼續往這個函式堆條件。",
"suggestion": "把診斷政策改成明確參數,並將「錯誤中繼資料整理」與「輸出片段清理」拆成小函式;在 `main` 或 context 載入階段解析環境設定後注入。這能讓各種 exit code、signal、空輸出與 verbose 模式以純輸入輸出直接測試。",
"suggestedCode": "```\nfunction agentFailureDetail(res, { includeStdout = false, inputLimit = 2000, outputLimit = 500 } = {}) {\n const parts = failureMetadata(res && res.error);\n appendSanitizedOutput(parts, 'stderr', res && res.stderr, inputLimit, outputLimit);\n if (includeStdout) {\n appendSanitizedOutput(parts, 'stdout', res && res.output, inputLimit, outputLimit);\n }\n return parts.length ? parts.join('') : 'AI CLI 執行失敗(無診斷輸出)';\n}\n```",
"sourceIssue": 12
},
{
"id": "F004",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 42,
"endLine": 61,
"problem": "`agentFailureDetail` 的文件自相矛盾:開頭宣稱「原始輸出預設隱藏」,後文卻明言預設附上經遮罩的 `stderr` 與 `stdout` 片段;另提到 `ACTIONS_STEP_DEBUG`,函式內卻沒有相應分支。註解與實作各唱各的調,維護者無法從文件判斷實際日誌行為。",
"suggestion": "統一文件敘述為實際行為:預設輸出經遮罩且限長的診斷片段;若目前並未依 `ACTIONS_STEP_DEBUG` 改變輸出,就移除該段說明,或待真正實作開關後再補上。",
"suggestedCode": "",
"sourceIssue": 13
},
{
"id": "F005",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 216,
"endLine": 218,
"problem": "在 `ensureIssueCreated` 函式中,使用 `for...of` 迴圈搭配 `await` 來逐條對 Gitea API 發送留言請求。由於網路請求存在延遲(每次 RTT 約 100-300ms),在迴圈內阻塞式等待會導致整體執行時間隨留言數量線性增加,浪費大量 CPU 週期與網路連線資源。",
"suggestion": "可以將 `issueBuffer` 中的所有情境留言合併為單一 Markdown 留言發送,僅需一次 API 呼叫;或者使用 `Promise.all` 將這些無相依性的留言請求並行化發送,大幅降低總延遲。",
"suggestedCode": "```\nif (issueBuffer.length > 0) {\n const combinedBody = issueBuffer.join('\\n\\n---\\n\\n');\n await gitea.createCommentOnIssue(ctx, issue.number, combinedBody);\n }\n issueBuffer.length = 0;\n```",
"sourceIssue": 14
},
{
"id": "F006",
"reviewer": "Rogue",
@@ -106,20 +64,6 @@
"suggestedCode": "```\n} catch (err) {\n // 過濾敏感資訊後保留錯誤細節\n const safeMessage = err.message ? redactSecrets(err.message) : '未知錯誤';\n const error = new Error(`推送審查結果 commit 失敗(${safeMessage})。`);\n error.cause = err;\n throw error;\n }\n```",
"sourceIssue": 14
},
{
"id": "F008",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 299,
"endLine": 299,
"problem": "在 `pushWithCredential` 中,環境變數 `GIT_CONFIG_KEY_0` 被動態拼接為 `http.${remoteUrl}.extraheader`。如果 `remoteUrl` 來自外部或未經嚴格驗證的 context,且 URL 中包含特殊字元(如點號、路徑分隔符號或引號等),可能會導致 Git 配置解析錯誤,或在特定平台下引發 Git 配置參數注入風險。",
"suggestion": "由於該執行程序僅為一次性的 `git push` 操作,可直接將該憑證應用於所有 HTTP 請求,將 `GIT_CONFIG_KEY_0` 設定為靜態的 `http.extraheader`,以避免動態拼接 URL 所帶來的注入風險。",
"suggestedCode": "```\nGIT_CONFIG_KEY_0: 'http.extraheader',\n```",
"sourceIssue": 14
},
{
"id": "F009",
"reviewer": "Bard",
@@ -148,20 +92,6 @@
"suggestedCode": "```\nconst commentPromise = gitea.createIssueComment(\n ctx,\n templates.issueLinkComment({\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n severeCount: severe.length,\n otherCount: others.length,\n })\n );\n const dependencyPromise = gitea.addIssueDependency(ctx, ctx.prNumber, issue.number)\n .then(() => log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`))\n .catch((err) => log('建問題', 'WRN', `設定 PR 相依失敗:${err.message}。`));\n \n await Promise.all([commentPromise, dependencyPromise]);\n```",
"sourceIssue": 14
},
{
"id": "F011",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 33,
"endLine": 34,
"problem": "在 redactSecrets 函式中,針對 authorization 以及其他憑證關鍵字(如 token、secret、password 等)的敏感資訊遮蔽,分別使用了兩條結構極為相似的正規表示式進行替換。這造成了重複的替換邏輯與額外的處理開銷,程式碼的旋律顯得不夠俐落。",
"suggestion": "建議將這兩條正規表示式合併為單一表達式,消除重複的 replace 呼叫,使程式碼更加簡潔優雅且提升運行效率。",
"suggestedCode": "```\n.replace(/((?:authorization|api[_-]?key|token|password|secret|bearer)\\s*[:=]\\s*)\\S+/gi, '$1***')\n```",
"sourceIssue": 14
},
{
"id": "F012",
"reviewer": "Rogue",
@@ -176,20 +106,6 @@
"suggestedCode": "```\nconst stderr = redactSecrets(String((res && res.stderr) || '').slice(0, 500));\n if (stderr) parts.push(`stderr${stderr}`);\n const stdout = redactSecrets(String((res && res.output) || '').slice(0, 500));\n if (stdout) parts.push(`stdout${stdout}`);\n```",
"sourceIssue": 14
},
{
"id": "F013",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 335,
"endLine": 337,
"problem": "issueFindingComment 函式的 @remarks 文件註解中,說明其使用情境為『建問題模式下 review.postSevereToIssue 把每條嚴重 finding... 作為問題明細的追蹤紀錄』。然而實際上,非嚴重的警告與建議(others)也會透過 review.postOthersToIssue 呼叫此模板進行發布,導致文件描述不夠完整。",
"suggestion": "修正 @remarks 的使用情境說明,將 postOthersToIssue 亦併入描述中,使 JSDoc 文件能如實且精準地反映實際程式碼的呼叫情境。",
"suggestedCode": "```\n* 使用情境:建問題模式下 `review.postSevereToIssue` 與 `review.postOthersToIssue`src/lib/review.js)把保留的各級問題明細\\n * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上,\\n * 作為問題明細的追蹤紀錄。\n```",
"sourceIssue": 14
},
{
"id": "F014",
"reviewer": "Leo",
@@ -218,90 +134,6 @@
"suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 15
},
{
"id": "F016",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 28,
"endLine": 37,
"problem": "這段註解的旋律前後打架:開頭說「原始輸出預設隱藏」,後文卻說失敗時預設附上遮罩後的 stderr/stdout 片段。讀者才剛建立心智模型,下一拍就被改調。",
"suggestion": "請讓摘要句與實際行為一致,明確說明「原始輸出不直接輸出,但會輸出遮罩與截斷後的診斷片段」。",
"suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上遮罩、去控制字元且截斷後的 stderr/stdout 片段。\n```",
"sourceIssue": 15
},
{
"id": "F017",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 334,
"endLine": 341,
"problem": "`issueFindingComment` 的註解只提到 `review.postSevereToIssue`,但主流程也以 `postOthersToIssue` 發布警告與建議。文件把共用模板寫成嚴重問題專用,讀起來像少了一個聲部。",
"suggestion": "把 remarks 改成同時涵蓋嚴重、警告與建議的 issue 留言產生器,避免維護者誤以為它只服務嚴重 finding。",
"suggestedCode": "",
"sourceIssue": 15
},
{
"id": "F018",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 149,
"problem": "攻擊者可以透過提交惡意 Pull Request,將 `baseRef`PR 目標分支)命名為包含路徑穿越(Path Traversal)的字串,例如 `../../hooks/pre-push`。由於 `resolveMergeBase` 直接將 `baseRef` 拼接至 `git fetch` 的 Refspec 參數中(例如 `+refs/heads/${baseRef}:refs/remotes/origin/${baseRef}`),這將導致 `git fetch` 寫入至 `.git/refs/remotes/origin/../../hooks/pre-push`(即 `.git/hooks/pre-push`)。這會覆寫或建立 Git Hook,並在後續執行 git 操作時自動觸發該惡意 Hook,從而造成遠端程式碼執行(RCE)。同樣地,`commitAndPushFindings` 函數中的 `headRef` 也存在類似的拼接風險。",
"suggestion": "在將 `baseRef` 與 `headRef` 傳入 git 指令之前,應進行嚴格的合法性檢查。建議使用正則表達式限制分支名稱僅能包含安全的字元(如英數字、斜線、底線、連字號、句點),且絕對不得含有 `..` 或以 `-` 開頭,必要時亦可使用 `git check-ref-format` 命令先行驗證該分支名稱是否安全。",
"suggestedCode": "```\nfunction resolveMergeBase(cwd, baseRef) {\n // 嚴格的分支名稱白名單檢查,防止路徑穿越與參數注入\n const safeBranchRegex = /^(?!-)(?!.*?\\.\\.)[a-zA-Z0-9/_.-]+$/;\n if (!safeBranchRegex.test(baseRef)) {\n throw new Error(`偵測到不合法的分支名稱: ${baseRef}`);\n }\n const remoteBase = `origin/${baseRef}`;\n```",
"sourceIssue": 16
},
{
"id": "F019",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 72,
"problem": "`redactSecrets()` 與 `agentFailureDetail()` 是低階日誌診斷/遮罩邏輯,現在放在 `review.js` 這個負責 diff 整理與審查決策的模組頂端。這會讓 `review.js` 的職責繼續膨脹:未來若其他模組也要安全輸出 CLI 錯誤,只能複製這段或反向依賴 review 模組,邊界會越來越不清楚。",
"suggestion": "把這兩個函式搬到專門的工具模組,例如 `src/lib/diagnostics.js` 或 `src/lib/log-redaction.js`,並由 `review.js` 引入。這樣遮罩規則可集中測試與重用,`review.js` 也能維持在「審查流程資料處理」的邊界內。",
"suggestedCode": "",
"sourceIssue": 17
},
{
"id": "F020",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 41,
"endLine": 45,
"problem": "這段註解的旋律前後走調:摘要先說「原始輸出預設隱藏」,下一段卻說失敗時「預設附上 stderr 與 stdout」。讀者還沒進函式本體,文件本身就已經互相拉扯。",
"suggestion": "請讓摘要與實作同拍,直接說明會輸出經遮罩與限長的診斷片段;若真的要隱藏原始輸出,也應同步改實作。",
"suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上經遮罩與限長的 stderrstdout 片段。\n```",
"sourceIssue": 17
},
{
"id": "F021",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 334,
"endLine": 338,
"problem": "`issueFindingComment` 的文件只唱「嚴重 finding」,但新版流程也讓警告與建議逐條發到 issue。函式名稱是通用的,註解卻把用途寫窄,後續讀者會誤以為它只服務嚴重問題。",
"suggestion": "把註解改成涵蓋所有 finding 等級,讓文件與函式名稱、呼叫情境保持一致。",
"suggestedCode": "```\n* 使用情境:建問題模式下,`review.postSevereToIssue` 與 `review.postOthersToIssue`\n * 會把各等級 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue`\n * 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。\n```",
"sourceIssue": 17
},
{
"id": "F022",
"reviewer": "Leo",
@@ -386,20 +218,6 @@
"suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```",
"sourceIssue": 19
},
{
"id": "F028",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 14,
"endLine": 75,
"problem": "`review.js` 這次新增 `redactSecrets()` 與 `agentFailureDetail()`,但這兩個函式處理的是 AI CLI 執行失敗診斷與機密遮罩,責任更接近 `agents.js` 或共用 log/sanitize 工具。現在審查結果整理模組同時負責 diff、裁決、issue 發文與 CLI 診斷格式,模組邊界越來越鬆;之後其他地方若也要記錄 agent 失敗,很容易複製一份遮罩邏輯或反向依賴 `review.js`。",
"suggestion": "將這兩個函式移到 `src/lib/agents.js`(例如匯出 `formatAgentFailure()`),或新增 `src/lib/sanitize.js`/`src/lib/diagnostics.js`。`review.js` 只消費格式化後的錯誤摘要,避免讓審查編排模組承擔 CLI 診斷細節。",
"suggestedCode": "",
"sourceIssue": 19
},
{
"id": "F029",
"reviewer": "Bard",
@@ -414,20 +232,6 @@
"suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```",
"sourceIssue": 19
},
{
"id": "F030",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/index.js",
"startLine": 369,
"endLine": 370,
"problem": "建問題模式把所有警告/建議改成逐條發 issue 留言,這裡會把 `others.length` 放大成 N 次遠端 POST;正常模式同一批資料只產生 1 則彙整表格留言。只要 AI 回出數十條警告,CI 時間就會被 API round-trip 線性吃掉,還更容易撞上 Gitea rate limit 或暫時性網路延遲。",
"suggestion": "警告/建議維持批次彙整成單一留言;只有嚴重問題需要逐條追蹤時再拆開。若產品需求一定要逐條回覆,至少在 `postOthersToIssue` 內用有上限的並行池,不要一筆等一筆。",
"suggestedCode": "```\nif (others.length > 0) {\n if (ctx.createIssue) {\n await gitea.createCommentOnIssue(ctx, issue.number, templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布到 issue${others.length} 條)。`);\n } else {\n await postComment(templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);\n }\n}\n```",
"sourceIssue": 20
},
{
"id": "F031",
"reviewer": "Leo",
@@ -470,90 +274,6 @@
"suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 21
},
{
"id": "F034",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 36,
"endLine": 43,
"problem": "`agentFailureDetail` 的 JSDoc 先說「原始輸出預設隱藏」,下一段又說「預設附上 stderr 與 stdout 片段」。同一段說明前後轉調,讀者會搞不清楚失敗診斷到底會不會輸出 CLI 內容。",
"suggestion": "請統一描述:若設計是輸出已遮罩片段,就刪掉「預設隱藏」;若設計是隱藏原始輸出,就把後段改成條件式說明。",
"suggestedCode": "",
"sourceIssue": 21
},
{
"id": "F035",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 334,
"endLine": 338,
"problem": "`issueFindingComment` 是通用 finding 留言模板,但更新後的說明只寫 `review.postSevereToIssue` 與「嚴重 finding」。然而主流程也將警告/建議逐條發到 issue,這段文件把模板唱窄了,和實際用途不一致。",
"suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的通用 issue finding 留言,避免日後維護者誤以為此模板只能用於嚴重問題。",
"suggestedCode": "",
"sourceIssue": 21
},
{
"id": "F036",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/index.js",
"startLine": 305,
"endLine": 305,
"problem": "攻擊方沒有找出任何 finding 時,這裡還是照樣啟動防守方 `runDefenders`。空陣列沒有東西可裁決,卻會多跑一輪 AI CLI/sub agent、讀 exclusions/history、組 prompt;每個乾淨 PR 都被偷走 1 次防守方呼叫的 CPU、等待時間與 token。",
"suggestion": "在 `findings.length === 0` 時直接略過防守方裁決,令 `kept/excluded` 都是空陣列,直接進入保存結果與收尾。這不是微優化,是整輪 AI 呼叫直接歸零。",
"suggestedCode": "```\nconst { kept, excluded } = findings.length === 0\n ? { kept: [], excluded: [] }\n : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });\n```",
"sourceIssue": 22
},
{
"id": "F037",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitrepo.js",
"startLine": 310,
"endLine": 310,
"problem": "`pushWithCredential` 的參數名叫 `secret`,但同一檔其他區段與呼叫端都稱它為 `token`;同一個旋律忽然換調,讀者需要多花心力確認這是不是另一種憑證。",
"suggestion": "沿用既有命名,把 `secret` 改成 `token`,並同步調整 JSDoc 與 `Buffer.from` 內的引用。",
"suggestedCode": "```\nfunction pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {\n const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64');\n // ...\n}\n```",
"sourceIssue": 22
},
{
"id": "F038",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 31,
"endLine": 46,
"problem": "`agentFailureDetail` 的註解前後語意不一致:開頭寫「原始輸出預設隱藏」,但後段又說預設會附上遮罩後的 stderr/stdout;最後還提到用 `ACTIONS_STEP_DEBUG=true` 取得原始輸出,但程式碼沒有任何 debug flag 分支。這種文件與實作脫節,會讓未來維護者誤判 CI log 會暴露多少診斷內容。",
"suggestion": "把註解改成符合目前實作:預設輸出限長且遮罩後的 stderr/stdout;若要支援 debug 模式,再補實作分支。不要在註解承諾程式沒有做的行為。",
"suggestedCode": "",
"sourceIssue": 22
},
{
"id": "F039",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 337,
"endLine": 340,
"problem": "`issueFindingComment` 是一般 finding 留言模板,但註解只寫 `review.postSevereToIssue` 的嚴重問題用途;和主流程中「警告+建議也逐條發到 issue」的描述不一致,註解像只唱了半段副歌。",
"suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的共用用途,避免後續維護者誤以為此模板只服務嚴重問題。",
"suggestedCode": "",
"sourceIssue": 22
},
{
"id": "F040",
"reviewer": "Assassin",
@@ -581,20 +301,6 @@
"suggestion": "改用 repo 相對連結、不固定行號,或把這段功能表改由腳本從 JSDoc 自動產生。若需要連到特定實作,優先連到檔案或錨點,避免每次重排程式碼都要同步更新幾十個行號。",
"suggestedCode": "```\n| log.taipeiNow | [src/lib/log.js](src/lib/log.js) | 取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串 |\n| review.runAttackers | [src/lib/review.js](src/lib/review.js) | 攻擊方 sub agent 並行找問題並合併列表 |\n```",
"sourceIssue": 23
},
{
"id": "F042",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 31,
"endLine": 40,
"problem": "這段註解的旋律前後失和:開頭寫「原始輸出預設隱藏」,下一句卻說預設附上 stderr 與 stdout 片段。讀者尚未進入程式碼,就已被兩個互相拉扯的描述絆住。",
"suggestion": "請讓文件只唱一個調性:若目前設計是預設輸出遮罩後的診斷片段,就刪掉「原始輸出預設隱藏」或改成「原始輸出會先遮罩與截斷」。",
"suggestedCode": "",
"sourceIssue": 23
}
],
"excluded": []
+2 -1
View File
@@ -4,7 +4,8 @@
"description": "AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings",
"main": "src/index.js",
"scripts": {
"build": "ncc build src/index.js -o dist"
"build": "ncc build src/index.js -o dist",
"test": "node --test"
},
"author": "Jeffery",
"license": "MIT"
+12 -7
View File
@@ -210,7 +210,7 @@ async function main() {
};
/**
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
* 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
* 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
*
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
@@ -302,7 +302,9 @@ async function main() {
await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
const { kept, excluded } = findings.length === 0
? { kept: [], excluded: [] }
: await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
review.sortFindings(kept);
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
@@ -398,12 +400,15 @@ async function main() {
}
// ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)───────
// 一般模式:findingsexclusions.json;建問題模式:問題明細已在 issue 留言,只 commit exclusions.json。
// 一般模式:findingsexclusions.json;建問題模式通常只 commit exclusions.json。
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
const result = severe.length === 0 ? 'success' : 'failure';
const filesToCommit = ctx.createIssue ? [] : [relativePath];
if (exclusionsChanged) {
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
}
const filesToCommit = review.resultFilesToCommit({
createIssue: ctx.createIssue,
severeCount: severe.length,
relativePath,
exclusionsChanged,
});
if (filesToCommit.length > 0) {
commitFindings({ cwd, ctx, files: filesToCommit, result });
} else {
+71
View File
@@ -0,0 +1,71 @@
'use strict';
// AI CLI 失敗診斷與機密遮罩工具:供 review 流程記錄安全、限長的一行錯誤摘要。
// 遮罩前先截去的輸入上限,避免對數 MB 失敗輸出跑整份 O(k*n) 正規掃描。
const AGENT_DIAGNOSTIC_INPUT_LIMIT = 2_000;
// 每段診斷片段(stderr/stdout)寫入日誌的字元上限。
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
/**
* 遮罩診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
*
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
* `Authorization` 標頭、`token=``token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
* 長 hex`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
*
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
* 遮罩且去除控制字元的 stderr/stdout 片段。
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
}
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
module.exports = {
AGENT_DIAGNOSTIC_INPUT_LIMIT,
AGENT_DIAGNOSTIC_OUTPUT_LIMIT,
redactSecrets,
agentFailureDetail,
};
+42 -4
View File
@@ -4,6 +4,34 @@ const { execFileSync } = require('child_process');
// git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。
/**
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
*
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
* @returns {string} 原樣回傳通過驗證的分支名稱。
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
* @remarks
* 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的
* `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。
*/
function assertSafeBranchRef(refName, fieldName) {
const value = String(refName || '').trim();
if (!value) throw new Error(`${fieldName} 不可為空。`);
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
}
try {
execFileSync('git', ['check-ref-format', '--branch', value], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch {
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
}
return value;
}
/**
* 同步執行 git 指令並回傳原始 stdout 輸出。
*
@@ -102,6 +130,7 @@ function latestCommitSubject(cwd) {
* 避免把 base 分支後續演進誤算進 diff。
*/
function resolveMergeBase(cwd, baseRef) {
baseRef = assertSafeBranchRef(baseRef, 'baseRef');
const remoteBase = `origin/${baseRef}`;
const diagnostics = [];
// 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。
@@ -251,6 +280,7 @@ function fileLastUpdatedIso(cwd, file) {
* 例外把命令列(含 token)回顯到 CI log 或程序清單。
*/
function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) {
headRef = assertSafeBranchRef(headRef, 'headRef');
const current = gitTrim(cwd, 'rev-parse', 'HEAD');
if (headSha && current !== headSha) {
git(cwd, 'checkout', '--detach', headSha);
@@ -294,17 +324,22 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
*
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
* @param {string} secret - 具 push 權限的 tokenPAT(作為 Basic 認證的密碼)。
* @param {string} token - 具 push 權限的 tokenPAT(作為 Basic 認證的密碼)。
* @param {string} refspec - push 的 refspec(形如 `HEAD:refs/heads/<branch>`)。
* @param {string} serverUrl - Gitea 伺服器根網址(用於定位 checkout 持久化 extraheader 的 scope)。
* @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。
* @throws {Error} 推送失敗時拋出固定訊息(已隱藏遠端 URL 與認證資訊)。
* @remarks 本函式未匯出,僅供 {@link commitAndPushFindings} 使用。
*/
function pushWithCredential(cwd, remoteUrl, secret, refspec, serverUrl) {
const basic = Buffer.from(`ai-review-bot:${secret}`).toString('base64');
function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {
const server = new URL(serverUrl);
const remote = new URL(remoteUrl);
if (remote.origin !== server.origin || !remote.pathname.endsWith('.git')) {
throw new Error('推送遠端 URL 與 Gitea 伺服器不相符,已停止推送。');
}
const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64');
// checkout 持久化自動 token 的 scope 為 `http.<serverUrl>/.extraheader`(結尾帶斜線)。
const headerScope = `http.${serverUrl.replace(/\/+$/, '')}/.extraheader`;
const headerScope = `http.${server.origin}/.extraheader`;
try {
execFileSync('git', ['push', remoteUrl, refspec], {
cwd,
@@ -333,4 +368,7 @@ module.exports = {
fileDiff,
fileLastUpdatedIso,
commitAndPushFindings,
__test: {
assertSafeBranchRef,
},
};
+30 -78
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { log, taipeiFromIso, taipeiNow } = require('./log');
const { runAgent, extractJson } = require('./agents');
const { agentFailureDetail } = require('./diagnostics');
const templates = require('./templates');
// 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
@@ -13,78 +14,6 @@ const templates = require('./templates');
const PER_FILE_DIFF_LIMIT = 16_000;
const TOTAL_DIFF_LIMIT = 160_000;
// AI CLI 失敗診斷輸出政策常數(agentFailureDetail 使用):與上方送審上限並列於模組頂層,
// 集中管理長度政策,避免藏在函式中段。
// INPUT_LIMIT:遮罩前先截去的輸入上限——先截再跑 redactSecrets,避免對數 MB 失敗輸出跑整份
// O(k×n) 正規掃描;2000 字已足以涵蓋跨界機密樣式。
const INPUT_LIMIT = 2_000;
// AGENT_DIAGNOSTIC_OUTPUT_LIMIT:每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策。
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
/**
* 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
*
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
* `Authorization` 標頭、`token=``token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
* 長 hex`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
*
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
* @remarks 本函式未匯出,僅供模組內部使用。
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式
* 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr 與 stdout**
* 片段(各先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI 為何失敗,
* 且部分 CLI(如 claude-code 的 `-p` 模式)將錯誤寫到 stdout 而非 stderr。純函式、不拋例外。
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
* @remarks
* 使用情境:{@link runAttackers}{@link runDefenders}{@link fillPurposes}{@link selectLabels}
* 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗;
* 需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑。
* 本函式未匯出,僅供模組內部使用。
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 先截去過長輸入(INPUT_LIMIT)再遮罩,最終仍截為 AGENT_DIAGNOSTIC_OUTPUT_LIMIT;長度政策常數見模組頂層。
// 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時
// 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的
// -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
/**
* 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。
*
@@ -628,6 +557,28 @@ function appendExclusions({ cwd, excluded, prNumber }) {
return true;
}
/**
* 依審查模式與結果決定收尾要提交的結果檔。
*
* 一般模式永遠提交本回合 findings 檔;建問題模式通常把問題明細留在 issue,不提交
* findings。例外是有嚴重問題時仍提交 findings 檔,讓 `[failure]` 結果 commit 一定能產生,
* 避免問題相依 API 不支援或設定失敗時 PR 缺少失敗檢查。
*
* @param {Object} params - 解構參數。
* @param {boolean} params.createIssue - 是否啟用建問題模式。
* @param {number} params.severeCount - 嚴重 finding 數量。
* @param {string} params.relativePath - 本回合保存的 findings 檔 repo 相對路徑。
* @param {boolean} params.exclusionsChanged - exclusions.json 是否有實際異動。
* @returns {string[]} 應交給 `commitFindings` 的 repo 相對路徑清單。
*/
function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusionsChanged }) {
const files = createIssue ? (severeCount > 0 ? [relativePath] : []) : [relativePath];
if (exclusionsChanged) {
files.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
}
return files;
}
/**
* 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
*
@@ -739,9 +690,9 @@ ${JSON.stringify(brief)}
* 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。
*/
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
for (const finding of severe) {
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
}
await Promise.all(
severe.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))),
);
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}`);
}
@@ -765,9 +716,9 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
* 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。
*/
async function postOthersToIssue({ ctx, gitea, issueNumber, others }) {
for (const finding of others) {
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
}
await Promise.all(
others.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))),
);
log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}`);
}
@@ -928,6 +879,7 @@ module.exports = {
runDefenders,
sortFindings,
appendExclusions,
resultFilesToCommit,
selectLabels,
postSevereToIssue,
postOthersToIssue,
+3 -3
View File
@@ -334,9 +334,9 @@ ${body || 'PR 無描述)'}
* @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
* @remarks
* 使用情境:建問題模式下 `review.postSevereToIssue`src/lib/review.js)把每條嚴重 finding
* 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上,
* 作為問題明細的追蹤紀錄。
* 使用情境:建問題模式下 `review.postSevereToIssue` 與 `review.postOthersToIssue`
* 把每條 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue`
* 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。
*/
function issueFindingComment(finding) {
const emoji = SEVERITY_EMOJI[finding.severity] || '🔵';
+73
View File
@@ -0,0 +1,73 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const gitea = require('../src/lib/gitea');
function withFetchStub(handler, fn) {
const originalFetch = global.fetch;
const calls = [];
global.fetch = async (url, options = {}) => {
calls.push({ url, options });
return handler(url, options);
};
return Promise.resolve()
.then(() => fn(calls))
.finally(() => {
global.fetch = originalFetch;
});
}
function jsonResponse(data, ok = true, status = 200) {
return {
ok,
status,
async text() {
return JSON.stringify(data);
},
};
}
test('addIssueDependency 使用正確 endpoint、method 與 IssueMeta body', async () => {
const ctx = {
apiBase: 'https://gitea.example.test/api/v1',
token: 'hidden',
owner: 'owner',
repo: 'repo',
};
await withFetchStub(() => jsonResponse({ ok: true }), async (calls) => {
await gitea.addIssueDependency(ctx, 12, 34);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, 'https://gitea.example.test/api/v1/repos/owner/repo/issues/12/dependencies');
assert.equal(calls[0].options.method, 'POST');
assert.equal(calls[0].options.headers.Authorization, 'token hidden');
assert.deepEqual(JSON.parse(calls[0].options.body), {
index: 34,
owner: 'owner',
repo: 'repo',
});
});
});
test('createIssue 空 labels 不送出 labels 欄位', async () => {
const ctx = {
apiBase: 'https://gitea.example.test/api/v1',
token: 'hidden',
owner: 'owner',
repo: 'repo',
};
await withFetchStub(() => jsonResponse({ number: 5 }), async (calls) => {
await gitea.createIssue(ctx, { title: 'title', body: 'body', labels: [] });
assert.equal(calls[0].url, 'https://gitea.example.test/api/v1/repos/owner/repo/issues');
assert.equal(calls[0].options.method, 'POST');
assert.deepEqual(JSON.parse(calls[0].options.body), {
title: 'title',
body: 'body',
});
});
});
+24
View File
@@ -0,0 +1,24 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const gitrepo = require('../src/lib/gitrepo');
test('assertSafeBranchRef 接受一般分支名稱', () => {
assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123');
});
test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => {
assert.throws(
() => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'),
/不是安全的分支名稱/,
);
});
test('resolveMergeBase 會在 git fetch 前拒絕不安全 baseRef', () => {
assert.throws(
() => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'),
/不是安全的分支名稱/,
);
});
+99
View File
@@ -0,0 +1,99 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const review = require('../src/lib/review');
const diagnostics = require('../src/lib/diagnostics');
test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
delete process.env.ACTIONS_STEP_DEBUG;
try {
const detail = diagnostics.agentFailureDetail({
ok: false,
error: Object.assign(new Error('boom'), { code: 1 }),
stderr: 'token=super-secret-value',
output: 'stdout with password=hidden',
});
assert.match(detail, /exit 1/);
assert.doesNotMatch(detail, /super-secret-value|password|stdout|stderr/);
} finally {
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
}
});
test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
process.env.ACTIONS_STEP_DEBUG = 'true';
try {
const detail = diagnostics.agentFailureDetail({
ok: false,
error: Object.assign(new Error('boom'), { code: 2 }),
stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890',
output: 'token=abcdefghijklmnopqrstuvwxyz1234567890TOKEN',
});
assert.match(detail, /exit 2/);
assert.match(detail, /stderrAuthorization: \*\*\*/);
assert.match(detail, /stdouttoken=\*\*\*/);
assert.doesNotMatch(detail, /abcdefghijklmnopqrstuvwxyz/);
} finally {
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
}
});
test('postOthersToIssue 批次送出 issue 留言', async () => {
const calls = [];
let active = 0;
let maxActive = 0;
const fakeGitea = {
async createCommentOnIssue(ctx, issueNumber, body) {
active += 1;
maxActive = Math.max(maxActive, active);
calls.push({ ctx, issueNumber, body });
await new Promise((resolve) => setTimeout(resolve, 20));
active -= 1;
return { id: calls.length };
},
};
await review.postOthersToIssue({
ctx: { token: 'hidden' },
gitea: fakeGitea,
issueNumber: 7,
others: [
{ severity: '警告', reviewer: 'Maya', file: 'a.js', startLine: 1, endLine: 1, problem: 'p1', suggestion: 's1' },
{ severity: '建議', reviewer: 'Bard', file: 'b.js', startLine: 2, endLine: 2, problem: 'p2', suggestion: 's2' },
],
});
assert.equal(calls.length, 2);
assert.equal(calls[0].issueNumber, 7);
assert.ok(maxActive > 1);
});
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {
assert.deepEqual(
review.resultFilesToCommit({
createIssue: true,
severeCount: 1,
relativePath: '.gitea/ai-review/findings/run.json',
exclusionsChanged: false,
}),
['.gitea/ai-review/findings/run.json'],
);
});
test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusions 異動', () => {
assert.deepEqual(
review.resultFilesToCommit({
createIssue: true,
severeCount: 0,
relativePath: '.gitea/ai-review/findings/run.json',
exclusionsChanged: true,
}),
['.gitea/ai-review/exclusions.json'],
);
});