chore: update ai-review findings [ai-review-bot][failure]
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
"location": "src/gitea.js:66",
|
||||
"problem": "這裡直接從 PR head 讀取 `.reviewignore`,再拿它當成排除規則。攻擊者可以在自己的分支塞入排除條目,讓 bot 故意跳過包含惡意變更的檔案或整個目錄,等於自己決定哪些地方不被審查。",
|
||||
"suggestion": "不要信任 PR head 裡的 `.reviewignore` 來決定安全掃描範圍;改從受保護的 base branch 或 maintainer 管控的位置讀取,且要與固定的預設排除清單合併,而不是讓它覆蓋預設規則。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "critical",
|
||||
@@ -29,6 +29,14 @@
|
||||
"location": "src/findings.js:352",
|
||||
"problem": "這裡會直接讀取 PR 工作樹中的 `.gitea/ai-review/exclusions.json` 當成可信排除來源。攻擊者可以先在分支裡放一份藏在 `.gitea/` 下的 exclusions 檔,利用被忽略的路徑把自己的問題先排除掉,讓後續的 findings 被靜默吃掉。",
|
||||
"suggestion": "把 exclusions 視為 bot 自己管理的狀態,不要從 PR head 的工作樹直接信任既有內容;應該改成只讀受保護來源,或在載入前驗證檔案確實由 bot 生成且未被 PR 作者預先植入。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "critical",
|
||||
"role": "Assassin",
|
||||
"location": "src/main.js:158",
|
||||
"problem": "這裡直接載入 PR 工作樹中的 `.gitea/ai-review/exclusions.json` 當成既有排除規則。攻擊者可以先在 PR 內預埋一份排除清單,因為 `.gitea/` 又被預設排除於 diff 之外,這些惡意排除不會被審查到,卻會被流程直接拿來吞掉真正的 findings,形成靜默的審查繞過。",
|
||||
"suggestion": "不要從 PR head 讀取可由提交者任意修改的 exclusions;只接受由受信任 bot、受保護分支或外部持久化儲存產生的排除資料,並驗證來源身分與 commit marker,避免使用者自行預埋排除規則。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
@@ -45,7 +53,7 @@
|
||||
"location": "src/main.js:69",
|
||||
"problem": "這裡先檢查 head SHA 對應的訊息是否為 failure,但如果 SHA 查詢失敗或是空值,後面的 `shouldSkipBotCommit()` 仍可能只看到分支 head 上的 `[ai-review-bot]` 標記就直接跳過。最小重現:`getCommitMessageBySha()` 因 Gitea API 暫時失敗回空字串,而分支 head 正好是 `[ai-review-bot][failure]`,流程就會 exit 0,等於把本來應該失敗的 bot commit 當成可跳過的自動提交。",
|
||||
"suggestion": "把「是否跳過」和「是否 failure」分開判斷,或讓 helper 回傳解析出的 outcome;只允許 success 標記走 skip,failure 標記不論 SHA/branch 來源都應優先讓流程失敗。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
@@ -109,14 +117,6 @@
|
||||
"location": "src/findings.js:442",
|
||||
"problem": "這裡每一筆 finding 都要跟整包 exclusions 做一次 `.some()`,而且內層還反覆跑 `normalizeText` 和字串包含比對,資料一多就直接變成 O(F×E) 的熱點。像 300 筆 finding 配 500 筆 exclusions,會吃掉 15 萬次以上的比對與正規化,CPU 和字串配置都在浪費。",
|
||||
"suggestion": "先把 exclusions 在載入時一次正規化並依 `filePath / role / textKey` 建索引,讓過濾改成近似 O(F);至少把 `normalizeText` 移到內層迴圈外,避免同一段字串被重算成百上千次。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Leo",
|
||||
"location": "src/resolve.js:183",
|
||||
"problem": "`reconcileConversations` 同時負責收 comment、分組、關閉、讀檔、AI 裁決、結果分類與降級處理,職責太多而且彼此耦合。任何一個小規則變動,都得先看完整條流程,單元測試也很難只鎖定某一段行為。",
|
||||
"suggestion": "拆成幾個可測的純函式與薄編排層,例如 `groupConversations`、`closeOpenComments`、`buildJudgeItems`、`applyVerdicts` 分開處理,讓主流程只保留資料流轉與錯誤收斂。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
@@ -141,7 +141,7 @@
|
||||
"location": "src/comments.js:233",
|
||||
"problem": "`postFindingsReview` 的降級流程有兩層:先嘗試批次 review,再失敗時改成 summary-only,最後 summary-only 也失敗才退回一般 comment。現在的測試只驗到第一層失敗後、第二層成功的情境,沒有驗證 summary-only 也失敗時是否真的會呼叫 `postIssue(body)`,這是最脆弱的 fallback 路徑之一。",
|
||||
"suggestion": "新增一個測試讓第一次 `postReview({comments})` 失敗、第二次 `postReview({comments: []})` 也失敗,然後斷言 `postIssue(body)` 有被呼叫,且 inline comments 仍會逐筆嘗試送出。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
@@ -157,7 +157,7 @@
|
||||
"location": "src/config.js:27",
|
||||
"problem": "這段註解已經跟著介面走音了。它宣稱使用端「只需傳 `with: token`」,但這次 action 其實已新增 `comment_token` 與 `model` 等輸入,註解仍停留在舊旋律,容易讓讀者誤判介面現況。",
|
||||
"suggestion": "把這組說明改成與目前 inputs 一致,明確列出 `token`、`comment_token`、`model` 的優先序與用途;如果無法精簡,就直接移到 README 或設計文件,避免在程式中留下過時註記。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
@@ -165,7 +165,7 @@
|
||||
"location": "src/findings.js:227",
|
||||
"problem": "這個抽取器一旦命中目標檔案,就一路把後面的 diff 全部帶進去,沒有在下一個 `diff --git` 區塊時停下來。最小重現:diff 同時有 `a.js` 和 `b.js`,要補 `a.js` 的行號時,送給 LLM 的內容會混進 `b.js` 的 hunks,結果很容易定位到錯的行,或讓模型把別檔的內容誤認成目標檔上下文。",
|
||||
"suggestion": "在開始捕捉後,遇到下一個 `diff --git ` 就應該停止,只回傳目前檔案那一段;找不到目標檔時再退回整份 diff。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
@@ -173,46 +173,70 @@
|
||||
"location": "src/findings.js:277",
|
||||
"problem": "`deduplicateWithAI` 是新的核心語意去重流程,但目前完全沒有直接測試它的成功與失敗分支。尤其是 LLM 回傳排序不同、夾雜幻覺項目、回傳空陣列或超量結果時,程式會改走保守 fallback,這些都是很容易壞掉但現在沒被驗證的邊界。",
|
||||
"suggestion": "替 `deduplicateWithAI` 補測兩類情境:一是 stub `chatJSON` 回傳重排後的重複項與一筆幻覺項,確認只保留能對應回原始 findings 的資料;二是回傳空陣列或不合法結果時,確認會保守回傳原始 findings。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/findings.js:293",
|
||||
"problem": "每筆缺行號的 finding 都是單獨處理,還在每筆裡串行重試最多 3 次 LLM 查詢。N 筆問題就會膨脹成 N×3 次遠端呼叫與等待,幾十筆時延遲會直接被放大好幾倍。",
|
||||
"suggestion": "把行號定位改成可平行的批次流程,例如同檔或同角色一起送出後用 `Promise.allSettled` 收結果;重試只保留在單筆失敗時,別讓每個 finding 都自己拖慢整條管線。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/findings.js:442",
|
||||
"problem": "這裡每一筆 finding 都要跟整包 exclusions 做一次 `.some()`,而且內層還反覆跑 `normalizeText` 和字串包含比對,資料一多就直接變成 O(F×E) 的熱點。像 300 筆 finding 配 500 筆 exclusions,會吃掉 15 萬次以上的比對與正規化,CPU 和字串配置都在浪費。",
|
||||
"suggestion": "先把 exclusions 在載入時一次正規化並依 `filePath / role / textKey` 建索引,讓過濾改成近似 O(F);至少把 `normalizeText` 移到內層迴圈外,避免同一段字串被重算成百上千次。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/gitea.js:239",
|
||||
"problem": "這裡逐一 await 每個 review 的 comments,PR review 一多就變成 N 次遠端呼叫的線性延遲累加;例如 30 個 review 就是 30 個 round-trip 排隊等,時間都被網路空轉偷走。",
|
||||
"suggestion": "把 reviews.map(review => getPullReviewComments(review.id).catch(...)) 丟進 Promise.all 或 Promise.allSettled 平行抓取,再 flat 結果;單筆失敗仍可記 warn 後略過。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/comments.js:40",
|
||||
"problem": "這裡把 `file:0` 也視為有效行號;最小重現:只要上游傳進 `app/foo.js:0`,`parseLocation()` 會回傳 line=0,後續 `postPullReviewComment` 會帶著 `new_position: 0` 發到 Gitea,通常會被拒絕或定位失敗。也就是說,0 行號沒有被當成缺值處理。",
|
||||
"suggestion": "把行號門檻改成 `> 0`,`0` 與負數都應視為無效;同時讓需要行號的呼叫端把這種情況當作缺行號,重新定位或降級處理。",
|
||||
"is_new": false
|
||||
"role": "Rogue",
|
||||
"location": "src/findings.js:322",
|
||||
"problem": "每一筆缺行號的 finding 都重新呼叫 `extractFileDiff(diff, file)` 掃完整份 diff,若同一檔案有 k 筆問題,就會重複做 k 次整份 diff 解析,浪費量是 O(k × diff長度)。",
|
||||
"suggestion": "先把 diff 依檔名切成快取 Map,一次掃描建立好 `file -> fileDiff`,後續同檔 finding 直接共用已切好的片段。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Bard",
|
||||
"location": "src/llm.js:56",
|
||||
"problem": "`cliArgs` 把不同提供者的參數拼湊在同一個分支裡,還讓 `opencode` 走了另一套文字輸入路線,整個 helper 的節奏忽然一分為二。讀起來像兩個介面硬塞進同一支笛子。",
|
||||
"suggestion": "拆成各提供者各自的 argv builder,或至少把 prompt 輸入方式抽成獨立 helper,讓每個分支只處理一種責任,結構會更俐落。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Leo",
|
||||
"location": "src/findings.js:1",
|
||||
"problem": "這個模組同時處理舊 findings 載入、合併去重、缺行號補齊、排除規則正規化、誤報過濾、AI 去重、以及 exclusions 的讀寫,職責已經混成一包。更麻煩的是 `loadExclusions`、`appendExclusions`、`applyExclusions` 各自都有一套相近但不完全一致的比對邏輯,未來只要規則改一處,另一處沒同步就會開始出現不可預期的行為差異。",
|
||||
"suggestion": "把 exclusions 的正規化與比對規則抽成唯一來源,例如 `normalizeExclusionEntry` + `matchesExclusion` 之類的共用 helper,並把 AI 去重、行號補齊、檔案持久化拆到不同模組,減少這個檔案的責任面。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/main.js:69",
|
||||
"problem": "這裡先檢查 head SHA 對應的訊息是否為 failure,但如果 SHA 查詢失敗或是空值,後面的 `shouldSkipBotCommit()` 仍可能只看到分支 head 上的 `[ai-review-bot]` 標記就直接跳過。最小重現:`getCommitMessageBySha()` 因 Gitea API 暫時失敗回空字串,而分支 head 正好是 `[ai-review-bot][failure]`,流程就會 exit 0,等於把本來應該失敗的 bot commit 當成可跳過的自動提交。",
|
||||
"suggestion": "把「是否跳過」和「是否 failure」分開判斷,或讓 helper 回傳解析出的 outcome;只允許 success 標記走 skip,failure 標記不論 SHA/branch 來源都應優先讓流程失敗。",
|
||||
"location": "src/json.js:113",
|
||||
"problem": "這裡只檢查 `JSON.parse(normalized)` 能不能成功,沒有確認修復後的內容真的是陣列。最小重現是 AI 把 `findings.json` 修成 `{ \"a\": 1 }`,函式會照樣寫回檔案並回報成功,但下一輪讀取時 `readJSONArray` 會把它當成非陣列而視為空值,等於把資料靜默吃掉。",
|
||||
"suggestion": "在寫檔前先 `const parsed = JSON.parse(normalized)`,再加上 `Array.isArray(parsed)` 檢查;不是陣列就直接丟錯,不要覆寫原檔。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/resolve.js:74",
|
||||
"problem": "這裡用 `path + line` 當唯一群組鍵,且只保留第一筆 `botFinding`。最小重現是同一個檔案同一行同時被兩個角色指出不同問題,`groupConversations` 會把它們合成同一組,後來的那筆 finding 會被吞掉,導致後續關閉、回寫或保留時少掉一個問題。",
|
||||
"suggestion": "不要只用 `path + line` 折疊所有 comment;至少要保留同一組內的所有 botFinding,或改成以 comment id / finding 本身為單位處理,再在最後階段做去重。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:214",
|
||||
"problem": "這裡新增了 `postOldFindingsComment` 與 `postNewNonCriticalComment` 兩條公開的留言分流路徑,但現有測試只驗證了 `postNewCriticalComments` 與 `postFindingsReview`,完全沒有案例確認這兩個函式的篩選條件、空陣列時是否跳過、以及輸出的 Markdown 內容是否真的只包含對應的 findings。這種分流邏輯一旦算錯,就會發生該發的沒發、或不該公告的問題被貼出去。",
|
||||
"suggestion": "補上這兩個函式的單元測試:空陣列時不呼叫 `postComment`;`postOldFindingsComment` 只送出 `is_new === false` 的項目;`postNewNonCriticalComment` 只送出 `is_new` 且 `level !== 'critical'` 的項目;再斷言 comment 標題與表格列數都符合預期。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:171",
|
||||
"problem": "`postFindingsReview` 的救援路徑只測到「批次 review 失敗後,改發逐筆 inline comment」這一段,卻沒有驗證第二次 `postReview({ comments: [] })` 也失敗時,會正確降級到 `postIssue(body)`。這條路徑是 Gitea review API 整個故障時保住摘要的最後保險絲,沒測到的話,真正出事時很容易靜默漏報。",
|
||||
"suggestion": "新增一個雙重失敗測試:第一次 `postReview` 因 comments 拋錯、第二次 `postReview` 也拋錯,最後斷言有呼叫 `postIssue`,而且 inline comments 仍會依序嘗試發布。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/main.js:17",
|
||||
"problem": "`main()` 整個流程目前沒有任何直接測試,只能靠零散的子函式單測推測結果;但這裡包含多個關鍵分支與 `process.exit` 行為,例如 preflight 失敗、bot 自動提交跳過、空 diff 提早結束、JSON 驗證失敗、以及偵測到 critical 後結束失敗。只要接線順序或退出碼改壞,現有測試不會第一時間抓到。",
|
||||
"suggestion": "補一組整合測試,把 `runPreflight`、`getPRDiff`、`reconcileConversations`、`validateJSONArrayFile`、`commitAndPush` 以 stub 注入,逐一覆蓋 Step3/5/9/11 的 exit 0/1 分支,至少驗證 `process.exit` 與主要副作用被正確觸發。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
@@ -229,7 +253,7 @@
|
||||
"location": "src/comments.js:102",
|
||||
"problem": "統計表與單行摘要各欄位都用 `filter(...).length` 重掃多次,同一批 findings 會被走 4 到 8 次。資料量一大,連 log 文字本身都開始吃不必要的掃描成本。",
|
||||
"suggestion": "改成單次迴圈同時累加 critical / warning / info / 未分類計數,再把結果組成表格和摘要;一次走完就好,別讓統計自己變熱點。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
@@ -237,7 +261,7 @@
|
||||
"location": "src/comments.js:266",
|
||||
"problem": "`postOldFindingsComment` 與緊接著的 `postNewNonCriticalComment` 都是這次新加的對外 comment 發布行為,但目前沒有專門測試它們的空陣列早退、標題文字與表格內容。這會讓 comment 分流邏輯只靠間接測試支撐,回歸時很容易漏掉。",
|
||||
"suggestion": "補這兩個函式的單元測試:至少驗證空陣列時不會送 comment、非空時 body 內容包含正確標題與表格,且 `postOldFindingsComment` 只收舊問題、`postNewNonCriticalComment` 只收新非 critical 問題。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
@@ -245,7 +269,7 @@
|
||||
"location": "src/findings.js:393",
|
||||
"problem": "前面已經把 exclusions 正規化、去重過一次了,這裡為了 log 又再丟進 `buildExclusionContext` 重做 normalize / dedupe / group。等於同一批資料在同一輪流程裡被重算兩次,白白多吃一輪 O(n) 到 O(n log n) 的 CPU。",
|
||||
"suggestion": "把第一次處理的摘要一起回傳或快取下來,後面的 log 直接重用同一份結果,不要再對同一批 exclusions 重跑分組。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
@@ -253,6 +277,22 @@
|
||||
"location": "src/resolve.js:88",
|
||||
"problem": "這個 `codeWindow` 每遇到一筆 open conversation 就對整份檔案內容再 `split('\\n')` 一次。若同一個檔案有多條 thread,O(L) 的切割和陣列配置會被重複吃掉,明明同一份內容卻一直重複解剖。",
|
||||
"suggestion": "先把檔案內容預先切成行陣列並快取,或讓 `codeWindow` 直接吃已分割好的 lines;這樣同檔多條對話就不用重複掃描整份內容。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Leo",
|
||||
"location": "src/resolve.js:201",
|
||||
"problem": "`reconcileConversations()` 同時在做 comment 分組、關閉遠端 review、讀檔、抽 code window、AI 裁決、再把結果拆成 resolved / excluded / carried 三條路徑,流程很完整,但也很難局部理解或替換。未來任何一段判斷要調整,都得先吞下整個函式的心智負擔,維護門檻偏高。",
|
||||
"suggestion": "把它拆成幾個可單獨測試的步驟,例如 `collectOpenConversations()`、`loadConversationCode()`、`judgeConversationVerdicts()`、`mapVerdictsToFindings()`,讓主流程只保留編排,不要把資料轉換與外部副作用全塞在一起。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Mage",
|
||||
"location": "src/usage.js:212",
|
||||
"problem": "這個百分比計算只擋了 `limit <= 0`,沒有擋 `remaining < 0`。最小重現是 `resolveRemainingPercent({ available: true, used: 150, limit: 100 }, null)` 或 `remaining = -1`,會算出負百分比,讓使用量摘要出現不合理的 `-50%` 之類結果,和函式註解宣告的「負數視為無法計算」不一致。",
|
||||
"suggestion": "把 `remaining < 0` 也納入無效值判斷,直接回 `null`;若你想保守顯示,可另外在輸出層把負值夾到 0,但不要讓計算層回傳負百分比。",
|
||||
"is_new": true
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user