
第四幕問的是:不知道正確答案的時候,還測得動嗎?
今天結帳。把十份 AI 寫的蛻變測試放到那十四個變異體前面,看它們有沒有抓到人漏掉的。
第一次量變異分數的時候,人手寫的那套測試殺掉 12 個,存活兩個:
✗ 存活 M09 金額負值校驗失效
✗ 存活 M14 退休後固定收入未隨通膨調升
今天重跑,數字一樣:85.7%,存活的還是這兩個。
當時的驗屍寫得很清楚,而且比「分數不夠高」難堪得多:
兩個存活的都不是「斷言不夠嚴」,是「案例沒餵那個輸入」。
M09 的變異是把金額負值的檢查整段換成 pass:
# 原本
if any(amt < 0 for amt in amounts):
raise ValueError("金額不得為負數")
# 變異後
pass
那二十三組案例裡,沒有一組餵過負值。那個 if 執行了二十三次,條件恆為 False,raise 一次都沒進去。所以拿掉整段檢查,沒有任何一組會變。
M14 的情況更隱蔽:total_income += other_income * 12 * (1 + i)^k 那一行每一組都執行了,但二十三組的 other_income 全部是 0。零乘什麼都是零,拿掉通膨指數沒有任何一組會變。那一行被執行過,從來沒被檢驗過:這正是行覆蓋率那篇當時只能推論的事,現在有一個具名的變異體站在那裡。
更難堪的是,退休後收入要隨通膨調升這件事,規格考古那天就裁決過、寫進 PRD-10,後來還改過一次基準年。決定做了兩次,寫了兩次,然後沒有任何一組測試輸入讓那個欄位非零。
M09 是沒觸發例外,M14 是用零掩蓋了運算。兩格都不是斷言的問題,是資料根本沒把真正的邏輯逼出來。
十份產物逐條跑,只有一份碰到了那兩格之一。
run-B-MR-01.txt|關係 17 條|變異體 14 個
test_mr_prd05_out_of_range_lump_sum_invariance 殺掉 2 M06、M07
test_mr_prd09_surplus_income_ceiling_invariance 殺掉 1 M05
test_mr_prd03_prd12_negative_balance_no_clipping 殺掉 1 M02
test_mr_prd04_input_validation_negative_amount 殺掉 1 M09
計分 17 條|聯集殺掉 5 / 14(35.7%)
三個數字在這裡對上了:它單獨是 5/14,而人寫的那套已經有 12/14,兩邊聯集正好 13/14,也就是 92.9%,跟流水線當初量到的合計分數一致。它多出來的那一個,就是 M09。
而補上它的是這條:
# 把月投資金額輸入為負值;呼叫 calculate 必須拋出例外;
# 因為 PRD-04 規定金額必須非負且禁止靜默丟棄。
def test_mr_prd04_input_validation_negative_amount():
p_negative = replace(make_base_params(), monthly_investment=-500.0)
with pytest.raises(Exception):
calculate(p_negative)
這不是蛻變關係。 它只執行一次,不比較兩次執行之間的任何東西。提示詞從頭到尾要的是蛻變測試,而真正補上人類缺口的那一條,是一條最土的 pytest.raises。
那十七條裡,13 條殺不掉這十四種錯法中的任何一個。會殺的四條,三條是蛻變關係(兩條不變量、一條單調性),殺的都是人寫的那套本來就殺得掉的;唯一補上人類缺口的那一條,不是蛻變關係。 至於齊次、疊加、對目標金額的單調性那些數學上最漂亮的,全部掛零,跟前幾天人手寫那四條關係的結果一樣。
另一格 M14,十份、142 條計分的關係,沒有任何一條碰到。跑之前就猜到了:前幾天寫關係的時候逐條推過,四類關係都碰不到它——今天只是兌現。
另外有一份也寫了校驗測試,但它沒碰到 M09。
run-A-MR-01.txt
test_mr_input_validation 這批錯法碰不到
翻開看,它測的是年齡順序:現齡等於退休年齡時必須拋出例外。而 M09 改的是金額負值那一段,兩者不相干。
差別只在切法。那份補上 M09 的把同一條規格拆成兩個測試:
test_mr_prd04_input_validation_invalid_age_ordering ← 年齡
test_mr_prd04_input_validation_negative_amount ← 負值
另一份寫成一條,只寫了年齡。
掃過十份:
| 寫了校驗測試 | 其中測「金額負值」 | |
|---|---|---|
| 十份產物 | 2 份 | 1 份 |
八份完全沒寫校驗測試。而提示詞要的是蛻變測試:所以那八份是照著指示做的,補上缺口的那一份,寫了指示沒有要求的東西。
一、變異體活下來的時候,先問「有沒有餵過那個輸入」。
直覺會先怪斷言太鬆。今天這兩格都不是——從來沒有一組輸入把那段邏輯逼出來。
行覆蓋率在這裡只是半個答案。M09 的 raise 沒跑到,覆蓋率會標紅,但它標的是「沒跑到」,不是「沒測到」,中間還隔一步。M14 那行乘以零照樣算執行過,覆蓋率是綠的,什麼都看不出來。
二、要 AI 補測試的時候,「補什麼」比「寫得多好」重要。
那條補上缺口的測試,論精巧程度是十份裡最低的幾條之一。它值錢是因為它餵了一個沒有人餵過的輸入。八份寫了大量結構漂亮的關係,在這十四種錯法上一格都沒多補。
三、把同一條規格拆開寫。
那份補上 M09 的,只是把一條規格的兩個子句分成兩個測試。另一份寫成一條,就只覆蓋了其中一句。這個習慣不用聰明,只要不偷懶。
草稿的骨架原本寫成「九份對人寫的測試毫無增益」,還加了一句「有點打臉第四幕」。數字確實是那樣,但那把尺不能這樣用:十四個變異體是我自己設計的目錄,殺不掉只說明錯法碰不到,不說明關係沒價值。三天前才更正過一次,又滑了。
老實說改完比較無趣。「九份都沒用」讀起來像個結論,「一份抓到一個」讀起來像個腳註。但前者是我沒有的東西。
這些數字不能拿來做什麼
逐條結果、事前預測與判定分支在 manifest.yaml。
只帶走一件事
變異體活下來的時候,先別怪斷言太鬆。
去查有沒有任何一組輸入,真的把那一行逼出來過。
判斷 AI 有沒有補上缺口,用的仍然是人做的那份目錄。
M09 被補上了,是因為有人先把「金額負值」寫成一個變異體。
那些沒被寫進目錄的錯法呢?
今天的 repo:https://github.com/eyelash500/2026_ironman_test_ai/tree/day28