From 4eb64ff7e952dc5a1476563938c8a4a0b2611ce5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 29 Oct 2026 18:48:46 +0100 Subject: [PATCH] :bug: Predict grid growth in the composable override model Retain authored cell bounds and direction through identity remapping. Use accepted reference context and child identities to predict new cells and track capacity without reading resulting cell values. Check expected bounds as well as cell values, while keeping pending copies unchanged. Cover row/column growth, corruption, structural histories, stale libraries and exact undo/redo. Keep the original four failing assertions unchanged. Pin the separate source-library frontend routing gap as F248 with an exact failure signature and a passing source-editor control. Validation: full common JVM/JavaScript and frontend suites; backend/exporter CI; common/frontend lint and format. All six common component properties pass 40 programs at seed 712 in both runtimes. Focused controls: 6 tests, 89 assertions. No production code changed; no sync benchmark was required. AI-assisted-by: gpt-6 --- .../common/test_helpers/composable/gen.cljc | 20 ++- .../common/test_helpers/composable/grid.cljc | 129 +++++++++++++----- .../test_helpers/composable/oracle.cljc | 4 +- .../test_helpers/composable/shadow.cljc | 33 +++-- .../logic/composable_gen_test.cljc | 93 +++++++++++++ .../composable_tests/generated_test.cljs | 69 +++++++++- 6 files changed, 290 insertions(+), 58 deletions(-) diff --git a/common/src/app/common/test_helpers/composable/gen.cljc b/common/src/app/common/test_helpers/composable/gen.cljc index 8a935314c3..fa70475e6b 100644 --- a/common/src/app/common/test_helpers/composable/gen.cljc +++ b/common/src/app/common/test_helpers/composable/gen.cljc @@ -36,6 +36,7 @@ [app.common.test-helpers.texts :as thtx] [app.common.types.component :as ctk] [app.common.types.container :as ctn] + [app.common.types.file :as ctf] [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] [clojure.test.check.generators :as gen])) @@ -227,6 +228,21 @@ (into {} (map (juxt (comp :id :shape) :shape)) (forest situation)) (mapcat #(vals (get-in % [:data :components])) (vals (cf/all-files situation))))) +(defn- settle-model + [model situation] + (let [references (when (some :layout-grid-cells (vals model)) + (into {} + (for [[file-id file] (cf/all-files situation) + container (concat (map #(ctn/make-container % :page) (vals (get-in file [:data :pages-index]))) + (map #(ctn/make-container % :component) (filter :deleted (vals (get-in file [:data :components]))))) + shape (vals (:objects container)) + :when (get-in model [(:id shape) :layout-grid-cells]) + :let [libraries (cf/reference-libraries situation file-id (:id shape)) + source (ctf/find-content-ref-shape file container libraries shape :include-deleted? true)] + :when source] + [(:id shape) source])))] + (shadow/settle model (all-objects situation) references))) + (defn world [[components depth opts]] (thi/reset-idmap!) @@ -706,7 +722,7 @@ (oracle/check-requested-update-main before settled model root-id)) model (-> model (shadow/parent-orientation (all-objects situation) (all-objects settled)) - (shadow/settle (all-objects settled))) + (settle-model settled)) all (into (set (synchronizable-components situation)) (r/pending-components situation)) full-before (r/settle situation all) full (:situation (cf/sync-world settled all)) @@ -882,7 +898,7 @@ next-model (cond-> (or next-model model) (not (#{:undo :redo} kind)) (shadow/parent-orientation (all-objects situation) (all-objects after))) - next-model (shadow/settle next-model (all-objects after)) + next-model (settle-model next-model after) meaningful (not (oracle/equivalent? (oracle/design-snapshot situation {:free-head-placement false}) (oracle/design-snapshot after {:free-head-placement false}))) state (-> state diff --git a/common/src/app/common/test_helpers/composable/grid.cljc b/common/src/app/common/test_helpers/composable/grid.cljc index d118e42dff..e88a3699c1 100644 --- a/common/src/app/common/test_helpers/composable/grid.cljc +++ b/common/src/app/common/test_helpers/composable/grid.cljc @@ -8,11 +8,18 @@ "Independent cell expectations. No production grid assignment or direction helper supplies expected values.") +(defn get-context + [shape] + {:rows (count (:layout-grid-rows shape)) + :columns (count (:layout-grid-columns shape)) + :direction (or (:layout-grid-dir shape) :row)}) + (defn get-cells [shape] - (->> (vals (:layout-grid-cells shape)) - (sort-by (juxt :row :column)) - (mapv #(dissoc % :id)))) + (with-meta (->> (vals (:layout-grid-cells shape)) + (sort-by (juxt :row :column)) + (mapv #(dissoc % :id))) + {::context (get-context shape)})) (defn remap-groups [groups ids-map] @@ -20,7 +27,8 @@ (:layout-grid-cells groups) (update-in [:layout-grid-cells :layout-grid-cells] (fn [cells] - (mapv #(update % :shapes (fn [ids] (mapv (fn [id] (get ids-map id id)) ids))) cells))))) + (with-meta (mapv #(update % :shapes (fn [ids] (mapv (fn [id] (get ids-map id id)) ids))) cells) + (meta cells)))))) (defn get-child-map "Pair identities through refs, never through resulting cell membership." @@ -50,10 +58,11 @@ (if-let [id (nth members index nil)] [id] [])]) new))] (if (= (or (:layout-grid-dir shape) :row) direction) cells - (mapv (fn [cell] - (if-let [ids (get by-cell [(:row cell) (:column cell)])] - (assoc cell :shapes ids) - cell)) cells)))) + (with-meta (mapv (fn [cell] + (if-let [ids (get by-cell [(:row cell) (:column cell)])] + (assoc cell :shapes ids) + cell)) cells) + (meta cells))))) (defn get-intended-values [shape action value] @@ -62,7 +71,9 @@ :direction (direction-cells shape value) :cell (mapv #(if (= [(:row value) (:column value)] [(:row %) (:column %)]) (merge % (:props value)) %) before))] - (cond-> {:layout-grid-cells cells} + (cond-> {:layout-grid-cells (with-meta cells + {::context (cond-> (get-context shape) + (= action :direction) (assoc :direction value))})} (= action :direction) (assoc :layout-grid-dir value)))) (defn edit @@ -75,31 +86,77 @@ model (get-intended-values shape action value)) model)) +(defn- extend-cells + [cells before after] + (into cells + (for [row (range 1 (inc (:rows after))) + column (range 1 (inc (:columns after))) + :when (or (> row (:rows before)) (> column (:columns before))) + :when (not-any? #(and (<= (:row %) row (+ (:row %) (:row-span %) -1)) + (<= (:column %) column (+ (:column %) (:column-span %) -1))) cells)] + {:row row :column column :row-span 1 :column-span 1 :position :auto + :align-self :auto :justify-self :auto :shapes []}))) + +(defn matches-context? + [shape cells] + (if-let [{:keys [rows columns] :as context} (::context (meta cells))] + (and (= context (get-context shape)) + (every? #(and (<= 1 (:row %) (+ (:row %) (:row-span %) -1) rows) + (<= 1 (:column %) (+ (:column %) (:column-span %) -1) columns)) cells)) + true)) + (defn settle-cells - "Drop departed/absolute children; fill existing empty cells with participating - children in parent order. A gap, manual cell or span ends the auto prefix." - [cells shape objects] - (let [order (if (= :column (:layout-grid-dir shape)) (juxt :column :row) (juxt :row :column)) - participating (filterv #(let [child (get objects %)] - (not (or (:hidden child) (:layout-item-absolute child)))) (:shapes shape)) - members (set participating) - prefix (fn [cells] - (second - (reduce (fn [[ended result] cell] - (let [manual (or (= :manual (:position cell)) - (not= 1 (:row-span cell) (:column-span cell)) - (and ended (seq (:shapes cell)) (= :auto (:position cell))))] - [(or ended manual (empty? (:shapes cell))) - (conj result (cond-> cell - (and manual (= :auto (:position cell))) (assoc :position :manual)))])) - [false []] cells))) - cells (->> cells (sort-by order) - (mapv #(update % :shapes (fn [ids] (filterv members ids)))) prefix) - assigned (set (mapcat :shapes cells)) - missing (remove assigned participating) - [cells _] (reduce (fn [[result pending] cell] - (if (and (seq pending) (empty? (:shapes cell))) - [(conj result (assoc cell :position :auto :shapes [(first pending)])) (rest pending)] - [(conj result cell) pending])) - [[] missing] cells)] - (->> cells prefix (sort-by (juxt :row :column)) vec))) + "Predict inherited and automatic track growth from prior cells, reference + context and child identities. Result cell values never supply expectations." + ([cells shape objects] (settle-cells cells shape objects nil)) + ([cells shape objects reference] + (let [prior (::context (meta cells)) + participating (filterv #(let [child (get objects %)] + (not (or (:hidden child) (:layout-item-absolute child)))) (:shapes shape)) + members (set participating) + trimmed (mapv #(update % :shapes (fn [ids] (filterv members ids))) cells) + assigned (set (mapcat :shapes trimmed)) + missing (remove assigned participating) + ;; A pending/unaccepted source can be ahead of this copy. Use its + ;; context once track writes or added children have reached the copy. + context (if (and prior reference + (or (not= prior (get-context shape)) + (> (count missing) (count (filter #(empty? (:shapes %)) trimmed))))) + reference prior) + cells (if context (extend-cells trimmed prior context) trimmed) + order (if (= :column (or (:direction context) (:layout-grid-dir shape))) + (juxt :column :row) (juxt :row :column)) + prefix (fn [cells] + (second + (reduce (fn [[ended result] cell] + (let [manual (or (= :manual (:position cell)) + (not= 1 (:row-span cell) (:column-span cell)) + (and ended (seq (:shapes cell)) (= :auto (:position cell))))] + [(or ended manual (empty? (:shapes cell))) + (conj result (cond-> cell + (and manual (= :auto (:position cell))) (assoc :position :manual)))])) + [false []] cells))) + cells (->> cells (sort-by order) prefix) + assigned (set (mapcat :shapes cells)) + missing (remove assigned participating) + free (count (filter #(empty? (:shapes %)) cells)) + minimum (when context (cond-> context + (> (count missing) free) + (-> (update :rows max 1) (update :columns max 1)))) + cells (if minimum (extend-cells cells context minimum) cells) + free (count (filter #(empty? (:shapes %)) cells)) + deficit (- (count missing) free) + grown (when minimum + (cond-> minimum + (and (pos? deficit) (= :row (:direction minimum))) + (update :columns + (quot (+ deficit (:rows minimum) -1) (:rows minimum))) + (and (pos? deficit) (= :column (:direction minimum))) + (update :rows + (quot (+ deficit (:columns minimum) -1) (:columns minimum))))) + cells (if grown (extend-cells cells minimum grown) cells) + [cells _] (reduce (fn [[result pending] cell] + (if (and (seq pending) (empty? (:shapes cell))) + [(conj result (assoc cell :position :auto :shapes [(first pending)])) (rest pending)] + [(conj result cell) pending])) + [[] missing] (sort-by order cells))] + (with-meta (->> cells prefix (sort-by (juxt :row :column)) vec) + (cond-> (meta cells) grown (assoc ::context grown)))))) diff --git a/common/src/app/common/test_helpers/composable/oracle.cljc b/common/src/app/common/test_helpers/composable/oracle.cljc index 8a421d1d68..59e4164573 100644 --- a/common/src/app/common/test_helpers/composable/oracle.cljc +++ b/common/src/app/common/test_helpers/composable/oracle.cljc @@ -480,7 +480,9 @@ (fail (if (some #(not (contains? expected %)) touched) :I2 :I3) {:shape (:name shape) :expected (set (keys expected)) :actual touched})) (doseq [[group value] expected] - (when-not (shadow/same-value? shape group value (:objects page)) + (when-not (and (shadow/same-value? shape group value (:objects page)) + (or (not= group :layout-grid-cells) + (grid/matches-context? shape (:layout-grid-cells value)))) (fail :I9 {:shape (:name shape) :group group :expected value :actual (shadow/values shape group)}))) (when (and settled (:shape-ref shape)) diff --git a/common/src/app/common/test_helpers/composable/shadow.cljc b/common/src/app/common/test_helpers/composable/shadow.cljc index c990a3aacb..37cca906be 100644 --- a/common/src/app/common/test_helpers/composable/shadow.cljc +++ b/common/src/app/common/test_helpers/composable/shadow.cljc @@ -338,20 +338,25 @@ (defn settle "A detached position turns its copies into ordinary shapes. Their former component choice stops applying; other authored groups stay in place." - [model objects] - (reduce-kv - (fn [result id groups] - (let [shape (get objects id) - position (get objects (:shape-ref shape)) - ordinary? (and shape position - (nil? (:component-id shape)) - (nil? (:component-id position))) - groups (cond-> groups - ordinary? (dissoc :swap-group) - (:layout-grid-cells groups) - (update-in [:layout-grid-cells :layout-grid-cells] grid/settle-cells shape objects))] - (if (seq groups) (assoc result id groups) (dissoc result id)))) - model model)) + ([model objects] (settle model objects {})) + ([model objects references] + (reduce-kv + (fn [result id groups] + (let [shape (get objects id) + position (get objects (:shape-ref shape)) + ordinary? (and shape position + (nil? (:component-id shape)) + (nil? (:component-id position))) + source (get references id) + context (when source + (grid/get-context (merge source (:layout-grid-rows groups) + (:layout-grid-columns groups) (:layout-grid-dir groups)))) + groups (cond-> groups + ordinary? (dissoc :swap-group) + (:layout-grid-cells groups) + (update-in [:layout-grid-cells :layout-grid-cells] grid/settle-cells shape objects context))] + (if (seq groups) (assoc result id groups) (dissoc result id)))) + model model))) (defn- subtree-shapes [objects id] diff --git a/common/test/common_tests/logic/composable_gen_test.cljc b/common/test/common_tests/logic/composable_gen_test.cljc index b68bab3db8..5f59bd5716 100644 --- a/common/test/common_tests/logic/composable_gen_test.cljc +++ b/common/test/common_tests/logic/composable_gen_test.cljc @@ -18,6 +18,7 @@ [app.common.test-helpers.composable.comp.setups :as setup] [app.common.test-helpers.composable.core :as tm] [app.common.test-helpers.composable.gen :as cg] + [app.common.test-helpers.composable.grid :as grid] [app.common.test-helpers.composable.oracle :as oracle] [app.common.test-helpers.composable.shadow :as shadow] [app.common.test-helpers.composable.variants :as variants] @@ -25,10 +26,102 @@ [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] [app.common.test-helpers.texts :as thtx] + [app.common.uuid :as uuid] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) +(t/deftest generated-grid-growth-retains-authored-cells-through-sync-and-undo + (doseq [direction [:row :column] + turn-copy? [false true]] + (let [ops (vec (concat (when (= direction :column) [[:grid-direction 0 0 1 0]]) + [[:grid-cell 0 0 0 1]] + (when turn-copy? [[:grid-direction 0 0 (if (= direction :row) 1 0) 1]]) + [[:add-child 0] [:add-child 0] [:sync] [:undo] [:redo]])) + program {:world [1 0 {:layout :grid :layout-children true :grid-square true}] + :ops ops}] + (t/is (true? (try (cg/run program) true + (catch #?(:clj Exception :cljs js/Error) error + (select-keys (ex-data error) [:invariant :phase :group :failing-op])))) + (pr-str program))))) + +(t/deftest grown-grid-intent-ignores-result-cells-and-copy-track-bounds + (let [before (-> (cg/initial-state [1 0 {:layout :grid :layout-children true :grid-square true}]) + (cg/step [:grid-cell 0 0 0 1]) cg/settle-state + (cg/step [:add-child 0]) cg/settle-state) + id (get-in (cg/resolve-op (:situation before) (:model before) [:grid-cell 0 0 0 1]) [:shape :id]) + grown (-> before (cg/step [:add-child 0]) cg/settle-state) + situation (:situation grown) + file (tm/file situation) + page (thf/current-page file) + objects (:objects page) + shape (get objects id) + source (get objects (:shape-ref shape)) + cell-id (:id (first (filter #(= [1 3] [(:row %) (:column %)]) (vals (:layout-grid-cells shape))))) + extra-id (uuid/next) + expected (:model grown) + cells (get-in expected [id :layout-grid-cells :layout-grid-cells]) + predict (fn [copy reference] + (shadow/settle (:model before) (assoc objects id copy) {id reference})) + check (fn [copy model] + (let [file (assoc-in file [:data :pages-index (:id page) :objects id] copy)] + (try (oracle/check (tm/with-file situation file) model) nil + (catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error))))))] + (t/is (= 6 (count cells))) + (t/is (= #{[1 1] [1 2] [1 3] [2 1] [2 2] [2 3]} (set (map (juxt :row :column) cells)))) + (t/is (= (frequencies (:shapes shape)) (frequencies (mapcat :shapes cells)))) + (t/is (nil? (check shape expected))) + (let [stale (tm/shape-by-id (:situation before) id) + pending (predict stale source)] + (t/is (= (:model before) pending) "A source ahead of its pending copy cannot grow that copy's model") + (t/is (grid/matches-context? stale (get-in pending [id :layout-grid-cells :layout-grid-cells])))) + (t/is (= expected (predict shape (update source :layout-grid-cells + #(update-vals % (fn [cell] (assoc cell :align-self :end :shapes []))))))) + (doseq [corrupt [#(assoc-in % [:layout-grid-cells cell-id :align-self] :end) + #(assoc-in % [:layout-grid-cells cell-id :justify-self] :center) + #(assoc-in % [:layout-grid-cells cell-id :position] :manual) + #(assoc-in % [:layout-grid-cells cell-id :shapes] []) + #(assoc-in % [:layout-grid-cells cell-id :shapes] [(first (:shapes shape))]) + #(assoc-in % [:layout-grid-cells cell-id :column] 4) + #(assoc-in % [:layout-grid-cells extra-id] + {:id extra-id :row 2 :column 4 :row-span 1 :column-span 1 + :position :auto :align-self :auto :justify-self :auto :shapes []}) + #(update % :layout-grid-columns conj {:type :flex :value 1}) + #(assoc % :layout-grid-dir :column)]] + (let [copy (corrupt shape) + model (predict copy source)] + (t/is (= expected model) "Corrupt result values cannot supply expected cells") + (t/is (= :I9 (check copy model))))) + (t/is (= :I3 (check (update shape :touched disj :layout-grid-cells) expected))) + (let [remapped (grid/remap-groups (get expected id) (zipmap (:shapes shape) (reverse (:shapes shape)))) + cells (get-in remapped [:layout-grid-cells :layout-grid-cells])] + (t/is (false? (grid/matches-context? (update shape :layout-grid-columns conj {:type :flex :value 1}) cells)))) + (let [prior (get-in before [:model id :layout-grid-cells :layout-grid-cells]) + ;; Both source and copy lose their track growth, while the new child remains. + dropped (assoc shape :layout-grid-columns (vec (take 2 (:layout-grid-columns shape)))) + context (grid/get-context dropped) + cells (grid/settle-cells prior dropped objects context)] + (t/is (= 6 (count cells))) + (t/is (= (frequencies (:shapes shape)) (frequencies (mapcat :shapes cells)))) + (t/is (false? (grid/matches-context? dropped cells)))))) + +(t/deftest grid-growth-context-survives-remapping-and-library-acceptance + (doseq [program + (concat + (for [tail [[[:duplicate 0] [:undo] [:redo]] [[:update-main 0] [:undo] [:redo]] + [[:reset 0] [:undo] [:redo]] [[:detach 0] [:undo] [:redo]]]] + {:world [1 0 {:layout :grid :layout-children true :grid-square true}] + :ops (into [[:grid-cell 0 0 0 1] [:add-child 0] [:add-child 0] [:sync 1]] tail)}) + [{:world [1 1 {:layout :grid :layout-children true :grid-square true}] + :ops [[:grid-cell 0 0 0 1] [:add-child 0] [:add-child 0] [:sync 1] [:detach 0] [:undo] [:redo]]} + {: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] [:library-sync] [:undo] [:redo]]}])] + (t/is (true? (try (cg/run program) true + (catch #?(:clj Exception :cljs js/Error) error + (select-keys (ex-data error) [:invariant :phase :group :failing-op])))) + (pr-str program)))) + (t/deftest whole-program-failures-keep-the-same-signature-without-a-skipped-tail (doseq [[world ops source?] [[[1 0] [[:edit 0 0 1 1] [:edit 0 1 1 1]] false] diff --git a/frontend/test/frontend_tests/composable_tests/generated_test.cljs b/frontend/test/frontend_tests/composable_tests/generated_test.cljs index 89de0b9606..c2c3e9c2c6 100644 --- a/frontend/test/frontend_tests/composable_tests/generated_test.cljs +++ b/frontend/test/frontend_tests/composable_tests/generated_test.cljs @@ -6,6 +6,8 @@ (ns frontend-tests.composable-tests.generated-test (:require + [app.common.test-helpers.composable.comp.files :as cf] + [app.common.test-helpers.composable.comp.nodes :as n] [app.common.test-helpers.composable.comp.undo-check :as uc] [app.common.test-helpers.composable.core :as tm] [app.common.test-helpers.composable.gen :as cg] @@ -53,11 +55,14 @@ 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 (if-let [op (:op actual)] - (await (ftm/run-generated-step live op)) - (do - (t/is (= :settled (await (ftm/await-step [])))) - (ftm/refresh-generated live))) + (let [next-live (try (if-let [op (:op actual)] + (await (ftm/run-generated-step live op)) + (do + (t/is (= :settled (await (ftm/await-step [])))) + (ftm/refresh-generated live))) + (catch :default error + (throw (ex-info (ex-message error) + (assoc (ex-data error) :replay-tuple tuple) error)))) next-state (-> state (cg/step tuple) (cg/settle-state))] (when-not next-live (throw (ex-info "Frontend step failed" {:program program :tuple tuple}))) @@ -102,6 +107,35 @@ (t/is false (str (ex-message error) "\nReplay: " (pr-str program) "\nDetails: " (pr-str (ex-data error))))))))) +(defn- ^:async replay-known-source-routing-failure + [program] + (await + (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock} + (t/is (map? (cg/run program))) + ;; The same source edits are valid when the source owns the editor. + (let [situation (cg/world (:world program)) + files (cf/all-files situation) + owner (first (keys (tm/aux-files situation))) + parent-id (get-in (cg/resolve-op situation {} [:library-add-child 0]) [:shape :id]) + editor (-> situation (tm/with-file (get files owner)) (tm/with-aux-files (dissoc files owner))) + initial (ftm/start-generated editor) + before (ftm/snapshot) + first (await (ftm/run-generated-step initial (n/add-child (constantly parent-id) :source-a :width 20 :height 20))) + second (await (ftm/run-generated-step first (n/add-child (constantly parent-id) :source-b :width 20 :height 20))) + shape (tm/shape-by-id second parent-id)] + (t/is (= 5 (count (:shapes shape)))) + (t/is (= [2 3] (mapv #(count (get shape %)) [:layout-grid-rows :layout-grid-columns]))) + (t/is (= (dissoc (:files before) owner) (dissoc (:files (ftm/snapshot)) owner))) + (t/is (empty? (uc/failures (await (ftm/finish-generated second))))) + (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]} + actual))))) + (def update-main-intent-programs [{:world [1 0] :ops [[:edit 0 0 1 1] [:update-main 0] [:undo] [:redo]]} {:world [1 0 {:library true}] :ops [[:edit 0 0 1 2] [:update-main 0] [:undo] [:redo]]} @@ -146,6 +180,31 @@ (t/is (= :I9 (:invariant failure)) (pr-str failure)) (t/is (= :requested-update-main (:phase failure)) (pr-str failure)))))))) +(t/deftest ^:async grid-growth-replays-with-independent-cell-expectations + (doseq [direction [:row :column] + turn-copy? [false true]] + (await (replay {:world [1 0 {:layout :grid :layout-children true :grid-square true}] + :ops (vec (concat (when (= direction :column) [[:grid-direction 0 0 1 0]]) + [[:grid-cell 0 0 0 1]] + (when turn-copy? [[:grid-direction 0 0 (if (= direction :row) 1 0) 1]]) + [[:add-child 0] [:add-child 0] [:sync] [:undo] [:redo]]))})))) + +(t/deftest ^:async grid-growth-context-replays-through-structural-and-library-operations + (doseq [program + (concat + (for [tail [[[:duplicate 0] [:undo] [:redo]] [[:update-main 0] [:undo] [:redo]] + [[:reset 0] [:undo] [:redo]] [[:detach 0] [:undo] [:redo]]]] + {:world [1 0 {:layout :grid :layout-children true :grid-square true}] + :ops (into [[:grid-cell 0 0 0 1] [:add-child 0] [:add-child 0] [:sync 1]] tail)}) + [{:world [1 1 {:layout :grid :layout-children true :grid-square true}] + :ops [[:grid-cell 0 0 0 1] [:add-child 0] [:add-child 0] [:sync 1] [:detach 0] [:undo] [:redo]]} + {: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] [:library-sync] [:undo] [:redo]]}])] + (if (get-in program [:world 2 :stale-libraries]) + (await (replay-known-source-routing-failure program)) + (await (replay program))))) + (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]