如果 Claude Code 在導致程式碼損壞的同一個提交中弱化了測試,我的測試閘門會讓該提交通過。Codex 在審查時被告知要尋找被弱化和刪除的測試,而指令碼中唯一的硬性檢查是針對零測試運行的情況。Kartik 在我關於 Claude Code 和 Codex 如何相互審查工作的 Hashnode 文章中指出了這個弱點。我在評論中給了他簡短的回答。這篇文章是完整的答案,並附上我運行檢查的指令。
為什麼在同一個提交中修改測試會成為弱點呢?「two-engines-gate」是我讓 Claude Code 和 Codex 相互檢查的免費、MIT 授權版本。gate.sh 會運行你的測試並寫下以提交命名的通過記錄,review.sh 會讓另一個引擎審查它,而 merge-check.sh 則會拒絕合併,除非該提交同時具備兩者。
我之前曾撰寫過關於審查迴圈和提交雜湊記錄的文章。gate.sh 會運行提交中的任何測試。當一個提交同時更改程式碼和測試時,該提交也決定了「通過」的定義。
我在 10 月 23 日使用公開指令碼和一個小型 Python 專案進行了檢查。該專案有一個 add 函數和兩個 assertEqual 測試。首先,我修改了 add 函數使其返回 a + b + 1,結果閘門如預期地失敗了:FAILED (failures=2) gate: FAIL (exit 1) on 8390173080fb; no pass record; full log .ai/tests/8390173080fb.log。
接著,在下一個提交中,我將兩個測試更改為 assertTrue(add(2, 3)) 和 assertIsNotNone(add(-1, 1))。這兩個測試對於損壞的函數都成立,因此閘門寫下了一個通過記錄:gate: running 'python3 -m unittest -q' on 266c4d979b71 gate: PASS on 266c4d979b71, record .ai/tests/266c4d979b7102820377e64ab3f20b069e41bf01.pass。
測試指令以 0 結束,並且仍有兩個測試運行,所以 gate.sh 沒有理由拒絕。捕捉到這種情況的責任落在審查者身上。
審查者被告知的內容是,免費的 review-prompt.md 要求進行靜態審查。Codex 會閱讀提交、差異(diff)以及結帳(checkout)中的任何檔案,並且不運行測試或建置。以下是涵蓋測試的提示詞內容:「特別檢查:提交承諾但差異未實現的行為、錯誤路徑和邊緣案例、在舊程式碼上仍會通過的測試,以及任何削弱現有檢查的內容。」
相同的提示詞指出,阻擋器(blocker)或需要修復(should-fix)的問題需要「具體、可重現的失敗」。對於被弱化的斷言,這種失敗很容易寫下。在新程式碼上呼叫 add(2, 3),得到 6,然後觀察新的測試仍然通過。一個同時閱讀測試差異和程式碼差異的審查者,擁有標記此問題所需的一切資訊。
在我自己的專案中,Codex 獲得的提示詞更為嚴格。每個測試變更都必須在舊行為上失敗,沒有明確理由不能弱化任何內容,並且審查者必須檢查差異以尋找任何被弱化或刪除的測試。沒有理由的寬鬆斷言會被要求「CHANGES REQUIRED」(需要變更)。
README 檔案告訴你可以編輯 review-prompt.md 來添加專案規則,這樣你就可以在自己的副本中加入這三條規則。這仍然是審查者的判斷。README 直接說明:「審查可能會遺漏一些東西,而且測試只檢查它們所測試的內容。」如果 Codex 遺漏了被弱化的測試並回覆「ACCEPT」(接受),merge-check.sh 會找到該提交的通過記錄和「ACCEPT」並讓其通過。
硬性檢查:零測試運行。有一種情況是由指令碼本身檢查的。當測試指令沒有輸出任何內容,或者它所知的測試摘要加起來為零個測試運行時,gate.sh 會拒絕通過。它會讀取 pytest、unittest、node --test、mocha、Jest 和 Vitest 的摘要行。
這涵蓋了刪除所有測試、選擇不到任何內容的篩選器或檔案列表,以及(除了 unittest 之外的所有運行器)所有測試都被跳過的情況。在我的 Python 專案中,刪除測試檔案導致 unittest 以 5 結束並顯示「NO TESTS RAN」,因此閘門已經因為退出碼而失敗。
為了單獨查看零測試檢查,我使用 || true 運行了相同的指令,使其始終以 0 結束:gate: the test summaries in the output show that no test ran; set GATE_COUNT_RE or GATE_MIN_TESTS (README) gate: FAIL (exit 1) on f93de2d8e8a6; no pass record; full log .ai/tests/f93de2d8e8a6.log。
刪除兩個測試中的一個是另一種情況。由於仍有一個測試運行,預設檢查讓該提交通過了。如果你希望閘門注意到測試數量的下降,請將 GATE_COUNT_RE 設定為你的運行器列印計數行的模式,並將 GATE_MIN_TESTS 設定為你目前擁有的測試數量。
使用 GATE_COUNT_RE='^Ran ([0-9]+) tests? in ' 和 GATE_MIN_TESTS=2,相同的提交失敗了:gate: the output reports 1 tests, fewer than GATE_MIN_TESTS=2; set GATE_COUNT_RE or GATE_MIN_TESTS (README) gate: FAIL (exit 1) on 1ea182ad01af; no pass record; full log .ai/tests/1ea182ad01af.log。
這個最小值是在你運行閘門的地方設定的,在提交之外,所以當你添加測試時需要手動提高它。go test 和 cargo test 需要 GATE_COUNT_RE 才能進行任何計數檢查,README 中有每個的範例。GATE_MIN_TESTS=0 會關閉此檢查。
涵蓋新測試的習慣。新測試還有一個額外步驟,我在關於如何讓 Codex 審查 Claude Code 提交的文章中描述過。編寫程式碼的引擎通常也會編寫測試,而一個在舊程式碼上已經通過的測試,並不能證明任何關於變更的內容。因此,編寫者會針對基礎提交運行每個新測試一次,並將失敗的輸出貼到提交主體中。這對於被弱化的舊測試沒有任何作用,這就是為什麼提示詞規則很重要的原因。
尚未建置的功能。目前還沒有一個指令碼可以讀取差異並標記被移除或寬鬆的斷言。我告訴 Kartik 這將是一個很好的補充,我會研究它。目前,無論是免費的閘門還是套件都沒有這個功能,所以單一被弱化的斷言只有在審查者注意到時才能被發現。
在你的專案中需要設定什麼?將測試規則添加到你的 review-prompt.md 中:新測試必須在舊行為上失敗,並且沒有明確理由不得弱化或刪除任何測試。保持零測試檢查開啟,如果你使用 go test 或 cargo test,請設定 GATE_COUNT_RE。
如果刪除測試應該導致閘門失敗,請將 GATE_MIN_TESTS 設定為你目前的測試數量。讓編寫引擎針對基礎提交運行每個新測試,並將輸出貼到提交主體中。
套件中的相同規則。付費的 Two Engines Kit(目前版本 v1.0.2)提供了一個審查提示詞範本,其專案規則包括「測試必須在舊行為上失敗;沒有理由不得弱化任何內容」,其檢查風險清單中也包含「任何被弱化的測試」。Two Engines Kit 是根據自第一個 Codex 和 Claude CLI 以來收集的資訊所建置的。
試試看。複製 two-engines-gate 並運行 bash demo.sh,以觀察一個完整的迴圈,其中包含一個替代審查者,因此不使用任何模型或帳戶。tests/run.sh 包含了所有六個測試運行器的零測試案例,以及 GATE_COUNT_RE 和 GATE_MIN_TESTS 的案例。