From 37a9558f179e3ffabe9f568729d28fafeaaf92c5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 29 Oct 2026 20:58:18 +0100 Subject: [PATCH] :bug: 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 --- .../composable_tests/generated_test.cljs | 67 ++++++++++++++++--- .../composable_tests/interpreter.cljs | 57 ++++++++++++++++ 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/frontend/test/frontend_tests/composable_tests/generated_test.cljs b/frontend/test/frontend_tests/composable_tests/generated_test.cljs index 42bfb358b2..13af186a2c 100644 --- a/frontend/test/frontend_tests/composable_tests/generated_test.cljs +++ b/frontend/test/frontend_tests/composable_tests/generated_test.cljs @@ -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] diff --git a/frontend/test/frontend_tests/composable_tests/interpreter.cljs b/frontend/test/frontend_tests/composable_tests/interpreter.cljs index 0a6418080f..5686830185 100644 --- a/frontend/test/frontend_tests/composable_tests/interpreter.cljs +++ b/frontend/test/frontend_tests/composable_tests/interpreter.cljs @@ -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))