iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0

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 的測試補上了。


disabled 只擋得住畫面

畫面上的 產生 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

今天新增的測試放在 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 守判斷,Day 26 守動作

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 的測試會失敗。

這就是它存在的價值。


Browser test 守結果,這條守前置條件

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。


needs-review 的差異還開著

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 27 可以接哪裡

Day 23 我說過,Setup Status 和按鈕 disabled 是兩套判斷,兩邊要一致。

那條 browser test 走的是正常表單走得到的狀態,兩邊確實一致。

還有一類 draft,正常表單做不出來,因為儲存的時候就會被擋下來。

但它可以從 localStorage 還原,或用匯入檔案帶進來。

例如 specified 的技術設定,沒有任何 items:

  • isSpecificationSetupComplete() 回傳 false,按鈕 disabled
  • Setup Status 的「技術設定」看有沒有選 mode 和 needsReview,不看 items,所以顯示「完成」

功能規格的 userValue 或 actor 是空白,或 behaviors 是空的,也會出現同樣的落差。

所以 Day 27 先要決定的是產品行為:

這種 draft,要讓 Setup Status 照 isSpecificationSetupComplete() 的規則顯示,還是在還原、匯入的時候就擋下來?

決定之後,再補測試守住。

這一天可能會動到產品程式,不會是純測試。

Day 23 到 Day 26 把「不能產生」守住了。

Day 27 要看的,是不能產生的時候,畫面說的話準不準。

Day 26 完成。

明天見。


上一篇
Day 25|按鈕該鎖住的時候,是哪個條件在擋?
下一篇
Day 27|Draft 載得進來,不代表它算完成
系列文
AI 寫不好,可能是我沒說清楚:30 天打造 ClarifyBuild,讓 Vibe Coding 從需求開始 共 27 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言