Day 17 我故意沒做一件事:匯入前確認。
那時候,匯入一份合法的 draft,會直接取代目前的 draft。
而 ClarifyBuild 每次變更都會寫進 localStorage。被取代的舊 draft,沒有版本可以回頭找。
Day 18 我也還沒補 browser test。
我先把按鈕背後的 app flow 抽出來測:
generate spec
↓
generate prompt
↓
export draft
↓
import draft
↓
regenerate spec and prompt
所以 Day 19 接著做的,就是 Day 17 留下的那道安全確認。
產品上的規則是:
合法,不等於可以立刻取代使用者目前的工作。
程式上的對應,是讓「合法」和「取代」變成兩個不同步驟:
先驗證
↓
再確認
↓
最後才套用
browser test 一樣先放著,文末再說。
ClarifyBuild 的 draft 不是一段普通文字。
它保存的是使用者目前整理到一半的來源資料和操作進度。
例如:
原始想法
需求釐清結果
功能範圍
Specification Setup
專案限制
技術設定
這些資料加起來,就是目前這份專案規格的來源。
如果使用者已經花了一段時間,把一個模糊想法整理成 draft,然後選到另一個 JSON 檔,App 不應該只因為檔案格式合法,就直接把目前工作蓋掉。
今天加的確認文字是:
匯入會取代目前 draft,且需要重新產生輸出
這句話提醒兩件事。
第一,匯入會取代目前 draft。
第二,匯入後不能沿用舊的 generated output。
這道確認擋的是「使用者沒意識到匯入會覆蓋目前 draft」。
它還擋不了「使用者選錯檔」,因為 native window.confirm() 裡沒有顯示即將匯入的 draft 摘要。這也是之後如果改成 App 內 modal,可以再補上的細節:例如顯示即將匯入的 idea。
加確認其實不難。
Day 18 的 importDraftState(raw) 本來就只是解析、驗證,然後回傳一份新的 state。它不會碰目前的 state。
所以如果只看今天的 native window.confirm(),在 state = imported 前面插一行確認,也能做到取消時保留 draft。
我還是把它拆成兩段,原因有三個。
第一,「檔案合法,但還沒生效」有了名字,也有固定的回傳形狀,可以被測。
第二,匯入的 state 要進 App,一定要經過 applyPreparedDraftImport()。
第三,今天用的是同步的 window.confirm(),跳出來的時候 App 整個停住。但之後如果換成 App 內 modal,畫面是靠 state render 出來的。打開 modal 本身就可能是一次 commit(),而 commit() 每次都會寫 localStorage。
那時候,如果 imported state 已經先換進 state,舊 draft 在確認視窗打開的那一刻就被蓋掉了。
第三點今天還沒發生。
今天先把順序切開,是為了之後換 modal 時不用重做這段流程。
Day 19 新增的第一段叫:
prepareDraftImport(raw)
它只做:
raw draft file
↓
parse JSON
↓
validate draft export shape
↓
restore imported builder state
↓
return prepared import result
如果檔案不合法,它回傳:
ok: false
state: null
message: "Draft 匯入失敗:檔案格式不符合 ClarifyBuild draft。"
如果檔案合法,它回傳:
ok: true
message: "匯入會取代目前 draft,且需要重新產生輸出"
state: importedState
重點是:prepareDraftImport() 不知道目前 App 的 current state 是什麼,也不會修改它。
它只是說:
這份檔案是合法 draft
如果使用者確認,可以套用這個 imported state
第二段叫:
applyPreparedDraftImport(preparedImport)
匯入後,Spec 和 Prompt 一定是空的。這件事有兩層在守。
第一層在 storage。
Day 17 開始,匯出檔本來就不寫 generatedSpec 和 generatedPrompt。匯入時,就算檔案裡多了這兩個欄位,也不會被還原。
第二層在 applyPreparedDraftImport()。
state 進 App 前,它再清一次。
為什麼這麼在意?
因為 Spec 和 Prompt 是從 draft 算出來的結果,來源資料只有 draft。
如果有哪個畫面還留著舊 draft 的 Spec,它很可能又被拿去產生 Prompt。這樣 Coding Agent 拿到的,就不是這份 draft 的決策。
所以 draft 一換,Spec 和 Prompt 就要從新的 draft 重新產生。
改完之後,browser action 裡的匯入流程是:
選檔案
↓
prepareDraftImport()
↓
不合法:跳錯誤 toast,目前 draft 不動,也不會問使用者
↓
合法:window.confirm()
↓
取消:目前 draft 和 Spec / Prompt 都在,跳「已取消」toast
↓
確定:applyPreparedDraftImport() → render + save → 跳「請重新產生輸出」toast
確定之後,如果 render 失敗,Day 17 留下來的 rollback 還在。
使用者反悔和程式出錯是兩回事。
使用者反悔時,imported state 不應該進 App。
程式出錯時,才需要把 state 放回 previousState。
今天先用 native window.confirm(),不是因為它是最好的互動。
而是因為 Day 19 要先把產品行為切清楚:
合法 draft 不再立即覆蓋目前 draft
等 browser test 有了,再決定要不要把確認視窗換成 App 內 modal。
Day 18 還有一個缺口。
改了來源資料就清掉 generated output,那段寫在 main.js 的 action 裡,Day 18 沒有測到。
這和今天的 import confirmation 有關。
因為兩者都在處理 generated output lifecycle。
ClarifyBuild 的規則是:
只要來源資料變了,舊的 Spec 和 Prompt 就不能繼續被當成目前輸出。
所以 Day 19 也把清空 generated output 的 helper 放進 appFlow.js:
invalidateGeneratedOutput(state)
main.js 原本的 local helper 現在改成呼叫它。
這沒有新增產品邏輯,只是把既有行為移到可以測的位置。
不過這個測試守得很小。
它只確認 helper 會清掉 generatedSpec 和 generatedPrompt。
至於 main.js 的 action 有沒有真的呼叫到它,今天沒有測。
清掉舊輸出,守的是 state 的規則:輸出一定是從目前 draft 算出來的。
今天的畫面上,反而看不出差別。使用者改完來源資料,要回到預覽頁就得重新產生,舊的 Spec 和 Prompt 不會被看到。
所以 browser test 從畫面驗不到這件事。要測它,得讓測試讀得到 state,或是像 Day 18 那樣,把這段狀態流程抽出來測。
Day 19 新增四個 app-flow 測試。
第一個測 prepareDraftImport():
draft import preparation validates without replacing the current draft
這個測試建立一份 current state,又建立另一份要匯入的 draft。
它確認合法檔案會變成 prepared import,而且 current state 沒有被改到。
嚴格說,current state 沒被改到,是函式簽名本身保證的:prepareDraftImport(raw) 根本沒有收到 current state。
但這個測試仍然有價值,因為它把 prepared result 的形狀固定下來:ok、message、state,還有匯入後的 stage 和空的 generated output。
第二個測壞檔:
draft import preparation rejects invalid draft files without changing current output
壞掉的 JSON 會失敗,不會產生可套用的 state。
第三個測 apply:
applying a prepared import clears generated output and rejects unusable results
它確認 applyPreparedDraftImport() 自己會清掉 generated output,也會拒絕不可用的 prepared result。
第四個測 source edit invalidation:
source edit flow invalidates generated output before regeneration
它鎖住 shared invalidation helper。
今天的測試沒有執行 src/main.js。
也就是說,真正的「使用者選檔 → confirmation → cancel / confirm → toast → render」還沒被自動化跑過。
Day 18 結束時:
tests 20
pass 20
fail 0
Day 19 結束後:
tests 24
pass 24
fail 0
另外也做了語法檢查:
node --check src/main.js
node --check src/state/appFlow.js
node --check test/appFlow.test.js
都通過。
靜態 server 也有確認可以啟動並回應首頁。
Day 19 沒有新增 browser UI automation。
也沒有引入 Playwright 或 jsdom。
src/main.js 仍然沒有被自動化測試真正執行。
確認 UI 也只是 native window.confirm(),不是 App 內 modal。
所以今天完成的是資料規則和 app-flow helper,還不是完整的瀏覽器互動驗證。
Day 19 表面上只是多了一句確認:
匯入會取代目前 draft,且需要重新產生輸出
但實際上,它是在修正一個產品邊界:
驗證合法,不等於可以立刻取代使用者目前的工作。
Day 17 讓匯入變安全:
不合法的檔案不能進 App
generated output 不能從 draft 復活
Day 18 讓匯入所在的 app flow 可以被測:
產生輸出
匯出
匯入
重新產生
Day 19 則補上使用者操作上的安全:
先驗證
再確認
最後才套用
今天最重要的承諾,是「取消就保留目前 draft」。
它寫在 main.js 的 if 裡,今天沒有任何測試會執行它。
Node tests 守住了 prepare 和 apply 的資料規則。
但使用者是不是真的選得了檔、看得到確認、按了取消之後什麼都沒少,還需要真的在瀏覽器裡跑一次。
明天 Day 20,我想補這條 browser-level test,先走三條路:
選壞檔 → 沒有確認視窗,跳錯誤 toast,目前 draft 還在
選好檔 → 取消 → 目前 draft 和 Spec / Prompt 都在
選好檔 → 確定 → 回到 Specification Setup,重新產生的 Spec 是新 draft 的內容
做到這裡,browser test 就繞不過 dependency 的決定了。
要不要把 window.confirm() 換成 App 內 modal,等這條測試有了再說。
Day 19 完成。
明天見。