🐛 Resolve direction edits for empty generated grids

Allow direction edits on eligible grid containers without requiring cells.
Keep cell edits limited to grids with an existing cell. Add an opt-in empty
grid fixture without changing random operation distributions.

Cover main and copy scopes, nested grids, meaningful edits, skipped cell
edits and exact undo/redo. Add reset, update-main, duplicate, detach, child
addition and direction-return histories in common and frontend runners.
The original 40 assertions failed 16 times before the fix and pass unchanged.

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 common: 61 assertions;
frontend controls: 56 direction and 204 history assertions.
No production code changed; no sync benchmark was required.

AI-assisted-by: gpt-6
This commit is contained in:
alonso.torres 2026-10-29 20:07:27 +01:00
parent 4eb64ff7e9
commit 3953129adb
3 changed files with 68 additions and 2 deletions

View File

@ -64,6 +64,18 @@
file (cond-> file (= layout :grid) (tho/place-in-grid-columns root 2 {1 child}))]
(tm/with-file situation file))))
(defn- with-empty-grid
[situation name]
(let [situation (-> situation (r/step (n/remove-child (n/remote-rect-of name))) r/settle
(r/with-history {}))
file (tm/file situation)
page-id (:id (thf/current-page file))
ids (keep #(when (= :grid (get-in % [:shape :layout])) (get-in % [:shape :id])) (tree situation))
changes (cls/generate-update-shapes
(pcb/empty-changes nil page-id) ids #(merge % cls/initial-grid-layout)
(:objects (thf/current-page file)) {:ignore-touched true :skip-grid-reassignment? true})]
(tm/with-file situation (thf/apply-changes file changes))))
(defn- with-settled-layout
[situation name]
(let [file (tm/file situation)
@ -292,6 +304,7 @@
(ths/update-shape :copy-child :grow-type :auto-height)
(ths/update-shape :copy-child :touched nil)))))
(range components))
(and (= :grid (:layout opts)) (:grid-empty opts)) (with-empty-grid "random-0")
(:library opts) (with-library opts)
(:stale-libraries opts) (cf/freeze-libraries)
(:rich-variants opts) (as-> s (assoc s :shape-budget (+ 80 (count (tree s)))))))
@ -543,8 +556,9 @@
(library-candidates situation kind scope)
(candidates situation model (if (#{:style :grid-cell :grid-direction} kind) :edit kind) scope))
(#{:grid-cell :grid-direction} kind)
(filter #(and (= :grid (get-in % [:shape :layout]))
(seq (get-in % [:shape :layout-grid-cells]))))
(filter #(= :grid (get-in % [:shape :layout])))
(= :grid-cell kind)
(filter #(seq (get-in % [:shape :layout-grid-cells])))
(= kind :style)
(filter (fn [{:keys [shape]}]
(and (shadow/attribute-applicable? shape style-property)

View File

@ -31,6 +31,44 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest empty-grid-direction-edits-resolve-and-change-the-requested-shape
(doseq [depth [0 1]
scope [nil 1]]
(let [spec [1 depth {:layout :grid :grid-empty true}]
initial (cg/initial-state spec)
situation (:situation initial)
tuple [:grid-direction 0 0 1 scope]
resolved (cg/resolve-op situation {} tuple)
grids (filter #(= :grid (get-in % [:shape :layout])) (cg/tree situation))
target-id (:id (:shape (first (filter #(= :grid (get-in % [:shape :layout]))
(cg/candidates situation {} :edit scope)))))
real (r/settle (r/step situation (n/edit-grid (constantly target-id) :direction :column)))
stepped (cg/step initial tuple)]
(t/is (some? (thf/validate-file! (tm/file situation) (cf/libraries situation))))
(t/is (seq grids))
(t/is (every? #(and (empty? (get-in % [:shape :shapes]))
(= {} (get-in % [:shape :layout-grid-cells]))) grids))
(t/is (= :column (:layout-grid-dir (tm/shape-by-id real target-id))))
(t/is (= (oracle/snapshot situation) (oracle/snapshot (r/step real (n/undo)))))
(t/is (= :executed (get-in resolved [:trace :status])))
(t/is (= target-id (get-in resolved [:shape :id])))
(t/is (= 1 (get-in stepped [:coverage :random :grid-direction :changed])))
(t/is (nil? (:op (cg/resolve-op situation {} [:grid-cell 0 0 0 scope]))))
(t/is (true? (try (cg/run {:world spec :ops [tuple [:undo] [:redo]]}) true
(catch #?(:clj Exception :cljs js/Error) error
(select-keys (ex-data error) [:invariant :phase :group :failing-op]))))))))
(t/deftest empty-grid-direction-intent-survives-component-histories
(doseq [depth [0 1]
tail [[[:grid-direction 0 0 0 1]] [[:update-main 0]] [[:reset 0]]
[[:duplicate 0]] [[:detach 0]] [[:add-child 0]]]]
(let [program {:world [1 depth {:layout :grid :grid-empty true}]
:ops (into [[:grid-direction 0 0 1 1]] (concat tail [[: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 generated-grid-growth-retains-authored-cells-through-sync-and-undo
(doseq [direction [:row :column]
turn-copy? [false true]]

View File

@ -180,6 +180,20 @@
(t/is (= :I9 (:invariant failure)) (pr-str failure))
(t/is (= :requested-update-main (:phase failure)) (pr-str failure))))))))
(t/deftest ^:async empty-grid-direction-edits-replay-through-the-frontend
(doseq [depth [0 1]
scope [nil 1]]
(await (replay {:world [1 depth {:layout :grid :grid-empty true}]
:ops [[:grid-cell 0 0 0 scope] [:grid-direction 0 0 1 scope]
[:undo] [:redo]]}))))
(t/deftest ^:async empty-grid-direction-histories-replay-through-the-frontend
(doseq [depth [0 1]
tail [[[:grid-direction 0 0 0 1]] [[:update-main 0]] [[:reset 0]]
[[:duplicate 0]] [[:detach 0]] [[:add-child 0]]]]
(await (replay {:world [1 depth {:layout :grid :grid-empty true}]
:ops (into [[:grid-direction 0 0 1 1]] (concat tail [[:sync] [:undo] [:redo]]))}))))
(t/deftest ^:async grid-growth-replays-with-independent-cell-expectations
(doseq [direction [:row :column]
turn-copy? [false true]]