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]