昨天講完稽核軌跡,這個系統該有的功能就齊了:登入、開會、寫記錄、送出版本、確認、生效、指派決議項。
然後問題就來了——全綠代表什麼?
前面每一天,測試都是跟著功能一起長出來的:寫一個功能、配一組測試、綠了就交。那些測試驗的是我當初寫進規格的東西。所以「全綠」嚴格來說只證明了一件事:它做出來的東西,跟我寫下來的東西一致。
至於我寫下來的東西對不對、有沒有漏,測試不會知道。
今天這篇是整理我怎麼切這條線:哪些測試交給 Claude、哪些得自己來、各自怎麼做。
我試過幾種切法。「簡單的給 AI、複雜的自己來」不對——最複雜的並行測試它寫得比我好。「自動化的給 AI、手動的自己來」也不對,那只是在描述工具,不是在分工作。
現在用的判準只有一句話:
判準已經寫在規格或契約裡的,交給 Claude。判準還在我腦子裡的,只能自己測。
這條線之所以有用,是因為它同時給出了「自己測完要做什麼」的答案——把腦子裡的那條判準寫下來,它就變成下一類了。 人測的東西應該越測越少,如果每次都在測同一件事,代表沒有回收。
下面分兩邊講。
每個功能自己那一段,這是前面十幾天的常態,不多說。唯一要注意的是它的盲區——等一下第二部分會講。
這個系統有 73 條後端測試,但沒有一條走過使用者真的會走的路。原因是每個測試檔都從半路開始:前置資料是 fixture 直接塞進資料庫的,身分是直接簽一張 token 出來的。
def bearer(user: User) -> dict[str, str]:
"""A real access token for this user."""
return {"Authorization": f"Bearer {security.issue_access_token(user.id)}"}
# 既有的 45 條測試都透過這個拿身分。換認證的時候改的是這裡,不是那 45 條——
# 它們驗的是業務規則,不是身分機制怎麼傳。
auth = bearer
這樣切是對的。Day 21 換認證的時候,正因為身分收斂在這一個函式,45 條測試一條都沒改。但代價是「怎麼走到那裡」永遠沒有主人。
所以我另外開了一條,給 Claude 的指令是三條限制:
db.add 都不准有。note.id,要用上一個回應 JSON 裡的 id。加上一條關於範圍的:只寫一條。它不涵蓋分支,涵蓋分支是那 73 條的工作。它只回答「接不接得起來」。
第一次跑就紅了,紅在開草稿那一步:
AssertionError: {"detail":{"code":"VALIDATION_ERROR","message":"body: Field required"}}
DraftCreate 兩個欄位都是 optional,但端點簽名是 payload: DraftCreate,沒有預設值——FastAPI 因此要求 request body 本身必須存在。契約沒寫這件事,而前端剛好送了 {},所以從來沒撞到。
有一類問題的答案不在任何一條測試裡,而在「把全部檔案排開來看」。這種工作 Claude 做得比人快太多,而且不會看漏。
我請它列出所有端點,標出沒有宣告 CurrentUser 的:
⚠️ GET /api/users
⚠️ GET /api/meetings/{meeting_id}
⚠️ GET /api/notes/{note_id}
⚠️ GET /api/notes/{note_id}/versions
⚠️ GET /api/notes/{note_id}/draft
⚠️ GET /api/notes/{note_id}/action-items
⚠️ GET /api/versions/{version_id}
⚠️ POST /api/meetings
⚠️ POST /api/auth/login
最後一條是應該的——契約寫得很清楚,login 是「全系統唯一不需要 token 的端點」。其他八條不是。
這一類的訣竅是:要它產出清單,不要它產出結論。 「幫我檢查有沒有安全問題」會得到一篇作文;「列出所有端點與它們宣告的 dependency」會得到一張可以自己看的表。
前端有六個 zod schema,是契約在前端那一側的副本。前端測試全部走 MSW mock——但那些 mock 是照著 schema 寫的,所以測的是「我寫的假資料符合我寫的 schema」,永遠會過。
做法是讓後端的一條龍測試把真實回應存成 JSON,前端拿去餵自己的 schema:
Test Files 1 passed (1)
Tests 6 passed (6)
六個全過,契約兩側是對齊的。重點不是結果,是這件事之前沒有人做過,而它只花了 30 行。
這是最重要的一種,因為它的失敗方式最隱蔽。
我把端點掃描的結果實際打了一次,確認不是靜態分析誤判:
🔓 放行 GET /api/users
🔓 放行 GET /api/notes/{id}
🔓 放行 GET /api/notes/{id}/versions
🔓 放行 GET /api/versions/{id}
🔓 放行 GET /api/notes/{id}/action-items
🔒 MISSING_TOKEN GET /api/users/me
🔓 放行 POST /api/meetings
GET /api/versions/{id} 回的是版本全文、必要確認人名單、完整的決策時間軸。這整個系統存在的理由就是那份東西,它不需要任何憑證就讀得到。最後一條還是寫入——不帶 token 可以開一場會。
為什麼 73 條測試沒有一條紅?我的契約裡,這些端點的權限欄寫的是**「任何人」**。我的意思是「任何登入的使用者」。實作出來是「任何人」。
而測試也是照同一份契約寫的。
Claude 讀規格寫實作,也讀同一份規格寫測試。規格有兩種讀法的時候,它兩邊會用同一種讀法。 一致不等於正確——它只是把同一個誤解執行了兩次。
這就是為什麼「叫 AI 自己寫測試驗自己的實作」有天花板。不是它不夠仔細,是它兩次的輸入是同一份文件。要戳破它,只能有人拿著不在那份文件裡的東西去打——例如「如果完全不帶 token 會怎樣」。契約沒有回答這個問題,所以兩邊都沒有處理它。
跑完整套測試,最後一行是:
74 passed, 455 warnings in 8.82s
455 個。往上翻:
InsecureKeyLengthWarning: The HMAC key is 21 bytes long, which is below the
minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2.
簽章金鑰只有 21 bytes。原因是 .env 沒設 JWT_SECRET,吃到程式裡的預設值 "dev-only-not-a-secret"——那個值刻意寫成一看就知道是假的,但沒人注意到它連長度都不夠。這個 warning 從認證做完那天就在印了。
沒有任何一份文件說過「warning 數要是零」。所以它不屬於任何人的驗收條件。綠燈旁邊的數字是不會有人讀的——它不是紅的,所以它不在任何人的路徑上。
同一類的還有:測試跑多久(從 15 秒變 94 秒那次是 argon2)、log 裡有沒有莫名其妙的東西、畫面卡不卡。這些都要人自己去看一眼。
最後一種其實最難寫成步驟,但不能不做:打開來用一次,問自己「這個東西做出來,是不是我當初想要的」。不是「符不符合規格」——那個 Claude 已經驗過了,而且驗得比我仔細。是規格本身對不對。
明天:把今天這幾個洞交給 AI 去 review,然後驗證 review 本身。今天的發現是靠「換一個視角從外面打」找到的——而 AI review 最常見的失敗,剛好也是它只看得到你指給它看的那一面。