test-driven-development (obra/superpowers)
以測試先行方式實作功能與修正缺陷,先寫會失敗的測試,再寫實作程式碼。
superpowers test-driven-development:讓代理先寫測試,代價是 Token 用量約 1.6 倍
Codex CLI · gpt-6-astra
24.0 / 30
相對未使用技能的提升:+1.00 · 推薦 · 18 次執行
推薦:總分 24.0/30 至少 22,產出品質 5.0 至少 3.5,提升幅度 +1.00 至少 +1.0。
SOTA
透過使用 Codex 測試。
涵蓋 Codex, Claude Code
Codex CLI 0.166(SOTA 等級):18 次評分執行。依我們的等級規則,在這裡通過也涵蓋 Claude Code;但這不代表第二等級的結果。
分數
每個維度為 0 到 5 分,六個維度相加即為滿分 30 的總分。每個維度都是 5 分最好。成本 5 分代表便宜又快速;安裝 5 分代表容易安裝。
- 觸發可靠性
- 5.0
- 相對未使用技能的提升
- 3.0
- 產出品質
- 5.0
- Token 與時間成本
- 2.0
- 安裝與可攜性
- 4.0
- 安全性
- 5.0
安全審查
安全靜態掃描:0 CRITICAL / 0 HIGH / 0 MEDIUM。人工審查:可在無網路沙箱中安全執行(未發現問題)。掃描結果乾淨,不代表技能一定安全。
安裝
npx skills@latest add obra/superpowers --skill test-driven-development
固定 Commit: 2ce8aa15d05bd57213e6f746f953aafb7ded7a8b
測試日期: 2026-11-02
我們測了什麼
- 依精確規格實作函式 — 在 slug.py 中實作 slugify(text, max_length=None)(Python 3,僅使用標準函式庫)。規格:將文字轉為小寫;把每一段非 ASCII 字母或數字組成的字元替換成單一個 '-';去除開頭與結尾的 '-';結果為空字串時變成 'n…
- 實作小型有狀態類別 — 撰寫 ratelimiter.py(Python 3,僅使用標準函式庫),包含 class RateLimiter(limit, window_seconds, clock=time.monotonic):依 key 分別計算的滑動視窗限流器。allow(key) 在最近…的呼叫次數少於 limit 時,會傳回 True 並記錄該次呼叫。
- 修正捨入誤差 — 客戶反映 cart.py 的 cart_total 有時與我們的會計系統差一分錢;會計系統在每個步驟(每行小計、折扣後小計、稅額)都以四捨五入到分。例如 cart_total([(1.005, 1)]) 應傳回 1.09,而…
優點
- 相對未使用技能,平均分數從 4.0 提升到 5.0(+1.00)。
- 技能即使未被點名也會被採用:6/6 次實驗組執行都讀取了技能檔案。
- 比只用一句話寫出同一條規則的版本高 +0.33。
- 使用技能後的絕對品質很高:5.0 / 5。
缺點
- Token 用量是未使用技能的 1.65 倍。
- 測試注意事項:一句話提示組(6 次執行)是後來在另一台機器上追加的,與對照組、實驗組不同機器。
- 每個任務的每個組別 n=2;約 1 分以內的差異屬於雜訊。
評測
在 Mac 測試機上使用 Codex CLI 0.166(gpt-6-astra,低推理),每次執行都使用全新的暫存專案與暫存的 HOME/TMPDIR/快取;技能已安裝(實驗組)、未安裝(對照組),或未安裝但把技能的核心要求濃縮成一句話附在提示詞後(一句話提示組)。每個任務的每個組別執行 2 次,關閉網路,封鎖 /tmp 寫入。提示詞從未提及技能。評分以程式化方式進行(隱藏測試/植入問題召回率/限制條件檢查/相關時以渲染 + OCR 檢查),每次執行 0–5 分。沒有任何分數是由 LLM 評審決定的。
三組對照
| 組別 | 平均分數(0-5) | 每次執行 Token 用量 |
|---|---|---|
| 未使用技能 | 4.0 | 65k |
| 一句話提示 | 4.67 | 127k |
| 使用技能 | 5.0 | 108k |
以 Codex CLI 執行 18 次,採規則式檢查評分,不經 LLM 評審。總分 24.0 / 30,評級為 推薦。
揭露事項:一句話提示組(6 次執行)是後來才加入的,使用的機器與對照組、實驗組不同。
評定:推薦(24.0 / 30)。 來源:obra/superpowers,skills/test-driven-development,固定於 commit 2ce8aa1。安裝:npx skills@latest add obra/superpowers --skill test-driven-development。授權: MIT(儲存庫 LICENSE)。
這是什麼
obra/superpowers 的 test-driven-development 技能。它的描述寫著「在實作任何功能或修正 bug、尚未開始寫實作程式碼時使用」。約 1,500 字的 Markdown(紅燈-綠燈-重構的「鐵律」、一張跳過測試的藉口表、一份驗證清單),外加一份談如何寫好測試的檔案。沒有腳本。
我們如何測試
在 Mac 測試機上使用 Codex CLI 0.166(gpt-6-astra、低推理),每次執行都使用全新的臨時專案與臨時 HOME/TMPDIR/快取,安裝技能(使用技能組)、不安裝(對照組),或是不安裝但把技能核心要求濃縮成一句話附加在提示詞後(一句話組);每個任務的每組各執行 2 次,關閉網路、封鎖 /tmp 寫入。提示詞完全沒有提到技能名稱。
- 依精確規格實作函式 — 在 slug.py 中實作
slugify(text, max_length=None)(Python 3,只用標準函式庫)。規格:把文字轉小寫;把每一段不是 ASCII 字母或數字的字元換成單一 '-';去除開頭與結尾的 '-';結果為空時變成 'n… - 實作小型有狀態類別 — 撰寫 ratelimiter.py(Python 3,只用標準函式庫),包含類別
RateLimiter(limit, window_seconds, clock=time.monotonic):每個 key 一個滑動視窗限流器。若最近…內記錄的呼叫少於limit次,allow(key)會回傳 True 並記錄這次呼叫… - 修正四捨五入 bug — 客戶說 cart.py 中的
cart_total有時與我們的會計系統差一分錢,會計系統在每一步都採四捨五入到分(每個品項小計、折扣後小計與稅金)。例如 cart_total([(1.005, 1)]) 應該回傳 1.09,而…
評分採程式化方式(隱藏測試/植入問題召回/限制條件檢查/視情況搭配渲染+OCR),每次執行 0–5 分。所有分數都不是由 LLM 評審決定。
結果
給第三組的一句話提示:"遵循嚴格的紅燈-綠燈-重構:先寫一個失敗的測試,看著它因正確的原因失敗,再寫最小實作讓它通過,然後重構;沒有失敗的測試就不寫正式程式碼。"
| 任務 | 對照組分數 | 一句話提示 | 使用技能 | 對照組 token | 一句話組 token | 使用技能 token | 技能讀取 |
|---|---|---|---|---|---|---|---|
| t1-slugify | 4.0 | 5.0 | 5.0 | 62k | 134k | 108k | 2/2 |
| t2-ratelimiter | 4.0 | 5.0 | 5.0 | 63k | 147k | 97k | 2/2 |
| t3-cart-bugfix | 4.0 | 4.0 | 5.0 | 69k | 97k | 116k | 2/2 |
平均分數:未使用技能 4.0,使用技能 5.0(差異 +1.0)。每次執行 token:65k 對 108k(1.65x);時間 55 秒對 51 秒。一句話組:4.67(技能相對一句話提示的增益:+0.33;一句話提示相對未使用技能的增益:+0.67);一句話組每次執行 127k token,因此技能成本是一句話提示的 0.85 倍。
實際情況
- 正確性完全相同:兩組共 12 次執行的隱藏測試都通過(12/12、10/10 與 11/11 項測試)。在這些小任務上,技能不會改變程式碼是否能運作。
- 它改變的是工作順序。6 次使用技能組執行中,測試檔都在實作之前的步驟寫好;6 次對照組執行中,agent 則把測試與實作寫在同一個修補裡(或者在購物車任務中先修 bug、之後才補測試)。在我們逐行讀過的那次使用技能組執行(slugify)中,agent 先寫測試、跑一次看它失敗,然後才寫程式碼。
- 它光靠描述就能觸發:技能檔在 6/6 次使用技能組執行中都被讀取,儘管沒有任何提示提到 TDD。
- 成本更高:每次執行約 1.65 倍 token(107k 對 65k),實際耗時相近(51 秒對 55 秒),因為 agent 要讀技能與其同伴檔案,並把迴路跑好幾次。
- 一句話版本(嚴格紅燈-綠燈-重構、沒有失敗測試就不寫正式程式碼):平均 4.67,介於對照組(4.0)與技能(5.0)之間,每次執行 127k token,對照組 65k(2.0x),技能 108k(一句話組是技能組的 1.2 倍)。
- 測試先於實作的次數:對照組 0/6、一句話組 4/6、技能組 6/6;一句話組缺少的兩次都在購物車 bug 修正任務。技能是唯一每次都做到的組別,而且用的指令數(每次 10 個)比一句話組(19 個)少。
面向分數
| 面向 | 分數 | 原因 |
|---|---|---|
| 1_output_quality | 5.0 | 使用技能時平均 5.00/5(程式化檢查,見各任務表)。 |
| 2_lift_vs_no_skill | 3.0 | 相對未使用技能的 4.00 提升 +1.00 分(每個任務 n=2;約 1 分以下屬雜訊)。 |
| 3_trigger | 5.0 | 使用技能組 6/6 次執行都讀取技能檔案。 |
| 4_instructions_portability | 4 | |
| 5_efficiency | 2 | token 1.65x(每次執行 108k 對 65k)、時間 0.93x(51 秒對 55 秒)。 |
| 6_safety_hygiene | 5 |
這項測試的限制
- 兩組的隱藏測試全都通過(天花板),因此測到的提升是流程檢查(測試先於實作),由事件記錄計算而來,而不是成果差異。
- 任務都是小型單檔 Python 練習;較大的儲存庫、其他語言與測試品質(例如突變測試)都沒有測試。
- 每組每任務 n = 2:約 1 分以下的差異屬雜訊。
- 規則式評分只能衡量檢查項目看得到的部分,無法判斷品味,也無法判斷結果是否為最佳。
安全性
請見 security.md。掃描:CRITICAL 0 / HIGH 0 / MEDIUM 0。評定:可安全執行(無網路沙盒)。掃描乾淨並不代表技能安全。
總結
它說到做到:它是唯一在每次執行都先寫失敗測試的組別(6/6,對照組 0/6,一句話版本 4/6),token 是對照組的 1.7 倍、一句話版本的 0.85 倍。在這些小任務上,各組的最終正確性相同,因此增益在於流程,不在程式碼。
完整紀錄
執行日誌:test-driven-development (Codex CLI, network off, per-run temp HOME/TMPDIR, one row per graded run.) 欄位:任務、組-輪次、得分(0-5)、token、秒、通過的檢查、代理開啟的 skill 檔案。 t1-slugify control-1: 得分 4.0, token 62017, 秒 55.5, 檢查 2/3, 未開啟 t1-slugify control-2: 得分 4.0, token 62139, 秒 53.8, 檢查 2/3, 未開啟 t1-slugify oneliner-1: 得分 5.0, token 135535, 秒 52.9, 檢查 3/3, 未開啟 t1-slugify oneliner-2: 得分 5.0, token 134448, 秒 46.7, 檢查 3/3, 未開啟 t1-slugify treatment-1: 得分 5.0, token 118034, 秒 56.3, 檢查 3/3, 開啟 test-driven-development t1-slugify treatment-2: 得分 5.0, token 97974, 秒 48.3, 檢查 3/3, 開啟 test-driven-development t2-ratelimiter control-1: 得分 4.0, token 63363, 秒 46.1, 檢查 2/3, 未開啟 t2-ratelimiter control-2: 得分 4.0, token 63431, 秒 98.9, 檢查 2/3, 未開啟 t2-ratelimiter oneliner-1: 得分 5.0, token 140042, 秒 60.8, 檢查 3/3, 未開啟
18 次執行、3 個產出檔案
範例產出
SOTA · 装了 skill:test_slug.py(t1-slugify,第 1 次,Codex)
import unittest from slug import slugify class SlugifyTests(unittest.TestCase): def test_normalizes_text(self): cases = [ ('Hello, World!', 'hello-world'), (' --ABC__123\t\nxyz!! ', 'abc-123-xyz'), ('café déjà vu 12٣', 'caf-d-j-vu'), ('İ X', 'i-x'), ('already-a-slug', 'already-a-slug'), ] for text, expected in cases: with self.subTest(text=text): self.assertEqual(slugify(text), expected) def test_empty_result_uses_fallback(self): for text in ('', '--- !\t', '中文', '😀'): with self.subTest(text=text): self.assertEqual(slugify(text), 'n-a') def test_length_limits_respect_word_boundaries(self): cases = [ ('Hello, World!', 8, 'hello'), ('Hello, World!', 11, 'hello-world'), ('Hello, World!', 20, 'hello-world'), ('Hello, World!', 5, 'hello'), ('Hello, World!', 6, 'hello'), ('one two three', 7, 'one-two'), ('one two three', 9, 'one-two'), ('Supercalifragilistic', 5, 'super'), ('Supercalifragilistic next', 5, 'super'), ('abc', 1, 'a'), ('abc', 0, ''), ('!!!', 1, 'n'), ('!!!', 2, 'n'), ('!!!', 3, 'n-a'), ] for text, limit, expected in cases: with self.subTest(text=text, limit=limit): self.assertEqual(slugify(text, limit), expected) def test_explicit_none_leaves_length_unlimited(self): self.assertEqual(slugify('Hello, World!', None), 'hello-world') if __name__ == '__main__': unittest.main()