在前幾天,我們都是告訴新同事我們需要什麼,而新同事幫我們去做這些工作。但今天呢,我們希望我們的同事可以更獨立一點,所以我們將不會告訴他要做哪些事情了。
從今天開始,新同事自己決定目標、自己決定界線,並且要如何探索。我們的角色將會從原本決策者變成一個 Mentor,而新同事則是除了執行者也是決策者。
而今天要教會他的東西就是,撰寫一份 Exploration Charter。
Test case 有個很難承認的限制:它只驗得到你已經想到的那條路。 你寫「購買兩件商品,把數量改成零,檢查總金額不是負數」,它就永遠只會做這件事。負數這個點子是你想到的,那 charter 產生的價值就等於你的想像力,一個字都不會多。
這件事在人身上還算划算,因為人邊走邊想。但交給 agent 之後就不划算了:你多寫一步,它就多走一步,你沒寫的它一步都不會走。把每一步都寫死,等於花錢請它幫你按滑鼠。
要讓它走出你沒想過的路,你能給的就只剩三樣:要達成什麼、憑什麼算對、哪裡不准去。路怎麼走留給它。這三樣寫成一份檔案,就是 charter。
換句話說,這一天真正在做的事不是「換一種格式寫測試」,是把控制權從步驟移到判準。步驟你放手,判準你抓緊;抓不緊的時候它會走得很遠,但走去哪裡你管不著。
exploration-charter 是使用者才能觸發的 skill,agent 不會因為某些關鍵詞自己叫它。所以下指令的時候,目標和範圍要自己講清楚:
/sdet-skills:exploration-charter 探索 https://with-bugs.practicesoftwaretesting.com 的結帳流程,找付款相關的 bug
輸入就這一句。它寫出 charters/toolshop-checkout-payment.yaml:
# 探索章程(exploration charter)
# 由 exploration-charter 產生、供 explore 讀取的輸入。
# 位置慣例:charters/<slug>.yaml(committed;output/evidence/ 才是每次跑的輸出、被 gitignore)
charter:
goal: "在 with-bugs build 走完 Toolshop 的結帳流程,找付款與金額計算相關的 bug"
target: "https://with-bugs.practicesoftwaretesting.com/#/?bug-hunting=true"
scope: [商品頁, 購物車, 結帳表單, 配送資訊, 付款方式, 訂單確認頁]
oracles:
- "金額內部一致:單價 × 數量 = 小計,小計 + 運費 − 折扣 = 應付總額,購物車與結帳頁與確認頁三處數字要一致"
- "付款失敗時要有明確錯誤訊息,而且不得建立訂單;成功才有訂單編號"
- "UI 顯示的結果要與 API 狀態碼一致"
- "正常操作不應出現 console error"
out_of_bounds:
- "不得對正式站 practicesoftwaretesting.com 做任何操作,只打 with-bugs 這個 build"
- "不得修改或刪除既有他人資料,只能動自己這輪建立的購物車與訂單"
- "不得使用或嘗試取得 admin 權限"
- "不得做安全類測試(SQLi、越權列舉)—— 這輪的目標是金額與付款邏輯"
- "不得寫迴圈連打端點"
- "不得對外部金流服務送出真實請求"
test_account: customer@practicesoftwaretesting.com # 帳密走 env:TOOLSHOP_TEST_USER / env:TOOLSHOP_TEST_PASS
max_steps: 25
把輸入跟產出擺在一起比,差別在這裡:
| 欄位 | 你講了嗎 |
|---|---|
goal、target |
講了 |
scope 六個畫面 |
沒講,它從「結帳流程」展開 |
oracles 四條 |
沒講,它從「付款相關」推出判準 |
out_of_bounds 六條 |
一條都沒講 |
test_account 走環境變數 |
沒講 |
max_steps: 25 |
沒講 |
一句話進去,六條界線出來。這些不是它憑空想的。exploration-charter 這支 skill 自己規定了 out_of_bounds 的最低涵蓋範圍:不對 production 寫入、不付款、只用測試帳號;要走端點時再加一層,不打 config/product-context.md 標記「不得碰」的端點、不做資料列舉、不自動化連打。剩下那幾條是它讀了這個站的情況補的(正式站與 with-bugs 是兩個 build、這輪的目標是金額不是安全)。Day 5 寫的產品知識與 Day 6 立的權責,在這裡第一次派上用場。

七個欄位裡,你只講了兩個。
這一步的意義是:護欄不該靠你每次記得寫。你會忘記「不要打正式站」,但一份會自動補上這條的 skill 不會忘。你的工作從「想齊所有該禁止的事」變成「檢查它補的這幾條夠不夠」,後者你做得到,前者你做不到。
Exploration Charter 中文寫成探索章程(Charter),但它更像一份任務書。傳統的測試案例會告訴你每一步怎麼做、每一步該看到什麼,只檢驗那一條路線。章程不一樣,它給目標、不給路徑;對錯交給 oracle 判,路線由執行的人自己決定。
| Test Case | Exploration Charter | |
|---|---|---|
| 寫的是 | 怎麼走(步驟) | 要達成什麼(目標) |
| 路徑 | 寫死 | 留白 |
| 判對錯 | 每一步的預期結果 | oracle |
| 安全靠什麼 | 步驟本身不會越界 | out_of_bounds 護欄 |
| 能發現新東西嗎 | 只驗得到寫下的那條 | 走沒人想過的路 |
舉個例子,如果 test case 寫「購買兩件商品,並且把數量改成零,檢查總金額不是負數」,這就是一個測試案例。
但 charter 只會寫「探索購物車和結帳,最後的金額要算得正確」。要不要把數量改成零,由執行 charter 的人自己決定,或是自己想出來。
Charter 由四個東西組成:
在撰寫 charter 的時候,很容易把它寫得太細。例如在 scope 裡面描述了操作的步驟,那它就會造成這個步驟冗長,基本上來說它就變成像是一個 test case,但可能是更難維護的一份成本。
另一種是寫得太廣泛。例如目標寫了「測試購物車」,但沒有告訴它怎樣的行為是正確的(也就是沒有給 oracle),那它就會去瀏覽購物車的流程,但因為不知道什麼是正常的,可能會忽略掉某些 bug,認為系統是正常的。
兩種偏差都有實跑的數字可以看。
太窄的那一邊。 上面那張 toolshop-checkout-payment.yaml 圈了六個畫面、max_steps: 25、oracle 只管金額與 console。它第一次實跑(2026-08-08)交回 3 筆發現,全部命中既有指紋,新的是 0 筆。原因寫在那輪的 log 裡:金額類缺陷在一週前那輪已經掃過一遍,charter 把範圍圈在同一塊地。這不是它偷懶,是 charter 指定它去挖一個已經挖過的坑。
太廣的那一邊。 charters/academybugs.yaml 面對的是一個有標準答案的站:站方宣稱植入 25 支 bug。第一版 charter 的 oracle 寫得很像「常識清單」,例如「圖片與商品相符」「表單驗證要擋」。探索兩輪,獨立命中 17 支,0.68。
漏掉的 8 支不是能力問題,是判準問題。那一版 charter 的判準全是相對的:問它「這張圖跟其他張一不一樣」。18 張列表圖裡只有一張離群,抓到了;另外 14 張一致地沒填滿容器,因為彼此一致,所以一個訊號都沒有。
那 8 支後來收斂成四條根因,逐條寫回 charters/academybugs.yaml,而不是另開一份檢討文件:
| 根因 | 原本錯在哪 | 改成什麼 |
|---|---|---|
| 每條相對判準配一條絕對判準 | 問「跟其他張一不一樣」,一致地錯就沒訊號 | 量 rendered height 是否等於容器 height |
| 靜態解析只用來選目標 | 分享鈕 href 看起來正常,事件處理器攔截後才改開拼錯網域 | 每一顆都實際點過,看最終落地 URL 與狀態 |
| 通過條件寫到終態 | 「表單可載入、可送出」記成 OK,實際送出後 loader 永遠轉 | 等到成功訊息、錯誤訊息或逾時三者之一才記結果 |
| 把沒被畫面主動端出來的東西列進計畫 | minicart 收合時 DOM 裡沒有商品標題,忘記密碼整條沒走 | scope 列到 URL 層級,收合元件單獨列一條 |
同一次回填還動了兩個地方:scope 從功能區塊名稱展開成五條實際 URL(/account/?ec_page=forgot_password 那條前兩輪從沒走過,裡面藏著一支 bug),max_steps 從 40 改成 60,理由直接寫在該行後面:「40 不足以同時做完 18 商品的廣度與交叉配對的深度」。
補完之後第三輪以站方的答案校準,25 支補齊。
這件事的重點不在 25 這個數字,在於修正留在 charter 檔裡,而且指得到是哪幾筆逼出來的(每條根因都掛著 caught: [F-021, F-031] 這種欄位)。下一輪讀 charter 的人不必知道這段歷史,也會拿到已經修好的判準。
一句話發動 exploration-charter,它產出一份 YAML,你只講了目標,它補上 scope、oracle、六條界線、步數上限與憑證來源;你檢查它補的夠不夠,而不是從零想齊所有該禁止的事。跑完之後不要把 charter 當一次性的輸入丟掉:漏掉什麼、判準為什麼接不住、步數為什麼不夠,全部回填到同一份檔案,下一輪直接站在修好的版本上開始。
一句話:目標 + 網址
│
▼
/sdet-skills:exploration-charter
(讀專案授權分級與 knowledge/)
│
▼
charters/<slug>.yaml
goal|scope|oracles|out_of_bounds
+ max_steps + 憑證來源
│
▼
人審一次:夠不夠
太窄?判準相對?步數不足?
│
▼
交給 explore 跑一輪
│
▼
漏掉的收斂成根因,回填 charter
(scope 展開、判準改絕對、步數調高)
│
└──────► 下一輪讀的是修好的版本
charter 給的是目標、判準與界線,不是步驟,步驟寫死等於花錢請它按滑鼠。護欄也不該靠你每次記得寫,那六條 out_of_bounds 你一條都沒講,而它每次都會補上。而 charter 是活的,academybugs 那份從 17/25 走到 25/25,靠的不是換一支更強的 agent,是把漏掉的四個根因寫回同一份檔案。
charter 寫好了,但它現在只是一份躺在 charters/ 裡的 YAML。
明天把它交出去:explore 拿著這份章程,自己決定第一步點哪裡、下一步往哪走。這裡會冒出一個新問題,它憑什麼認為這一步比那一步值得走? 還有一個更現實的,它怎麼記得自己已經走過哪裡,不要在同一個畫面上鬼打牆。
charters/toolshop-checkout-payment.yaml - 一句話產出的那份charters/academybugs.yaml - 含 2026-08-01 回填的四條 coverage_rules 與 scope_urls
skills/explore/exploration-charter/ - 產生 charter 的 skilldocs/explore/explore.md - 覆蓋鐵則的來由