# 人工 Review 看不完，測試全綠仍會漏 Bug：用反例與證據審查 AI 程式碼

> Canonical: https://vxi.me/zh-tw/posts/vibe-coding-ai-code-review-evidence-gates/
> Locale: zh-tw
> Published: 2026-10-25
> Updated: 2026-10-25
> Markdown: https://vxi.me/zh-tw/posts/vibe-coding-ai-code-review-evidence-gates.md

<figure class="ai-review-evidence-hero not-prose" lang="zh-tw">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-evidence-gates.mobile.still.zh-tw.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-evidence-gates.still.zh-tw.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-evidence-gates.mobile.zh-tw.svg" type="image/svg+xml"><img src="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-evidence-gates.zh-tw.svg" alt="三個不同程式版本分別通過 8/8、10/10、12/12 頂層測試；獨立審查發現四類缺陷，最終四個額外探針均遭拒絕，僅限離線驗證。" width="1200" height="700" loading="eager" fetchpriority="high" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption style="font-size:0.8rem;line-height:1.5;opacity:.78;margin-top:.35rem">三個程式版本的測試與獨立審查。綠色不代表正式上線。</figcaption>
</figure>

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 是原工程離線驗收的結果；公開倉庫只重現四類缺陷模式，不複製原有生產實作。

<a class="gh-repo-pill" href="https://github.com/vxime/ai-code-review-counterexamples/tree/74d9f12331ef4d015bf882a6e09d66e59434368b" target="_blank" rel="noopener noreferrer"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" width="18" height="18"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645.12.54 2.873.12 3.176.765.84 1.23 1.91.81 3.22.61 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606.21 2.896.015 3.286C20.565 22.092 24 17.592 24 12.297"/></svg><span>脫敏合成範例與可重現測試</span></a>

## 為什麼 8/8 通過仍會接受錯誤輸入？

測試全綠只代表目前的輸入滿足目前的斷言，不代表每個不合法輸入都會被拒絕。

第一個反例把合法圖片與未獲允許的跨文章資源引用放在同一行。舊邏輯只要找到一個允許的圖片標籤，就放行整行。獨立審查先在另一個文章 slug 下建立真實 SVG，再把 Markdown 引用加到八語文章中合法圖片的同一行，以排除目標檔案不存在的干擾。舊實作仍回傳成功，報告 posts_checked=8。

~~~html
<img src="/assets/terminal-demo.svg" alt="Terminal demo" width="356" height="137" /> ![foreign](../foreign/terminal-demo.svg)
~~~

第二個反例是檔案系統競態：檢查過的路徑不一定是最後開啟的檔案。測試在開檔邊界替換父目錄；checkout 外有一份內容和預期 SHA-256 完全相符的 canonical SVG。父目錄改成指向外部目錄的符號連結後，舊實作讀取並接受外部檔案。被繞過的是來源限制，不是雜湊檢查。

最終 Linux 路徑先開啟可信 checkout 根目錄，再以已開啟的父目錄檔案描述符逐層解析路徑。中間元件要求是目錄並使用 O_NOFOLLOW，最終檔案也拒絕符號連結；讀取前後會比較裝置、inode 和大小，再驗證摘要。它使用 Linux /proc/self/fd 目錄句柄；procfs 不可用時失敗關閉。範圍依賴可信 checkout 根目錄與可用 procfs，不涵蓋任意同 UID 或主機層級對手。[Linux openat2(2) 文件](https://man7.org/linux/man-pages/man2/openat2.2.html)說明了路徑解析限制。

## 第二輪：雜湊沒錯，內容也可能不可見

第一輪修正後測試 **10/10 通過**。第三個反例針對 reduced motion：SVG 影格預設透明，正常動畫會逐步顯示內容；若減少動態效果的規則只停止動畫，卻沒顯示最後一格，畫面就可能空白。

~~~css
.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 說明](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/%40media/prefers-reduced-motion)說明如何偵測減少非必要動態效果的偏好。

第四個反例來自 Markdown。舊實作以正規表示式計算圖片標籤，沒有排除 HTML 註解。把唯一圖片包進註解後，原始文字仍有標籤，頁面卻不會顯示圖片；舊邏輯仍把它算成一張圖。[CommonMark 0.31.2 的 HTML block 規則](https://spec.commonmark.org/0.31.2/#html-blocks)說明註解中的標籤不是可見圖片。

~~~html
<!-- <img src="/assets/demo.svg" alt="Terminal demo" /> -->
~~~

<figure class="ai-review-counterexample not-prose" lang="zh-tw">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-counterexample.mobile.still.zh-tw.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-counterexample.still.zh-tw.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-counterexample.mobile.zh-tw.svg" type="image/svg+xml"><img src="/images/posts/vibe-coding-ai-code-review-evidence-gates/ai-review-counterexample.zh-tw.svg" alt="首輪修正後的 10/10 測試通過後，獨立審查仍發現註解內的圖片被誤計為可見圖片；最終 12/12 通過，四個獨立探針遭拒。" width="1200" height="700" loading="lazy" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption style="font-size:0.8rem;line-height:1.5;opacity:.78;margin-top:.35rem">測試全綠仍會漏掉反例：註解中的圖片標籤不代表頁面可見圖片。</figcaption>
</figure>

## 把測試結果綁定到精確版本

| 程式版本 | 主測試套件 | 後續獨立審查 |
| --- | --- | --- |
| 原始版本 | 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、分支、設定和呼叫點，確認它們保護什麼不變條件、是否重複、是否有真實使用者。程式碼更短不一定更簡單；刪除路徑防護可能重新引入競態。


<!-- vxi-evidence-panel: ai-code-review-counterexamples -->

延伸閱讀：[Mutation Testing](/posts/ai-generated-tests-mutation-testing/)、[OpenQodex 的 pre-push 審閱失敗邊界](/posts/openqodex-prepush-review-fail-open/) 與 [Diffle 的評論錨點](/posts/diffle-git-diff-comment-anchoring/)。

## 從測試通過到正式放行，還差什麼？

最終獨立複驗涵蓋 Node.js v22.16.0、Linux x64 的離線校驗器：主測試 12/12 通過，四個額外攻擊探針全數遭拒，canonical SVG、manifest 與八語綁定正例通過，三個 JavaScript 檔案也通過 node --check。

這支持有邊界的結論：在該環境與固定規則下，四類已知錯誤接受已修正。但它不能證明整個部落格已可正式上線。原生 macOS 面對惡意併發檔案寫入的能力尚未驗證，完整網站建置與最終頁面執行驗收也不在本次離線測試範圍。

實際放行線應依風險分層：單元測試檢查已涵蓋行為，靜態檢查尋找特定模式，建置與整合測試確認模組組合，目標平台和執行環境驗收檢查部署邊界。關鍵風險尚未驗證，就不能把局部通過說成整體可上線。

用契約與反例找出最值得懷疑的行為，再讓精確版本上的測試與獨立證據決定下一步該深讀哪裡。這不是取消人工判斷，而是讓 Review 從「看起來沒問題」變成有證據、有範圍、能複驗的決定。

## Verification context

- Level: first-party
- Verified: 2026-10-25T00:00:00.000Z
- Environment: Node.js v22.16.0 / Linux x64
- Evidence mode: mixed
- Public repository: https://github.com/vxime/ai-code-review-counterexamples
- Evidence commit: 74d9f12331ef4d015bf882a6e09d66e59434368b
- Evidence receipt: https://vxi.me/evidence/vibe-coding-ai-code-review-evidence-gates.json
