🐛 Replay source-library edits in their owning editor

Keep separate workspace undo state for source and consumer editors. Run
source operations through their real watchers, verify they leave other
files unchanged, and check exact per-step and whole-history undo/redo.
Retain each editor's own baseline when loaded foreign files change.

Cover source additions, interleaved source/consumer edits and undo/redo,
and two source owners. The original F248 positive regression passes without
changing its assertions. Keep the full acceptance history unchanged and
pin its newly exposed F249 reference failure, with a real acceptance control.

Validation: full frontend suite, 1,760 tests and 14,721 assertions; build,
lint and format. Focused controls: 80 interleaved, 31 two-source, 9 acceptance
and 138 full-history assertions. Production and common code did not change;
no new sync benchmark was required.

AI-assisted-by: gpt-6
This commit is contained in:
alonso.torres 2026-10-29 20:58:18 +01:00
parent 3953129adb
commit 37a9558f17
2 changed files with 115 additions and 9 deletions

View File

@ -51,12 +51,14 @@
pure (:situation state)
live (ftm/refresh-generated live)
expected (cg/resolve-op pure (:model state) tuple)
actual (if (#{:undo :redo} (first tuple))
actual (if (#{:undo :redo :library-undo :library-redo} (first tuple))
expected
(cg/resolve-op live (live-model state live) tuple))]
(t/is (= (:trace expected) (:trace actual)) (str "Replay changed a target: " tuple))
(let [next-live (try (if-let [op (:op actual)]
(await (ftm/run-generated-step live op))
(if-let [owner (:library-id actual)]
(await (ftm/run-generated-source-step live owner op))
(await (ftm/run-generated-step live op)))
(do
(t/is (= :settled (await (ftm/await-step []))))
(ftm/refresh-generated live)))
@ -82,7 +84,7 @@
(oracle/check next-live (live-model next-state next-live))
(catch :default error
(throw (ex-info (str "Frontend oracle at " tuple ": " (ex-message error))
(ex-data error) error))))
(assoc (ex-data error) :replay-tuple tuple) error))))
(let [parity {:relative-free-head-placement true}
expected (oracle/canonical (:situation next-state) parity)
actual (oracle/canonical next-live parity)]
@ -107,7 +109,7 @@
(t/is false (str (ex-message error) "\nReplay: " (pr-str program)
"\nDetails: " (pr-str (ex-data error)))))))))
(defn- ^:async replay-known-source-routing-failure
(defn- ^:async replay-known-library-reference-failure
[program]
(await
(mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock}
@ -130,10 +132,9 @@
(ftm/uninstall!))
(let [actual (try (await (replay-program program)) :success
(catch :default error
(select-keys (ex-data error) [:type :code :hint :replay-tuple])))]
(tm/known-failure :F248 :success
{:type :assertion :code :data-validation :hint "expected valid shape"
:replay-tuple [:library-add-child 0]}
(select-keys (ex-data error) [:type :code :hint :invariant :phase :group :replay-tuple])))]
(tm/known-failure :F249 :success
{:invariant :I4 :group :structure :replay-tuple [:library-sync]}
actual)))))
(def update-main-intent-programs
@ -216,9 +217,57 @@
:ops [[:grid-cell 0 0 0 2] [:library-add-child 0] [:library-add-child 0]
[:edit 0 1 1 2] [:library-sync] [:undo] [:redo]]}])]
(if (get-in program [:world 2 :stale-libraries])
(await (replay-known-source-routing-failure program))
(await (replay-known-library-reference-failure program))
(await (replay program)))))
(t/deftest ^:async source-grid-additions-replay-without-accepting-the-library
(await (replay {:world [1 0 {:library true :library-depth 0 :library-grid true :stale-libraries true}]
:ops [[:grid-cell 0 0 0 2] [:library-add-child 0] [:library-add-child 0]
[:edit 0 1 1 2] [:undo] [:redo]]})))
(t/deftest ^:async source-editors-retain-their-own-undo-and-redo-histories
(doseq [ops [[[:library-add-child 0] [:library-add-child 0] [:library-undo 0]
[:edit 0 1 1 2] [:library-redo 0] [:undo] [:redo]]
[[:edit 0 1 1 2] [:library-edit 0 0 1] [:undo]
[:library-undo 0] [:redo] [:library-redo 0]]
[[:library-transform 0 0 1 0] [:edit 0 1 1 2]
[:library-undo 0] [:undo] [:library-redo 0] [:redo]]]]
(await (replay {:world [1 0 {:library true :library-depth 0 :library-grid true :stale-libraries true}]
:ops ops}))))
(t/deftest ^:async two-source-editors-keep-their-histories-separate
(await (replay {:world [1 0 {:library true :library-depth 0 :library-dependency true :stale-libraries true}]
:ops [[:library-edit 0 0 1] [:library-edit 1 0 2]
[:library-undo 0] [:library-undo 1]
[:library-redo 0] [:library-redo 1]]})))
(t/deftest ^:async source-grid-acceptance-preserves-authored-cells-and-undo
(await
(mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock}
(let [initial (cg/world [1 0 {:library true :library-depth 0 :library-grid true :stale-libraries true}])
edit (cg/resolve-op initial {} [:grid-cell 0 0 0 2])
id (get-in edit [:shape :id])
props (get-in edit [:op :value :props])
live (ftm/start-generated initial)
live (await (ftm/run-generated-step live (:op edit)))
live (loop [live live tuples [[:library-add-child 0] [:library-add-child 0] [:edit 0 1 1 2]]]
(if-let [tuple (first tuples)]
(let [{:keys [op library-id]} (cg/resolve-op live {} tuple)
after (if library-id
(await (ftm/run-generated-source-step live library-id op))
(await (ftm/run-generated-step live op)))]
(recur after (rest tuples)))
live))
live (await (ftm/run-generated-step live (:op (cg/resolve-op live {} [:library-sync]))))
shape (tm/shape-by-id live id)
cell (first (filter #(= [1 1] [(:row %) (:column %)]) (vals (:layout-grid-cells shape))))]
(t/is (some? (cthf/validate-file! (tm/file live) (cf/libraries live))))
(t/is (= 5 (count (:shapes shape))))
(t/is (= [2 3] (mapv #(count (get shape %)) [:layout-grid-rows :layout-grid-columns])))
(t/is (= props (select-keys cell (keys props))))
(t/is (empty? (uc/failures (await (ftm/finish-generated live)))))
(ftm/uninstall!)))))
(t/deftest ^:async authored-grid-histories-use-independent-cell-expectations
(doseq [ops [[[:grid-direction 0 0 1 1] [:grid-direction 0 0 1 0]
[:grid-direction 0 0 0 0] [:update-main 0]

View File

@ -872,6 +872,63 @@
[situation op]
(run-ops situation [op]))
(defn- get-generated-editor
[situation]
{:situation (dissoc (refresh-generated situation) ::source-editors)
:undo (:workspace-undo @st/state)})
(defn- resume-generated-editor
[{:keys [situation undo]}]
(let [file (tm/file situation)]
(uninstall!)
(st/emit! (ptk/reify ::resume-generated-editor
ptk/UpdateEvent
(update [_ state]
(assoc state :current-file-id (:id file)
:current-page-id (cthf/current-page-id file)
:workspace-undo undo)))
(dw/initialize-edit-watchers)
(dwl/watch-library-changes)
(headless-text-measurement))
(let [situation (refresh-generated situation)
baseline (uc/baseline situation)
current (snapshot)]
(if baseline
;; Another editor can change loaded libraries between this editor's
;; steps. Keep its own undo baseline and refresh only foreign files.
(uc/mark-baseline situation (:depth baseline)
(assoc current :files
(assoc (:files current) (:id file)
(get-in baseline [:snapshot :files (:id file)]))))
(mark-baseline situation)))))
(defn ^:async run-generated-source-step
"Run a source edit in its owning editor and retain both undo histories.
Returning to the consumer loads source data without accepting it."
[situation owner op]
(let [consumer (get-generated-editor situation)
files (cf/all-files situation)
editor (or (get-in situation [::source-editors owner])
{:situation (-> (tm/make-situation (get files owner))
(tm/with-aux-files (dissoc files owner))
(assoc ::undo-check? true ::generated true)
(uc/with-runner :frontend))
:undo {}})
source (resume-generated-editor editor)
before (snapshot)
source (await (run-ops source [op]))]
(when-not source
(throw (ex-info "Source editor step failed" {:owner owner})))
(t/is (= (dissoc (:files before) owner) (dissoc (:files (snapshot)) owner))
"A source editor must not write another file")
(let [source (await (check-variant source))
editor (get-generated-editor source)
live (resume-generated-editor consumer)]
(t/is (empty? (uc/failures source)) (pr-str (uc/failures source)))
(-> live
(assoc ::source-editors (assoc (::source-editors situation) owner editor))
(tm/record-application op {:library-id owner})))))
(defn finish-generated
[situation]
(check-variant situation))