Day 25 最後留了一個小洞,我寫的是:
setup 不完整時,
generateSpecFromState()應該拒絕產生 spec,stage 不應前進,generatedSpec 不應被填入。
前幾天我一直在補「不能產生 Project Specification」這件事。
Day 23 補的是 browser test:Specification Setup 還沒完成時,產生 Project Specification 按鈕必須保持 disabled。
Day 24 補的是另一條 browser flow:前面的需求被修改後,已填好的 setup 資料會變成需要重新檢查,按鈕要重新鎖住。
Day 25 再往下一層,補 isSpecificationSetupComplete() 的 focused unit coverage。
今天 Day 26,補的是再下一個問題:
如果有人繞過畫面,直接呼叫
generateSpecFromState()呢?
ClarifyBuild 是我這系列正在做的小工具:它會把一個模糊的產品 idea,透過幾站釐清流程,整理成 Project Specification,最後再組成 AI Coding Prompt。
今天新增的是一條 unit test。
結果是:npm test 32 條全過,31 條舊的,加今天這 1 條。
產品程式沒有改。
Day 26 把第二道 guard 的測試補上了。
畫面上的 產生 Project Specification 按鈕是第一道門。
它用 isSpecificationSetupComplete() 決定要不要 disabled。
Day 23 的 browser test 守的就是這道門。
可是 disabled 只擋得住畫面上的點擊,擋不住有人直接呼叫 function。
把 Spec 寫進 state、並把 stage 推進到預覽的,是 generateSpecFromState(state)。
它裡面本來就有第二道 guard:
if (!isSpecificationSetupComplete(state.draft)) {
return {
ok: false,
message: "請先完成主要流程、Must Have 功能規格、專案限制與技術設定。"
};
}
如果 setup 不完整,它應該直接拒絕。
不能把 generatedSpec 改成新內容。
不能前進 stage。
Day 25 結束時,這道 guard 本來就在。
只是還沒有測試守它。
把它拿掉,當時 31 條測試一條都不會紅。
原因在於:
generateSpecFromState() 的測試,傳進去的 state 都是完整的,期待的也都是 ok: true。所以當時的測試守的是正常路徑:
該產生的時候,產生得出來。
但還沒守到:
不該產生的時候,會不會真的停下來。
這就是 Day 26 要補的小洞。
今天新增的測試放在 test/appFlow.test.js。
因為 generateSpecFromState() 本來就是 app flow 的 action。
測試名稱是:
spec generation rejects incomplete setup without overwriting existing output
它的起點是一份完整 state:
const state = completeMvpState();
然後先塞一份既有 output:
state.generatedSpec = "# existing generated spec";
接著只破壞一個 setup 條件:
state.draft.targetProjectUserFlow.steps = [];
主要流程沒有步驟時,Specification Setup 不完整。
最後呼叫:
const result = generateSpecFromState(state);
然後確認三件事。
第一,回傳值剛好是「失敗」加上產品既有的錯誤訊息:
assert.deepEqual(result, {
ok: false,
message: "請先完成主要流程、Must Have 功能規格、專案限制與技術設定。"
});
訊息是從 appFlow.js 抄過來的,沒有另寫新文案。
因為是整個物件比對,之後不管是改文案,還是回傳值多長出欄位,這條都會變紅。
第二,stage 沒有前進:
assert.equal(state.stage, BUILDER_STAGE.SPECIFICATION_SETUP);
第三,既有的 generated spec 沒有被動到:
assert.equal(state.generatedSpec, "# existing generated spec");
先放一份舊 spec 是故意的。
如果 guard 被拿掉,最先失敗的是第一條:函式會回傳 ok: true。
後面兩條抓的是另一種壞法:回傳還是失敗,但函式已經先動了 state。
舊 spec 被覆寫、被清空,或 stage 先往前推,這兩條都看得出來。
測試本身很短。
但它釘住的是另一種責任:
generateSpecFromState()這個 app flow action 自己也要保護自己的前置條件。
Day 25 已經補了 isSpecificationSetupComplete()。
那為什麼 Day 26 還要補 generateSpecFromState()?
因為兩者守的責任不一樣。
Day 25 問的是:
setup 到底算不算完整?
Day 26 問的是:
如果 setup 不完整,產生 Spec 的動作會不會真的停下來?
前者是 predicate。
後者是 action。
predicate 回傳 false,只代表「有人問它時,它答對了」。
但 action 有沒有真的使用這個 predicate,是另一件事。
Day 26 的測試確認的是:app flow 這一層有照 isSpecificationSetupComplete() 的答案停下來。
這條只用「主要流程沒有步驟」當代表。
其他條件會不會讓判斷失敗,是 Day 25 的事。
如果有一天有人重構 generateSpecFromState(),不小心移除了上面那道 guard,Day 25 的測試仍然會過。
但 Day 26 的測試會失敗。
這就是它存在的價值。
Day 23 的 browser test 已經確認按鈕 disabled。
那為什麼還需要 unit test?
因為 button disabled 守的是「正常 UI 操作」。
但 generateSpecFromState() 守的是「真正執行那一步時的前置條件」。
Browser test 能回答:
使用者在畫面上不能按。
Unit test 能回答:
就算 function 被呼叫,也不能產生。
這兩件事看起來很像,但其實是不同位置的責任。
如果只守 UI,底層可能變脆。
如果只守底層,使用者可能看到錯誤的互動狀態。
這四天守的,是同一個判斷,和用到它的兩個地方:
| 位置 | 在哪裡 | 負責什麼 | 守住它的測試 |
|---|---|---|---|
| 判斷 | isSpecificationSetupComplete() |
定義 setup 算不算完整 | Day 25:unit test,17 個條件各自單獨壞一次 |
| 入口一:按鈕 | render.js 的 disabled |
不完整時,使用者按不到 | Day 23、Day 24:browser test(缺資料;資料過期) |
| 入口二:action | generateSpecFromState() |
不完整時拒絕,stage 不前進,既有 Spec 不被覆寫 | Day 26:unit test |
按鈕和 action 用的是同一個判斷。
Day 25 守判斷本身。
Day 23、Day 24 守按鈕有沒有照判斷鎖住。
Day 26 守 action 有沒有照判斷拒絕。
所以 Day 23 到 Day 26 守的是同一個產品承諾,只是分給三個地方:
不完整或過期的 Specification Setup,不能產生 Project Specification。
Day 24、Day 25 提過的那個差異還沒處理:
目前程式的 needs-review 規則,和 Day 11 Project_Spec_Schema_v0.1.md〈十四、失效與 Needs Review 規則〉不完全一致。
例如 Target User、Problem、Goal 只要有變,已填過的 tech setting 在程式裡會被標成需要重新檢查,規則表對 Tech Stack 寫的卻是「不影響」。
今天不處理。
這條測試直接把主要流程的步驟清空,不經過「哪些變更會觸發 needsReview」的規則。
所以之後不管要不要對齊 Schema,它都不用動。
先跑 focused test:
node --test test/appFlow.test.js
tests 6
pass 6
fail 0
再跑完整測試:
npm test
tests 32
pass 32
fail 0
和 Day 25 一樣,我的執行環境第一次跑完整測試時,browser tests 會因為 local server 不能 bind 到 127.0.0.1 失敗。
放寬本機執行權限後,同一組測試通過。
Day 25 是 31 條。
今天多了 1 條,變成 32 條。
Day 23 我說過,Setup Status 和按鈕 disabled 是兩套判斷,兩邊要一致。
那條 browser test 走的是正常表單走得到的狀態,兩邊確實一致。
還有一類 draft,正常表單做不出來,因為儲存的時候就會被擋下來。
但它可以從 localStorage 還原,或用匯入檔案帶進來。
例如 specified 的技術設定,沒有任何 items:
isSpecificationSetupComplete() 回傳 false,按鈕 disabledneedsReview,不看 items,所以顯示「完成」功能規格的 userValue 或 actor 是空白,或 behaviors 是空的,也會出現同樣的落差。
所以 Day 27 先要決定的是產品行為:
這種 draft,要讓 Setup Status 照
isSpecificationSetupComplete()的規則顯示,還是在還原、匯入的時候就擋下來?
決定之後,再補測試守住。
這一天可能會動到產品程式,不會是純測試。
Day 23 到 Day 26 把「不能產生」守住了。
Day 27 要看的,是不能產生的時候,畫面說的話準不準。
Day 26 完成。
明天見。