昨天那一輪交回來的東西裡,有一筆很刺眼。
結帳地址那一步,postcode 欄位預填的值是字串 missing value。任何人看到都會皺眉:那明顯是程式內部代表「查不到值」的字面字串,怎麼會跑到使用者的表單裡,而且還通得過必填驗證。

value="missing value"、placeholder="Your Postcode *"。它是真的值,不是提示文字,不改就會被送出去。
但 finding 上寫的是 status: anomaly,不是 fail。
它看到了,卻不敢說那是錯的。今天要回答為什麼,答案是判準沒寫進 charter,它就不敢判。
「這是錯的」這句話,比大多數人以為的難講。
你說 postcode 不該顯示 missing value。憑什麼?憑常識。但常識不是判準 —— 換一個產品,missing value 可能就是設計好要顯示的佔位文字;換一個團隊,那可能是他們刻意保留的除錯訊息。你覺得錯,是因為你腦子裡有一份沒說出口的期待。
Test oracle 就是那份期待被寫下來的樣子:你憑什麼說這是錯的。找不到判準,你只能說「怪」,不能說「錯」。
所以今天要把兩件事切開:
切開的好處是誠實。沒有 oracle 接得住的東西,就老實標成 anomaly 留著,不硬判成 fail。硬判的代價昨天算過了:一張退不回去的單,換來下一次沒人看你的單。
回到 postcode 那一筆。它的檔案裡有一段 verdict_history,記著它被判過兩次。
第一次判定(當天稍早):
verdict: anomaly(未判定)
oracle: user-expectation
why: "charter 四條 oracle 涵蓋不到;當時只知道 UI 顯示什麼,不知道 API 回什麼,
分不出是後端存了這個字串還是前端捏的。缺的就是上游那一筆證據。"
關鍵在最後一句。它分不出來的不是「這對不對」,是「這是誰做的」。如果後端真的把 missing value 存進資料庫,那是資料問題;如果是前端替 null 捏出來的,那是前端問題。兩種修法完全不同,而光看畫面,這兩種長得一模一樣。
當時手上只有「使用者期待」這條最弱的 oracle 撐著,所以它標 anomaly,交出去等人定奪。
第二次判定(同一天,端點側那輪跑完之後):
verdict: bug
oracle_used: 內部一致性
basis: >
兩處自相矛盾,都不需要外部規格:
(1) 同一筆資料兩個值 —— GET /users/me(同一個帳號 id=2)回 postcode: null,
但結帳地址表單的 [data-test=postcode] 顯示字串 "missing value"。
(2) 同一張表單、同樣的上游輸入,兩種處理 —— API 對 postcode 與 state 都回 null,
前端卻把 postcode 塞成 "missing value"、把 state 留成空字串。
同輸入不同輸出,前後不一。
結論:那五個字不是後端存進去的資料,是前端替 null 捏出來的字面值。
現象一模一樣,證據多了一筆,判準就從最弱那條換成最強那條。
GET /users/me 回 null 這件事,把「後端存的」那條分支砍掉了,剩下的解釋只有一種。而一旦能指著兩個互相矛盾的值,就不再需要任何人同意「postcode 不該顯示 missing value」這件事 —— 產品自己打自己臉。
這一筆說明了今天最重要的一件事:判不動的時候,缺的通常不是判斷力,是一筆證據。標 anomaly 不是認輸,是把缺口記下來等它被補上。
要誠實講一件事:r2 那一輪的五筆 finding 裡,只有這一筆被重判過,所以只有它補上了 verdict、oracle_used、basis 三個欄位。其他四筆用的還是 oracle 與 oracle_detail,跟 skill 規定的輸出欄位對不上。規格寫了、實作沒跟上,這種落差跟 Day 13 那個一筆都沒有的 category 是同一種病。
Test oracle 的來源不只一種。傳統測試教科書會告訴你三類:原有系統(開發新財務系統時,拿舊系統的穩定報表當比對標準)、使用者手冊或文件(手冊說選了「兩日送達」就該顯示預計送達日)、專家知識(心電圖的判讀要靠醫生)。
這三類的共同問題是它們都要問別人。所以 test-oracle 這支 skill 把它們重排成六條,排序的依據只有一個問題:要不要外部資訊?
| # | oracle | 判什麼 | 要外部資訊嗎 |
|---|---|---|---|
| 1 | 內部一致性 | 產品自己前後矛盾 | 不用 |
| 2 | API 對 UI | 畫面說的跟端點回的對不上 | 不用 |
| 3 | 無 console error | 正常操作不該噴錯 | 不用 |
| 4 | 規格 | 違反寫下來的約定 | 要,knowledge/ |
| 5 | 使用者期待 | 違反常識與領域慣例 | 要,而且沒有白紙黑字 |
| 6 | 對照品 | 同一功能別的地方不是這樣 | 要,要有對照組 |
前三條最強,因為它們不用等任何人回答。你不需要找 PM 確認、不需要翻規格、不需要問「這是不是刻意設計的」。產品自己提供了矛盾,你只要指出來。
r2 那一輪的 F-004 是最乾淨的例子:同一個畫面上,頁面內文寫著「Hello Jane Doe, you are already logged in」,導覽列同時寫著「User Data not found」。
不必知道規格、不必問任何人。這一頁自己說了兩句互相打臉的話,其中一定有一句是錯的。這種判定沒有人吵得贏,也不會因為換了 PM 就翻案。
反過來看第 5 條有多弱:postcode 第一次判定就是靠它,結果只能標 anomaly。「使用者期待」的問題不在它不對,在它沒有仲裁者 —— 你說使用者不期待看到 missing value,對方說我們的使用者不在意,這場架吵不完。
被測物是端點的時候,前面六條照樣成立(內部一致性、對照品這些跟介面層無關),但另外還有七條只有直接打端點才用得到:
| oracle | 判什麼 |
|---|---|
| 狀態碼語意 | 失敗回 200 包一個錯誤訊息,這是說謊 |
| error envelope | 同一個 API 的錯誤結構要一致 |
| 冪等重放 | 宣稱冪等的端點,送兩次結果要一樣 |
| 分頁排序 | 逐頁不重疊、不漏、總數對得上 |
| 授權邊界 | 誰能讀誰的資料 |
| 契約 schema | 回應要對得上契約,不多不少 |
| header 與 content-type | 送對的標頭、拒絕不支援的 |
最強的是狀態碼語意,因為它跟內部一致性同源:200 OK 配一個「付款失敗」的 body,那是產品自己打自己臉,不需要規格。
最弱的是契約 schema,因為它要有契約才可用。契約來源填「無」的時候,這條 oracle 直接停用 —— 不准拿「我覺得應該有這個欄位」當判準。
端點側判定長什麼樣,看實跑的這一筆:
id: F-001
verdict: bug
oracle_used: 授權邊界(主)+ 內部一致性(佐)
basis: >
顧客 token(login 回 id=2、role=user)打 GET /users/1 回 200,body 含 admin 完整記錄
(email=admin@、role=admin、password sha256 雜湊);requests.jsonl#06 /
raw/06-users-1-idor.body.json。
對照 #07 GET /users/999999 同 token 回 404「Requested item not found」
→ 該端點依 id 取真資料、非公開資源。
佐證 #04:同一 token 打 GET /users(列表)被擋 401
→ 產品自己認定此 token 無權讀他人帳號,卻在 by-id 路徑放行,前後不一。
注意 basis 那三段都在做同一件事:排除別的解釋。 第二段排掉「那個端點本來就是公開的」,第三段排掉「這個 token 本來就有權限」。剩下的只有越權這一種讀法。
判定的結果有三種,第三種最常被跳過:
| verdict | 意思 | 必須寫什麼 |
|---|---|---|
bug |
命中某條 oracle | oracle_used 與 basis |
needs-spec |
判不動,因為沒有判準 | 缺哪一份規格、該問誰 |
inconclusive |
判不動,因為證據不夠 | 還缺什麼證據 |
needs-spec 跟 inconclusive 常被混在一起,但它們缺的東西不一樣:前者缺的是別人的決定,後者缺的是你自己還沒收集的資料。postcode 那一筆一開始屬於後者,所以補一輪端點證據就解決了;如果它屬於前者,跑一百輪也沒用。
inconclusive 有一個很典型的樣子。端點那一輪的 F-005 是「越權寫入提權」:讀的部分已經證實有洞,寫的部分照理說也要驗一次,但 charter 的 out_of_bounds 寫死了不准做破壞性的安全測試。
所以它停在那裡,標 inconclusive,記著等人授權。這不是能力不足,是設計。
前面講的都是自己說自己對。有沒有辦法拿外部的答案驗一次?
AcademyBugs 這個練習站給了難得的機會:站方自己植入了 25 支 bug,而且附一套確認機制 —— 你點下畫面上那個元素,它跳出兩題問答(issue type、expected result),答對才放行,然後顯示官方的 Bug #N。
2026-08-01 那一輪跑完的結果寫在 site-oracle-verdicts.yaml:
result: 25/25 confirmed
final_message: "Bug #25: Congratulations! You found all 25 bugs, nice work!"
quiz_retries: 3 # bug 15 型別先答 Visual、bug 18/20 先答 Functional,各重答一次才過
25 支全中,而且這是外部 ground truth,不是自我宣告。開跑前先把 localStorage['found-bugs'] 清成 {},所以那 25 筆確定是本輪打中的。
那 3 次重答也值得記一筆:型別答錯不影響「有沒有找到」,但它說明判定分兩層 —— 找到那個現象是一回事,說得出它屬於哪一類又是一回事。
多出來的 12 筆沒有官方答案。這一段要誠實處理,因為它才是真實情況:大多數產品沒有站方幫你確認。
那 12 筆分成兩種。
站得住的那種。 F-011 是熱門商品區塊:圖片、標題、連結各自指向三個不同的商品。這一筆不需要官方名單,內部一致性硬接得住:一個商品卡片上的三個元素指向三個東西,產品自己矛盾。
只是我方主觀的那種。 F-023 是「運費越快應該越貴」。這是常識,不是規格。站方沒說過運費怎麼算,所以它只能標 needs-spec,不能算 bug。
分野就在這裡:官方清單沒有的不等於誤報,但也不等於 bug,差別在有沒有 oracle 接得住。
一、4xx 本身不是 bug。 打錯參數換來 400,那是產品做對了。要判的是「該擋沒擋」與「該過卻擋」,不是看到紅字就記一筆。
二、429 先當環境訊號。 它通常代表你打太快,不是產品壞了。先降速重試,別急著寫成缺陷。
三、契約來源填「無」的時候,schema oracle 直接停用。 沒有契約就沒有「多回了不該回的欄位」這種判定,因為你不知道什麼是該回的。
四、用了「使用者期待」這條,要在 basis 裡明講。 寫成「這是常識判斷、非規格」。因為它是六條裡最弱的一條,讀的人有權知道你的判定站在什麼上面。
test-oracle 是 model-invoked 的 skill,多數時候你不用叫它。
一、自動發生。 explore 或 bug-hunter 走到需要判定的那一步,會自己交過來。
二、單獨重判某一筆。 補到新證據的時候用得上,postcode 那筆就是這樣升級的:
/sdet-skills:test-oracle 重判 output/sessions/2026-08-08_toolshop-checkout-payment-r2/findings/F-003-postcode-prefilled-missing-value.yaml
三、直接描述一個情況加證據。
GET /users/1 顧客的 token 回 200 帶 admin 個資,證據在
output/evidence/20260808-toolshop-api-oracles/,這算 bug 嗎
沒給證據,或者證據不足以判定的時候,它應該回 inconclusive,不該硬判。
但真正的旋鈕不在這三種用法,在 charter 的 oracles 欄位。
postcode 那筆第一次標 anomaly,根本原因是 charter 的四條 oracle 只管金額與 console:
oracles:
- "金額內部一致:單價 × 數量 = 小計⋯"
- "付款失敗時要有明確錯誤訊息,而且不得建立訂單"
- "UI 顯示的結果要與 API 狀態碼一致"
- "正常操作不應出現 console error"
四條裡沒有一條管得到「表單預填值該不該是內部字串」。想讓它敢判,判準要先寫進去。這也回到 Day 15 那句話:charter 是活的,判不動的東西會告訴你下一版該補哪一條。
判定的問題不是「這對不對」,是「你憑什麼說它錯」;六條 oracle 照「要不要外部資訊」排序,前三條最強因為不必等別人回答,端點側另有七條,最強的狀態碼語意跟內部一致性同源。判不動有兩條退路:缺別人的決定寫 needs-spec,缺自己的證據寫 inconclusive。而 charter 的 oracles 欄位才是總開關 —— 沒寫進去的判準,它一條都不敢用。
一個現象(Day 17 挑出來的)
│
▼
有沒有 oracle 接得住?
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
不需外部資訊 需要外部資訊 接不住
①內部一致性 ④規格(knowledge/) │
②API 對 UI ⑤使用者期待 │
③無 console error ⑥對照品 │
(端點側另加 (端點側另加 │
狀態碼語意) 契約 schema) │
│ │ │
▼ ▼ ▼
verdict: bug 判得動就 bug needs-spec
oracle_used 判不動往右走 (缺別人的決定)
basis = 排除了什麼 或
│ inconclusive
│ (缺你的證據)
│ │
│ ▼
│ 補一筆證據再判一次
│ postcode:anomaly → bug
│ (多了 GET /users/me 回 null)
▼ │
往下走:Day 19 的自主探索 ◄───────────────────────────┘
│
▼
判不動的那些,回頭改 charter 的 oracles
找不到判準只能說「怪」,不能說「錯」,而說「錯」說得太快,代價是下一次沒人看你的單。判不動的時候缺的通常是一筆證據不是判斷力,postcode 那筆補上 GET /users/me 回 null,判準就從最弱那條換成最強那條。而 charter 的 oracles 沒寫進去的,它一條都不敢用,所以判不動的東西會直接告訴你下一版 charter 該補什麼。
判成 bug 只是它自己說的。
明天留一個問題給這位同事:同一個 agent 又找又判,它有沒有可能只是在替自己的發現背書?
第三週的驗收是一次完整的自主探索 —— 只給 charter,中間不插手,然後看它交回來的東西。那一輪要看的就是找、判、留證三件事湊在一起,還站不站得住。
skills/explore/test-oracle/ - 六條與七條的真檔docs/explore/test-oracle.md - 設計理念output/sessions/2026-08-08_toolshop-checkout-payment-r2/findings/F-003-postcode-prefilled-missing-value.yaml - 含 verdict_history
output/sessions/2026-08-08_toolshop-api-oracles/findings/ - 端點側五筆output/sessions/2026-08-01_academybugs/site-oracle-verdicts.yaml - 25/25 逐筆對照