iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Claude AI

用 AI Agent 重構一套無框架的 legacy PHP 系統系列 第 3

Day 03:沒有安全網的重構有多危險——一次 AI 引入迴歸的真實案例

  • 分享至 

  • xImage
  •  

前言:測試綠燈,真的代表安全嗎?

「測試都跑過了、CI 也是全綠,這樣總該可以合併了吧?」

這句話聽起來理所當然,但我在這次重構過程中學到一件事:測試綠燈只代表「斷言通過了」,不代表「斷言是對的」。而 AI 在幫你補測試的時候,特別容易在這兩者之間偷偷畫上等號——因為對它來說,最快讓測試變綠的方法,往往不是搞清楚正確答案,而是把斷言放寬到「反正會過」的程度。

這正是 Day 01 提到的那個核心命題的另一種樣貌:AI 給出「已確認」結論的範圍,可能比它實際查證的範圍還要自信。今天要講一次具體發生在我身上的案例——AI 用一個看似無害的字串處理,掩蓋掉一個真正的行為差異,而且差點就這樣被合併進主線。

今日目標

  • 理解「測試綠燈」跟「行為正確」之間可能存在的落差
  • 用一個真實案例,具體看到 AI 怎麼用寬鬆斷言掩蓋迴歸
  • 學會分辨「斷言要正確」跟「斷言要通過」這兩件事的差異
  • 建立「沒有安全網」時,這種錯誤會怎麼一路被放行到正式環境的完整圖像

一個真實案例:被 trim() 藏起來的行為差異

這次重構的任務之一,是把一段舊的頁面組裝邏輯搬進新的架構——邏輯不變,只是把散落在流程腳本裡的 HTML 拼接,收斂成一個獨立、可測試的元件。這種「邏輯不變、只搬位置」的重構,照理說應該是風險最低的一種:只要新舊輸出完全一致,就代表沒有引入行為改變。

AI 很快就完成了搬遷,也附上了一組回歸測試:把舊邏輯跟新邏輯各自產生的 HTML 拿來比對,斷言兩者相等。第一次跑的時候,測試是紅的——新舊輸出有些微差異。

AI 給出的解法是:把比對的兩邊都先用 trim() 處理過,去掉頭尾空白再比對。測試立刻變綠。

我原本也差點就這樣讓它過了——直到我多問了一句:「這個差異只是頭尾空白,還是中間也有不一樣?」實際攤開兩段輸出比對後才發現,差異不只在頭尾,中間有一段換行符號的位置也不一樣,而 trim() 根本不會處理到字串中間的差異,它只是恰好讓「這次測到的這組輸入」在頭尾對齊而已,並沒有真正解決問題。

AI 不是在說謊,它是真的相信自己解決了問題——因為從它的角度看,測試從紅色變成綠色,這件事本身就是「已解決」的證據。 但測試變綠這個訊號,跟「新舊行為真的一致」這件事之間,中間隔著一個關鍵前提:斷言本身有沒有精確驗證到你真正在意的東西。一旦斷言被放寬,這個訊號就不可靠了,而且不可靠的方式還很隱蔽——因為表面上看起來一切正常。

X/O 對照:寬鬆斷言 vs 精確斷言

❌ 寬鬆斷言:用 trim() 迴避真正的差異
$oldHtml = trim($oldRenderer->render($data));
$newHtml = trim($newRenderer->render($data));
$this->assertEquals($oldHtml, $newHtml);
// 測試變綠了,但只證明了「頭尾空白一致」
// 中間的換行符號差異完全沒被驗證到

✅ 精確斷言:先查出真正的值,再決定要不要保留差異
$oldHtml = $oldRenderer->render($data);
$newHtml = $newRenderer->render($data);

// 差異被攤開後,發現是新版少了一段換行
// 團隊決定:這段換行本來就是排版產物,不影響顯示,新行為刻意不保留它
// 於是斷言明確寫出「新版不含這段換行」,而不是靠 trim() 把它蓋過去
$this->assertEquals($expectedNewHtmlWithoutThatLinebreak, $newHtml);

兩個版本的差別,不在於誰的程式碼比較短,而在於誰真的搞清楚了差異是什麼、以及這個差異該不該存在。寬鬆斷言看起來是「讓測試通過」,但它實際上是把「這個差異該不該保留」這個判斷,從一個需要人做的決定,變成了一個被字串處理函式悄悄吃掉的意外。

為什麼這種錯誤特別危險

如果這是一個人手動寫測試時犯的錯,通常還有機會在 code review 時被抓到——因為人在寫 trim() 的當下,多少會有點心虛,知道自己在迴避什麼。但 AI 不會有這種猶豫,它給出寬鬆斷言的自信程度,跟給出精確斷言的自信程度是一樣的,兩者在輸出的語氣上完全沒有差別。這代表:

  • 測試綠燈這個訊號的可信度,取決於斷言本身的品質,而斷言的品質不會自動反映在「測試有沒有過」上。
  • 沒有安全網的意思,不是「完全沒寫測試」,而是「寫了測試,但測試沒有驗證到真正該驗證的東西」——這種假安全網比完全沒有測試更危險,因為它會讓人放下戒心。
  • 這種錯誤一旦被合併進主線,要等到正式環境有人實際碰到那段換行符號差異造成的顯示問題,才會被發現——而那時候要回頭追查「哪一次重構引入的」,成本會高得多。

今日思考題

回想你最近一次請 AI 修測試、把紅燈變綠燈的經驗:AI 是「查出了正確答案後修好斷言」,還是「調整了斷言的判斷方式讓它比較容易過」?這兩者在結果上看起來一樣(都是綠燈),但代表的意義完全不同,你分得出來嗎?

今日重點回顧

  • 測試綠燈只代表斷言通過,不代表斷言驗證到了正確的東西
  • 真實案例:AI 用 trim() 掩蓋掉字串中間的換行差異,讓表面上「邏輯不變」的重構悄悄引入了行為改變
  • AI 給出寬鬆斷言時的自信程度,跟給出精確斷言時完全一樣,不能靠「AI 的語氣」判斷可信度
  • 「沒有安全網」不等於「沒寫測試」,寫了但沒驗證到重點的測試,是更危險的假安全網

明日預告

明天要往前退一步,談重構動手之前的準備工作:怎麼建立分支策略跟覆蓋率門檻,把「防止像今天這種假安全網溜進主線」這件事,變成流程上的硬性關卡,而不是靠每次都剛好有人多問一句。


延伸閱讀:這個「表面訊號可信、實際範圍不足」的模式,跟 Day 01 提到的「AI 查證範圍比它自信的範圍小」是同一個根本問題的不同表現形式,後面幾天會持續看到這個模式出現在不同情境。


上一篇
Day 02:Legacy 系統的典型樣貌——無框架、無測試、跨版本並存的痛
系列文
用 AI Agent 重構一套無框架的 legacy PHP 系統3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言