人工 Review 看不完,測試全綠仍會漏 Bug:用反例與證據審查 AI 程式碼
Vibe Coding 讓程式碼產出變快,卻沒有讓 Review 同樣輕鬆。LLM 能快速產生整齊的 diff,但模型說明再流暢、測試再全綠,也不能單獨證明實作適合上線。
這次案例是我為這個部落格程式的動態終端 SVG 寫的建置前校驗器。它檢查受限的 XML/CSS 規則、manifest 路徑與 SHA-256 是否對應真實檔案,以及八種語言的文章是否引用可見的示範圖片。固定 canonical SVG 是正向樣本。四類缺陷都在合併前的隔離測試中發現,證據只適用於這個校驗器的離線範圍。
原始測試 8/8 通過,獨立審查仍找到兩種錯誤接受;第一輪修正後 10/10 通過,第二輪又發現另外兩種。最終固定版本的主測試套件 12/12 頂層測試通過;獨立審查另跑四個攻擊探針,全部遭拒。另有三個 JavaScript 檔案通過 node —check。
我把驗收線拆成五步:固定範圍與不變條件;建立最小反例;只有實際觀察到失敗才記為 RED;在精確 commit 上獨立重跑正例與負例;最後依風險決定是否放行。邏輯測試不能取代建置、目標平台或執行環境驗證。高風險改動可用小卡記錄範圍、Base/Head commit、不變條件、正例、反例、實際命令與環境、未驗證邊界及結論。校驗器是部落格倉庫中的建置前元件,不是獨立包裝的通用 CLI;目前也沒有量測 Review 時間的節省。
公開倉庫只包含經脫敏的合成反例與可執行教學測試,不包含這個部落格程式的生產校驗器、生產修復或內部架構。文中的 8/8 → 10/10 → 12/12 是原工程離線驗收的結果;公開倉庫只重現四類缺陷模式,不複製原有生產實作。
為什麼 8/8 通過仍會接受錯誤輸入?
測試全綠只代表目前的輸入滿足目前的斷言,不代表每個不合法輸入都會被拒絕。
第一個反例把合法圖片與未獲允許的跨文章資源引用放在同一行。舊邏輯只要找到一個允許的圖片標籤,就放行整行。獨立審查先在另一個文章 slug 下建立真實 SVG,再把 Markdown 引用加到八語文章中合法圖片的同一行,以排除目標檔案不存在的干擾。舊實作仍回傳成功,報告 posts_checked=8。
<img src="/assets/terminal-demo.svg" alt="Terminal demo" width="356" height="137" /> 
第二個反例是檔案系統競態:檢查過的路徑不一定是最後開啟的檔案。測試在開檔邊界替換父目錄;checkout 外有一份內容和預期 SHA-256 完全相符的 canonical SVG。父目錄改成指向外部目錄的符號連結後,舊實作讀取並接受外部檔案。被繞過的是來源限制,不是雜湊檢查。
最終 Linux 路徑先開啟可信 checkout 根目錄,再以已開啟的父目錄檔案描述符逐層解析路徑。中間元件要求是目錄並使用 O_NOFOLLOW,最終檔案也拒絕符號連結;讀取前後會比較裝置、inode 和大小,再驗證摘要。它使用 Linux /proc/self/fd 目錄句柄;procfs 不可用時失敗關閉。範圍依賴可信 checkout 根目錄與可用 procfs,不涵蓋任意同 UID 或主機層級對手。Linux openat2(2) 文件說明了路徑解析限制。
第二輪:雜湊沒錯,內容也可能不可見
第一輪修正後測試 10/10 通過。第三個反例針對 reduced motion:SVG 影格預設透明,正常動畫會逐步顯示內容;若減少動態效果的規則只停止動畫,卻沒顯示最後一格,畫面就可能空白。
.f { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
#f0 { animation: none; opacity: 1; }
#f3 { animation: none; } /* 最終影格仍透明 */
}
測試重新計算 SVG SHA-256、大小、內容定址檔名與 canonical manifest,真正檢查的是「最終結果必須可見」這項契約。MDN 的 prefers-reduced-motion 說明說明如何偵測減少非必要動態效果的偏好。
第四個反例來自 Markdown。舊實作以正規表示式計算圖片標籤,沒有排除 HTML 註解。把唯一圖片包進註解後,原始文字仍有標籤,頁面卻不會顯示圖片;舊邏輯仍把它算成一張圖。CommonMark 0.31.2 的 HTML block 規則說明註解中的標籤不是可見圖片。
<!-- <img src="/assets/demo.svg" alt="Terminal demo" /> -->
把測試結果綁定到精確版本
| 程式版本 | 主測試套件 | 後續獨立審查 |
|---|---|---|
| 原始版本 | 8/8 通過 | SEC-01、SEC-02 錯誤接受 |
| 第一輪修正版 | 10/10 通過 | SEC-03、SEC-04 錯誤接受 |
| 最終離線驗收版 | 12/12 頂層測試通過 | 四個獨立攻擊探針另行執行,全部拒絕 |
最終 12 個頂層測試包含 SEC-01 至 SEC-04 的回歸測試組。四個獨立探針不計入 12/12,不能寫成 16/16。三個 JavaScript 檔案通過 node —check,但語法檢查不能取代行為測試。
後兩類回歸測試的提交早於原始碼修正,但完整測試套件沒有在只有新測試的提交上執行,因此沒有完整的 10/12 RED 回執。可確認的是,獨立審查在舊版完整校驗器中重現了錯誤接受,而最終固定版本的主測試和額外探針均通過。
行為證據穩定後,才做複雜度審查:追蹤 helper、分支、設定和呼叫點,確認它們保護什麼不變條件、是否重複、是否有真實使用者。程式碼更短不一定更簡單;刪除路徑防護可能重新引入競態。
互動式證據面板
選擇合成反例,查看輸入、不變條件、重現的失敗與證據邊界。
目前案例: SEC-01 · 存在不等於排他
SEC-01 · 存在不等於排他
- 輸入條件
- 允許圖片與未授權跨文章資源引用同時出現。
- 應維持的不變條件
- 每個相關引用都必須符合規則;找到一個合法引用不代表整筆輸入安全。
- 合成案例重現的失敗
- 舊邏輯找到允許圖片後就放行整行,忽略額外引用。
- 預期正確行為
- 逐一檢查所有引用;任何越界引用都應遭拒。
- 原工程發現輪次
- 第一輪獨立審查發現
- 可延伸的審查經驗
- 建立合法與非法內容並存的最小反例,確認規則檢查完整輸入。
SEC-02 · 檢查與使用競態
- 輸入條件
- 路徑通過檢查後,在開啟前替換父目錄或符號連結。
- 應維持的不變條件
- 被驗證物件必須與實際存取物件一致,並維持在預期檔案系統邊界內。
- 合成案例重現的失敗
- 檢查路徑後再依路徑開啟,可能讓目標在兩步之間遭替換。
- 預期正確行為
- 驗證與存取必須維持邊界;合成例不代表已實作正式修復。
- 原工程發現輪次
- 第一輪獨立審查發現
- 可延伸的審查經驗
- 檢查驗證與使用之間是否存在可被並行改變的空隙。
SEC-03 · 減少動態效果仍須顯示終幀
- 輸入條件
- SVG 影格預設透明;啟用減少動態效果後動畫停止。
- 應維持的不變條件
- 停止動畫不能讓重要最終內容消失。
- 合成案例重現的失敗
- 只停用動畫卻未顯示終幀,結果可能空白。
- 預期正確行為
- 減少動態效果模式也應直接呈現完整可讀的最終狀態。
- 原工程發現輪次
- 第二輪獨立審查發現
- 可延伸的審查經驗
- 將無障礙偏好視為獨立輸入,檢查內容是否仍可見。
SEC-04 · 註解不是可見內容
- 輸入條件
- HTML 註解內含看似有效的圖片標籤。
- 應維持的不變條件
- 原始碼文字與瀏覽器實際呈現的可見元素不同。
- 合成案例重現的失敗
- 簡單統計標籤可能把註解中的圖片誤算成可見圖片。
- 預期正確行為
- 解析結構並排除註解節點,只判斷真正呈現的元素。
- 原工程發現輪次
- 第二輪獨立審查發現
- 可延伸的審查經驗
- 把偽造標籤放入註解,驗證檢查器理解結構而非只有文字。
公開合成教學證據
公開倉庫只重現合成缺陷模式;測試成功代表重現成立,不代表正式修復通過。
查看固定版本的合成教學倉庫原工程離線摘要
原工程紀錄:8/8 → 10/10 → 12/12。四個獨立探針分開執行,不計入 12/12。
驗證邊界
範圍僅限 Node.js v22.16.0 / Linux x64 離線校驗。macOS 惡意並行寫入與全站執行狀態仍未驗證。
延伸閱讀:Mutation Testing、OpenQodex 的 pre-push 審閱失敗邊界 與 Diffle 的評論錨點。
從測試通過到正式放行,還差什麼?
最終獨立複驗涵蓋 Node.js v22.16.0、Linux x64 的離線校驗器:主測試 12/12 通過,四個額外攻擊探針全數遭拒,canonical SVG、manifest 與八語綁定正例通過,三個 JavaScript 檔案也通過 node —check。
這支持有邊界的結論:在該環境與固定規則下,四類已知錯誤接受已修正。但它不能證明整個部落格已可正式上線。原生 macOS 面對惡意併發檔案寫入的能力尚未驗證,完整網站建置與最終頁面執行驗收也不在本次離線測試範圍。
實際放行線應依風險分層:單元測試檢查已涵蓋行為,靜態檢查尋找特定模式,建置與整合測試確認模組組合,目標平台和執行環境驗收檢查部署邊界。關鍵風險尚未驗證,就不能把局部通過說成整體可上線。
用契約與反例找出最值得懷疑的行為,再讓精確版本上的測試與獨立證據決定下一步該深讀哪裡。這不是取消人工判斷,而是讓 Review 從「看起來沒問題」變成有證據、有範圍、能複驗的決定。