From 59d06a10663e658874f4efd4628fde9907b3d9a0 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 23 Oct 2026 18:46:54 +0200 Subject: [PATCH] :wrench: Allow machine precision geometry noise in composable undo checks Keep snapshots exact and compare only shape geometry within an absolute 1e-10 tolerance. Reflow recomputed 0.01 as 0.010000000000005116 after undo. Opacity, touched groups and other authored fields still compare exactly. Fix the false positive F130 and document the narrow exception. The regression failed before the fix. Focused comparison and structural undo checks pass 2 tests with 9 assertions, including negative controls. AI-assisted-by: GPT-6 --- .serena/memories/common/testing.md | 9 +++++ .../composable/comp/undo_check.cljc | 35 ++++++++++++++++++- .../logic/composable_runner_test.cljc | 14 ++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.serena/memories/common/testing.md b/.serena/memories/common/testing.md index 8504fffcac..530b48ba9e 100644 --- a/.serena/memories/common/testing.md +++ b/.serena/memories/common/testing.md @@ -93,3 +93,12 @@ For geometry-sensitive tests, read `mem:common/geometry-invariants` before posit ## Debugging Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes. + +## Composable undo comparison + +`comp/undo-check` keeps normalized snapshots exact, including `:touched` and +`:remote-synced`. Comparison permits only absolute floating noise below 1e-10 +inside shape geometry: x/y, width/height, rotation, points, selrect and transform +matrices. Reflow can recompute 0.01 as 0.010000000000005116 after undo (F130). +This tolerance does not apply to opacity, other authored attrs or touched groups. +The regression rejects a 1e-5 height change and a 5e-15 opacity change. diff --git a/common/src/app/common/test_helpers/composable/comp/undo_check.cljc b/common/src/app/common/test_helpers/composable/comp/undo_check.cljc index ce9fe0bef6..dcd01cd0f0 100644 --- a/common/src/app/common/test_helpers/composable/comp/undo_check.cljc +++ b/common/src/app/common/test_helpers/composable/comp/undo_check.cljc @@ -34,6 +34,9 @@ a pinned phase that does not fail fails the case, so the mark goes only with the fix." (:require + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.math :as mth] [app.common.test-helpers.composable.core :as tm] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.normalize :as thn] @@ -166,13 +169,43 @@ (into (entities-signature :component (:components ed) (:components ad) #{} (constantly nil))))))) +(defn- same-state? + "Reflow can recompute geometry at machine precision after undo. Keep + snapshots exact, allowing only absolute geometry noise below 1e-10." + ([expected actual] (same-state? expected actual false)) + ([expected actual geometry?] + (or (= expected actual) + (cond + (and geometry? (number? expected) (number? actual)) + (mth/close? expected actual 1e-10) + + (and geometry? (gmt/matrix? expected) (gmt/matrix? actual)) + (every? #(same-state? (get expected %) (get actual %) true) [:a :b :c :d :e :f]) + + (and geometry? (gpt/point? expected) (gpt/point? actual)) + (every? #(same-state? (get expected %) (get actual %) true) [:x :y]) + + (and (map? expected) (map? actual) (= (set (keys expected)) (set (keys actual)))) + (every? (fn [key] + (same-state? (get expected key) (get actual key) + (or geometry? + (and (contains? expected :parent-id) + (contains? #{:x :y :width :height :rotation :points + :selrect :transform :transform-inverse} key))))) + (keys expected)) + + (and (sequential? expected) (sequential? actual) (= (count expected) (count actual))) + (every? true? (map #(same-state? %1 %2 geometry?) expected actual)) + + :else false)))) + (defn compare-states "Record a failure of `phase` at `step` unless the normalized files `expected` and `actual` are equal. Phases: `:undo`, `:redo`, `:no-undo-entry` (a step that added no entry changed the file), `:variant-undo` and `:variant-redo`." [situation step phase expected actual] - (if (= expected actual) + (if (same-state? expected actual) situation (add-failure situation {:phase phase :step step diff --git a/common/test/common_tests/logic/composable_runner_test.cljc b/common/test/common_tests/logic/composable_runner_test.cljc index 379c8de27b..e654a9d6e1 100644 --- a/common/test/common_tests/logic/composable_runner_test.cljc +++ b/common/test/common_tests/logic/composable_runner_test.cljc @@ -21,6 +21,20 @@ (t/use-fixtures :each thi/test-fixture) +(t/deftest undo-comparison-allows-only-negligible-geometry-noise + (let [situation (setup/simple-component-with-labeled-copy) + file (tm/file situation) + id (:id (tm/role-shape situation :copy-instance)) + path [:data :pages-index (:id (thf/current-page file)) :objects id] + before (-> (uc/snapshot file) + (assoc-in (conj path :height) 0.01) + (assoc-in (conj path :opacity) 0.4)) + failures #(uc/failures (uc/compare-states {} nil :undo before %))] + (t/is (empty? (failures (assoc-in before (conj path :height) 0.010000000000005116)))) + (t/is (seq (failures (assoc-in before (conj path :height) 0.01001)))) + (t/is (seq (failures (assoc-in before (conj path :opacity) (+ 0.4 5e-15))))) + (t/is (seq (failures (assoc-in before (conj path :touched) #{:geometry-group})))))) + (t/deftest undo-main-child-structure-restores-remote-sync-state (let [initial (setup/nested-component-with-copy) updated (-> initial