iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護系列 第 26 篇

Day 26|AI 說測試全綠還不夠:如何固定版本、重跑驗證,留下可查的完成證據?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

「Build 成功、測試全綠、需求完成。」

這三句,我在 AI Agent 的完成回報裡看過很多次。看起來很完整,卻沒有交代驗證的是哪一份 Code、實際跑了哪些檢查,也沒有說綠燈在哪裡停止。

昨天修正人工重送功能後,我把產品固定在完整 Commit SHA,再請 Codex GPT-5.6-SOL-HIGH 整理一份 Evidence Packet。它除了保存 Prompt、Diff、環境、命令、Exit Code 與限制,也產生一支可以重跑六項 Gate 的 PowerShell 腳本。

腳本先通過語法檢查,真正執行時卻被 dotnet --info 裡正常的空白行弄壞。修正後,我再從另一個乾淨 Worktree 直接執行同一支腳本,才得到 Restore、Build、Format、Test 與 Smoke 的完整結果:93 項測試成功、0 項失敗,重跑相同資料也沒有再次處理或通知。

這份結果仍只發生在同一台 Windows 主機。Linux、macOS、Container、真實 Provider、Coverage 與 Mutation 都沒有因此自動變成已驗證。

所以今天真正要回答的問題只有一個:

AI 說完成時,我能不能把這句話綁定到固定版本、可理解的控制流程、實際執行結果與清楚的未知範圍,讓另一個執行流程重新驗證?

如果下一位工程師不知道使用哪個 Commit、在哪種環境執行,也不知道哪些結論來自工具、哪些只是人工判讀,那句「全部通過」就像一張只有「合格」兩個字的驗車單:沒有車牌、檢查項目與日期,根本不知道它證明了什麼。

〈可重複的證明〉把「我跑過」提升成「相同主張可以再驗一次」

〈可重複的證明〉要求完成主張可以被重新執行與查驗。多保存幾張綠色截圖,還不足以做到這件事。

流程拆成責任清楚的單元後,Reviewer 才能逐一確認輸入、輸出、負責規則與失敗位置。Clean Code 透過小函式、清楚命名、單一責任與架構邊界,讓測試能對準真正的行為,完成宣告也能指出哪項主張由哪份證據支援。

對這個 Work Item API,我能留下的是有範圍的工程證據:單元測試、驗收測試、靜態檢查、Build、Locked Restore 與 Smoke Test。它們無法證明系統永遠正確,只能說明固定版本在明列條件下通過哪些驗證,以及哪些部分仍然未知。

我把這條因果鏈整理如下:

flowchart LR
    A[說清楚完成主張] --> B[拆出可理解的責任與控制流程]
    B --> C[替各項行為找到測試或驗證關卡]
    C --> D[固定產品 Commit]
    D --> E[記錄環境選擇規則與實際版本]
    E --> F[保存命令、Exit Code 與原始輸出]
    F --> G[標示人工判讀與未知範圍]
    G --> H[由另一個流程重新執行]

如果只保存最後的成功摘要,規格、版本、環境與限制都會消失,下一個流程也無法重新驗證原本的完成主張。

我把書中的觀念延伸成證據封包,但它不是書中規定的格式

我把今天的交付物稱為「證據封包(Evidence Packet)」。

《無瑕的程式碼 第二版》沒有規定這套文件格式。這是我把〈可重複的證明〉延伸到 AI Coding 後,整理出的工程做法:把 Prompt、產品版本、驗證命令、原始輸出、人工判讀與限制放在一起,讓 Reviewer 能從完成主張一路查回原始資料。

一份能用來 Review 的證據封包,至少要回答六類問題:

證據內容 要回答的問題 缺少時會發生什麼事
完成主張與輸入 Agent 收到哪份需求、Prompt、限制與驗收條件? 綠燈存在,卻不知道原本答應完成什麼。
產品座標 驗證的是哪個 Commit、Tag 與 Diff? 結果無法綁定到確切 Code。
執行情境 使用哪個 SDK、Runtime、OS、套件選擇規則與外部替身? 重新執行出現差異時,無法判斷原因。
控制流程與驗證方式 規格經過哪些 Controller、Use Case、Adapter 與副作用?由什麼測試或關卡支援? 只看見測試總數,看不出保護了哪段行為。
原始結果與人工判讀 實際執行什麼命令、Exit Code 是多少?哪些對應是人看原始碼後做的判斷? Agent 摘要會和工具觀察混在一起。
限制與重跑方式 哪些項目沒執行、哪些環境沒驗證,下一個流程如何重跑? 未知範圍容易被誤寫成已通過。

我把這個角色稱為「證據整理 Agent(Evidence Compiler)」。這裡的 Compiler 是彙整證據,不是 C# 編譯器;它只能整理既有資料,不能修改產品來換取綠燈。

固定單一產品版本,演練 AI 的完成宣告如何交接

本次只驗證一個已接受版本,觀察證據封包能否支援另一個流程重跑。Prompt 與 Token 不在比較範圍內。

實驗固定兩個不同的 Git 座標:

座標 用途
28758b4d9345e890e22a650d3bd7e38933982b42 昨天接受的產品版本,Production Code 與 Tests 都固定在這裡。
de20071e528e356d960f40bad62dd8e2cb590d5e 今天保存證據封包、重跑腳本與原始輸出的發布版本。

驗證結果最後要綁定完整 Commit SHA;Annotated Tag 只是方便讀者找到對應位置。產品版本使用 day-25-harm-behavior-structure,證據發布版本使用 day-26-repeatable-proof。

我另外建立兩個 Worktree。一個讓證據整理 Agent 寫文件,另一個使用 Detached HEAD,直接固定在待驗證 Commit,不會跟著任何 Branch 往前移動。Agent 可以讀取產品 Worktree,不能在裡面修改 Code。

這些約束主要防止三件事:驗證對象漂移、Agent 為了綠燈修改產品,以及未執行項目被寫成通過。

實驗設計 為什麼要這樣做
固定完整 Commit SHA 避免 Agent 整理證據時,驗證對象已經悄悄改變。
證據 Worktree 與產品 Worktree 分開 讓文件與 Log 的新增不會污染待驗證產品版本。
禁止修改 src、tests、Solution、套件與 Lock File 避免 Agent 為了取得綠燈而移動功能或驗收標準。
缺少 Coverage、Mutation 或跨環境條件時必須標示未執行 不讓空白欄位被寫成通過。
Agent 自己跑完後,由主流程再次執行 驗證腳本真的能離開產生它的 Session,被另一個流程使用。

這次的核心 Prompt 如下,完整版本也保存在公開 Repository:

你正在替公開的 Work Item API 建立一份「可重複的證明」初稿。
你的任務是整理證據,不是修改產品。

固定條件:
- 執行模型為 Codex GPT-5.6-SOL-HIGH。
- 待驗證版本必須是 Annotated Tag
  day-25-harm-behavior-structure 指向的 Commit:
  28758b4d9345e890e22a650d3bd7e38933982b42。
- 禁止修改 src、tests、Solution、套件版本、Lock File、
  既有 Evidence 與 Repository Instruction。
- 不要 Commit、Tag、Push 或移除檔案。

你要回答:
另一位工程師只拿到固定 Commit 與這份 Evidence Packet,
能否理解完成宣告涵蓋什麼、執行相同 Gate,
並區分工具結果、人工判斷與未知範圍?

必做工作:
1. 驗證乾淨 Repository 的 HEAD、Git Status、Tag 與 Commit。
2. 盤點逾期處理與通知重試的主要控制流程。
3. 把昨天的外部行為與結構條件對應到測試或 Gate。
4. 執行固定的 Restore、Build、Format、Test 與 Smoke。
5. Coverage、Mutation、套件弱點與跨環境重現缺少條件時,
   標成未執行或受限,不得寫成通過。
6. 建立 README、proof matrix、replay.ps1、limitations,
   並保存每項 Gate 的完整命令、退出碼與原始輸出。
7. 說明 global.json 與 packages.lock.json 能固定到什麼程度,
   不得把相容 Patch、相同套件圖與位元級重現混為一談。

完成標準:
- 每個通過宣告都能連到原始輸出。
- 每個規格都能連到可理解的程式單元與驗證方式,或明列缺口。
- 失敗、跳過、網路限制與人工判斷都可見。
- 不把測試與工具輸出描述成完整數學證明。
- Production Code 與 Tests 保持零 Diff。

證據整理 Agent 不能臨時安裝 Coverage 工具,也不能改寫測試或 Production Code。只要它改變產品或驗收方式,今天驗證的對象就不再是原本的固定 Commit。

Clean Code 結構先回答責任在哪裡,證據矩陣才能回答怎麼驗

只列出測試數量,讀者仍然不知道它們保護哪段系統。證據整理 Agent 因此先追蹤兩條 HTTP 入口。

第一條是逾期處理。Controller 呼叫 IOverdueWorkItemProcessor;Processor 先讓 Marker 判定逾期並保存 Work Item 狀態與 Outbox,再交給 Dispatcher 處理 Pending 通知。

第二條是昨天新增的人工通知重試。Retry Scheduler 會找到既有 Pending Outbox,重新排定同一筆通知意圖;外部呼叫仍統一交給 Dispatcher。

flowchart LR
    A[POST process-overdue] --> B[Overdue Work Item Processor]
    B --> C[Marker 判定逾期]
    C --> D[(Work Item 與 Outbox 同次保存)]
    D --> E[Outbox Dispatcher]

    F[POST overdue-notification retry] --> G[Retry Scheduler]
    G --> H[重新排定既有 Pending Outbox]
    H --> E

    E --> I[Notification Sender]
    I --> J[Provider]

這張圖先把 HTTP 回應、逾期判定、資料保存與通知傳送分開。繼續往下追,還能找到重新排定、Lease Claim、ACK、Retry 與 Cancellation 各自的負責位置。

有了這份責任拆分,Reviewer 才能從外部行為追到負責的程式單元與測試。本次沒有準備控制流程混亂的對照版本,所以只能確認目前結構足以建立對照,不能推論它節省了多少 Token。

證據對照矩陣分開工具結果、人工判讀與未知範圍

Agent 接著建立 Proof Matrix。為了避免把它誤解成完整數學證明,正文稱它為「證據對照矩陣」:每一列都把完成主張、負責單元、驗證方式與未知範圍排在一起。

下面是其中幾列的簡化版本:

完成主張 負責單元 本次證據 證據停止在哪裡
找不到 Work Item 時,API 回 404 Not Found Controller 將 Use Case 結果映射成 HTTP 回應 具名契約測試存在,完整測試關卡通過 沒有部署環境的 HTTP Trace
沒有可重新排定的 Pending Outbox 時,API 回 409 Conflict Retry Scheduler 查詢通知意圖 契約測試存在,完整測試關卡通過 沒有涵蓋所有 Lease 競爭組合
相同 POST 重複執行時沿用同一筆 Outbox 與冪等鍵 Scheduler 只更新既有通知,不新增一筆 重複呼叫測試存在,完整測試關卡通過 只驗證單機、循序 Request
Controller 不直接呼叫 Sender HTTP 入口只依賴 Retry Use Case Architecture Test 存在,完整測試關卡通過 Source Scan 不是完整依賴圖證明
先保存狀態與 Outbox,再呼叫 Dispatcher Processor 固定協調順序 Boundary Test 與行為測試存在 沒有分散式交易驗證
Lost ACK 後沿用同一把 Key 重試 Dispatcher 與 Provider 契約 Dispatcher 測試存在,完整測試關卡通過 非冪等 Provider 仍可能產生重複外部效果

這張表沒有把每一列都寫成「已證明」。本次 dotnet test 只保存整體摘要,沒有產生 TRX;TRX 是能記錄測試回合與逐項結果的結構化檔案。因此,具名測試與需求的對應是我閱讀原始碼後建立的人工判讀,工具只能證明整體測試命令成功。

Restore、Build、Format、Test 與 Smoke 全部通過,仍只涵蓋明列的驗證範圍

Agent 實際執行的主要命令如下:

dotnet --info
dotnet restore .\AiCleanCode.sln --locked-mode
dotnet build .\AiCleanCode.sln --configuration Release --no-restore
dotnet format .\AiCleanCode.sln --verify-no-changes --no-restore
dotnet test .\AiCleanCode.sln --configuration Release --no-build --no-restore
.\scripts\run-series-baseline-smoke.ps1

每項驗證關卡回答的問題不同:

驗證關卡 能回答什麼 不能回答什麼
.NET 環境資訊 本次實際選到的 SDK、Runtime、OS 與 win-x64 執行環境識別 其他機器會不會取得完全相同環境
Locked Restore Lock File 與專案定義能否在鎖定模式下還原 NuGet 服務永遠可用、所有供應鏈都安全
Release Build 固定版本能否編譯,是否出現 Warning 或 Error 執行行為一定正確
Format Verify Repository 的格式規則是否被破壞 命名、責任與架構是否合理
Release Test 已寫入測試的 Assertion 是否通過 沒有被測試的需求與未知狀態
HTTP Smoke API 啟動後,主要成功路徑與相同資料重跑是否成立 所有失敗、並行、部署與真實 Provider 行為

證據封包會保留每項關卡的名稱、命令與涵蓋範圍,Reviewer 才知道這些綠燈分別回答了什麼。

從固定 Commit SHA 到 Restore、Build、Format、Test、Smoke、原始輸出與人工判斷的證據鏈

圖:可重複證明要固定版本、保存原始輸出並列出未知;所有工具全綠仍不等於全部需求已被證明。

global.json 與 Lock File 固定的是選擇規則,不是整台電腦

待驗證版本有一份 global.json 與五份 packages.lock.json,Agent 也真的使用 --locked-mode 還原套件。

Lock File 會保存 NuGet 解析後的直接與傳遞套件版本。當專案相依需求與 Lock File 不一致時,Locked Mode 應直接失敗,而不是悄悄改寫套件圖。這能降低套件解析結果在不同時間漂移的風險。

可是它仍然沒有凍結整個執行環境。

這個 Repository 的 global.json 指定 SDK 10.0.300,並設定 rollForward: latestPatch。依這項規則,.NET 會在相同 Major、Minor 與 Feature Band 中,選擇不低於指定版本的最新已安裝 Patch;找不到符合版本時就失敗。

Microsoft 文件建議,在 Package Lock File 必須與 SDK 嚴格同步時,把 rollForward 設成 disable。本 Demo 保留 latestPatch,接受同一 Feature Band 內的 Patch 更新,也就沒有要求每台機器使用完全相同的 SDK。

本次實際選到 SDK 10.0.300,Host Runtime 則是 10.0.8。SDK 與 Runtime 是兩個版本維度,OS、NuGet Cache、SQLite 原生資產與語系也可能不同。

Locked Restore 只能約束套件解析;本次沒有比較 DLL、PDB 等建置產物,因此沒有驗證位元級重現。

語法檢查通過,重跑腳本仍被正常空白行弄壞

證據整理 Agent 寫完重跑驗證腳本(Replay)後,先做了 PowerShell 語法檢查。語法通過,看起來一切正常。

真正執行時,腳本卻停在第一項主要關卡 dotnet --info:

Cannot bind argument to parameter 'RawOutput'
because it is an empty string.

dotnet --info 的輸出本來就包含空白行。Agent 產生的 Write-GateOutput 參數不接受空字串,於是這支「用來證明其他東西沒壞」的腳本,反而先被正常輸出弄壞。

這次出錯的不是 Production Code。產品測試甚至沒有機會先報錯,因為重跑流程在進入主要驗證前就停止了。

Agent 最後替 RawOutput 加上 [AllowEmptyString()],保留 Exit Code 1 與錯誤原因,再改用新的輸出目錄重跑。修正後,六項主要關卡才完整執行成功。

另一次重跑因外層執行器只給一秒而中止,沒有留下完整結果。我把它記為執行環境中斷,不算產品失敗,也不算驗證通過;細節保留在公開 Evidence。

如果只把最後成功的 Log 放上 GitHub,讀者就不會知道這支腳本曾經無法處理正常輸出。

產品要留下證據,產生與重播證據的工具也必須真的執行過。腳本沒有實際跑過,就不能宣稱這份證據可以重複。

另一個乾淨 Worktree 成功重跑腳本,但仍是同一台 Windows 主機

Agent 修好腳本並自行重跑成功後,我先 Review 腳本做了哪些事:

  • 執行前解析並比較 HEAD 與 Expected Commit。
  • Git Status 不是乾淨狀態就停止。
  • 輸出目錄已有內容就拒絕覆寫。
  • 不新增或更新 SDK、套件版本、Coverage 或 Mutation 工具;只依 Lock File 還原既有相依套件。
  • 依固定順序執行六項主要關卡。
  • 任一關卡的 Exit Code 不為 0,就停止並留下已產生的輸出。
  • 執行完成後再次檢查 tracked Diff。

我把「主流程複驗」定義為:從另一個乾淨 Worktree 直接執行 Agent 產生的腳本,並把結果保存到新的輸出目錄。

主流程結果如下:

驗證範圍 實際結果
Repository Preflight HEAD 與指定 Commit 相同,執行前工作區乾淨
.NET 環境 SDK 10.0.300、Host Runtime 10.0.8、Windows win-x64
Locked Restore 五個專案依 Lock File 還原成功
Release Build 零個 Warning、零個 Error
Format Verify 沒有格式差異
Release Test 測試命令成功完成;原始摘要記錄 93 項成功、0 項失敗、0 項略過
HTTP Smoke 第一次處理兩筆到期資料並嘗試兩次通知;對相同資料重跑時,沒有再次處理或通知

93 是測試執行數量,不代表 93 種風險都已涵蓋。這份結果能支持的主張是:固定產品 Commit 在本次 Windows/.NET 環境中,可以依相同順序完成六項驗證,而且重跑腳本能被另一個執行流程使用。

它不能支持的主張包括:

  • Linux、macOS、Container、其他 SDK Patch 或其他資料庫也已重現。
  • 真實通知 Provider、部署環境與長時間 Worker 已驗證。
  • Outbox、Lease 與冪等鍵已提供 Exactly Once。
  • Coverage 與 Mutation Test 已經通過。
  • 另一位工程師或另一台主機已實際完成重跑。

Repository 沒有固定的 Coverage 與 Mutation Test 工具或命令,所以本次標記為「未執行」。我也沒有在驗證途中臨時加入套件與門檻,避免改變固定產品版本的驗收條件。

建立證據封包成本很高,本次不能宣稱節省 Token

這次證據整理 Agent 的單一 Session 使用了 205,358 個 Fresh Input Token。完整的執行時間、Cached Input、Output、Reasoning 與工具呼叫次數都保留在公開 Evidence。

這不是節省 Token 的案例。

Agent 需要讀取規格、Production Code、Tests、既有 Evidence 與工具輸出,還要建立矩陣並實際驗證重跑腳本。這些工作構成主要的 Context 成本。

本次沒有安排「Reviewer 不使用證據封包」的控制組,也沒有讓兩組 Reviewer 執行相同交接任務。因此,205,358 個 Fresh Input Token 只能描述這次成本,不能推論後續一定省 Token。

目前能確認的價值比較窄:Reviewer 不必只靠一句「測試全綠」猜測完成範圍,可以直接從固定座標、控制流程、原始結果與未知範圍開始查證。

CLEAN 原則:用 A 區分完成主張、工具結果、人工判讀與未知範圍

A — Auditable by Evidence 實據可審:完成回報要分清楚主張、結果、判讀與未知

Agent 最後那段「已完成」只能算一筆回報,不能自動升級成完成證據。

A — Auditable by Evidence 實據可審 要求 User 至少分開四個層次:

層次 要問的問題
完成主張 這次究竟答應完成什麼?驗證哪個版本?
工具結果 實際執行哪些命令?Exit Code 與原始輸出是什麼?
人工判讀 哪些規格對應、契約與接受決策是 Reviewer 的工程判斷?
未知範圍 哪些工具沒執行、哪些環境與失敗路徑沒有涵蓋?

Agent 回報六項關卡通過後,我仍要核對產品 Commit、Review 重跑腳本並親自複驗。Coverage、Mutation、跨環境與真實 Provider 則保留在未知範圍。A 關心的是每份證據能支撐哪項風險,以及證據在哪裡停止。

高風險外部副作用需要較重證據,修改標點不需要

證據封包不應變成所有修改都必須繳交的巨大表格。合理的證據重量會跟著風險改變:

變更情境 合理證據
修正文件錯字或標點 固定 Diff、Markdown 檢查與人工預覽
內部 Rename Diff、編譯、格式、呼叫端與外部 Contract 測試
單一純函式規則 具名單元測試、邊界值與完整測試
資料庫結構、授權、交易或公開 API 資料庫變更、Contract/整合測試、錯誤路徑與回復方式
通知、付款或其他外部副作用 重送、冪等、Lost ACK、持久化意圖、Smoke 與人工契約決策
發布與跨環境交付 CI 產物、環境資訊、部署紀錄、健康檢查與 UAT

Work Item API 涉及 Outbox、人工重送與外部通知,所以需要保存控制流程、重跑腳本與限制矩陣。修改 Markdown 標點時,只要固定 Diff、執行 Markdown 檢查並人工預覽即可。

讀者先取得證據封包,再用固定 SHA 重跑產品版本

完整 Prompt、去識別化後的 Agent Session、證據對照矩陣、失敗紀錄、重跑腳本與主流程輸出,都已放進公開的 API Demo Repository。

讀者可以先切到 Evidence Tag 取得證據封包,再建立另一個 Worktree 固定產品版本:

git clone https://github.com/eric861129/AI-CleanCode-API-Demo.git
cd AI-CleanCode-API-Demo
git fetch --tags
git switch --detach day-26-repeatable-proof
git worktree add --detach ..\day-26-clean-replay 28758b4d9345e890e22a650d3bd7e38933982b42

.\docs\evidence\day-26\repeatable-proof\agent-draft\replay.ps1 `
  -RepositoryPath '..\day-26-clean-replay' `
  -ExpectedCommit '28758b4d9345e890e22a650d3bd7e38933982b42'

這些指令提供另一位工程師重跑的入口,但本次還沒有觀察到另一位工程師或另一台主機完成驗證。讀者執行時仍要使用符合 global.json 選擇規則的環境,並確認相同命令與明列行為條件成立;路徑、耗時、GUID、Port、語系與 Log 文字本來就可能不同。

完成證據要能重跑,也要說清楚未知範圍

回到標題,AI 說「測試全綠」還不夠,因為這句話若沒有固定 Commit、環境、命令、Exit Code 與原始輸出,就無法確認它究竟描述哪一次執行。即使這些都補齊,也還要把人工判讀與未知範圍分開。

這次真正成立的,是固定產品 Commit 能在本次 Windows/.NET 環境中,由另一個乾淨 Worktree 依相同腳本重跑六項 Gate。它沒有證明跨平台、真實 Provider、Coverage、Mutation 或 Production Deployment。

證據工具本身也要接受驗證。Replay 腳本通過語法檢查,仍被正常空白行弄壞;保留失敗、修正後再重新執行,才確認它真的能用。A — Auditable by Evidence 實據可審 要求 User 固定產品版本,分清楚工具結果、人工判讀與未知項目,再決定這份證據是否足以承擔本次風險。

今天把交付後的完成結果做成可重跑證據;明天,我會把驗證時間往前移,看看一次累積大批修改,和多個能快速整合、驗證與回復的小週期,會帶來什麼差別。

參考資料


上一篇
Day 25|AI 寫完功能、測試全綠,為什麼還會重複通知?
下一篇
Day 27|AI 一次改完再驗證,還是拆成三個小週期?比較紅燈時機、回退範圍與測試品質
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護 共 27 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言