alonso.torres 80b3cbfa7a ✨ Generate component programs across library files
Build a shared library world and route edits, updates and settlement across
working and library files. Check the independent override model, geometry,
propagation and exact undo for both files. Replay shrunk library failures
through the async frontend with the same target choices and barriers.

Keep common generation/shrinking and CLJS replay as the main regression
suite. Use the plugin for public API, WASM and measured-text smoke checks;
retain its existing fixed cases until equivalent coverage replaces them.

All four properties pass 5000 programs each with seed712, each in a full
run. Fresh swaps: 5009 assertions; library: 5008. Isolated Stage3: 55/379.
Live WASM, fonts, undo/redo and later propagation checks pass.
Full frontend: 1539 tests, 9312 assertions, including 39 generated replays.
Common JVM: 1851 tests, 30684 assertions; JS: 1782 tests, 29308 assertions.
Backend: 1610 tests, 8104 assertions; exporter: 14 tests, 32 assertions.
Plugin: 68 cases. Lint, formatting and unchanged goldens pass.

AI-assisted-by: GPT-6
2026-10-28 15:53:10 +01:00

339 lines
19 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.composable-tests.generated-test
(:require
[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]
[app.common.test-helpers.composable.oracle :as oracle]
[app.common.test-helpers.normalize :as thn]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[clojure.test.check.generators :as gen]
[frontend-tests.composable-tests.interpreter :as ftm]
[frontend-tests.helpers.mock :as mock]))
(t/use-fixtures :each ftm/fixture)
(defn- live-model
[state live]
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/forest live))]
(into {}
(keep (fn [{:keys [path shape]}]
(when-let [groups (get (:model state) (:id shape))]
(when-let [id (get by-path path)] [id groups]))))
(cg/forest (:situation state)))))
(defn- ^:async replay-program
"Replay a JVM-shrunk EDN program against the async frontend. The pure
runner chooses targets independently and both must make the same choices."
[{:keys [world ops] :as program}]
(try
(let [initial (cg/initial-state world)]
(loop [state initial
live (ftm/start-generated (:situation initial))
tuples ops]
(if-let [tuple (first tuples)]
(let [state (cg/settle-state state)
pure (:situation state)
live (ftm/refresh-generated live)
expected (cg/resolve-op pure (:model state) tuple)
actual (if (#{:undo :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 (if-let [op (:op actual)]
(await (ftm/run-generated-step live op))
(do
(t/is (= :settled (await (ftm/await-step []))))
(ftm/refresh-generated live)))
next-state (-> state (cg/step tuple) (cg/settle-state))]
(when-not next-live
(throw (ex-info "Frontend step failed" {:program program :tuple tuple})))
(try
(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))))
(let [expected (oracle/canonical (:situation next-state))
actual (oracle/canonical next-live)]
(t/is (true? (oracle/equivalent? expected actual))
(str "Backend mismatch at " tuple "; replay " (pr-str program)
"; diff " (pr-str (thn/diff expected actual)))))
(t/is (empty? (uc/failures next-live)) (pr-str (uc/failures next-live)))
(recur next-state next-live (rest tuples))))
(let [checked (await (ftm/finish-generated live))]
(t/is (empty? (uc/failures checked)) (pr-str (uc/failures checked)))
(ftm/uninstall!)))))
(catch :default error
(ftm/uninstall!)
(t/is false (str (ex-message error) "\nReplay: " (pr-str program)
"\nDetails: " (pr-str (ex-data error)))))))
(defn ^:async replay
[program]
(await (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock
rx/timer mock/timer-mock}
(await (replay-program program)))))
(t/deftest ^:async seeded-programs-replay-through-the-frontend
(doseq [seed [712 20261022 1792691]]
(await (replay (gen/generate cg/program-gen 20 seed)))))
(t/deftest ^:async detach-undo-shrunk-regression
(await (replay {:world [2 0] :ops [[:edit 0 0 1 1] [:detach 0] [:undo] [:redo]]})))
(t/deftest ^:async structure-programs-replay-through-the-frontend
(doseq [seed [712 20261022]]
(await (replay (gen/generate cg/structure-program-gen 10 seed)))))
(t/deftest ^:async nested-text-rotation-shrunk-regression
(await (replay {:world [2 0 {:text true}]
:ops [[:nest 0] [:detach 0] [:rotate 0]]})))
(t/deftest ^:async nesting-restores-default-constraints-on-undo
(await (replay {:world [2 0] :ops [[:nest 0]]})))
(t/deftest ^:async swap-programs-replay-through-the-frontend
(doseq [seed [712 20261022]]
(await (replay (gen/generate cg/swap-program-gen 5 seed)))))
(t/deftest ^:async library-programs-replay-through-the-frontend
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:sync 1]]}))
(await (replay (gen/generate cg/library-program-gen 5 712))))
(t/deftest ^:async inherited-rotation-switch-shrunk-regression
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:rotate 0 0 0 0] [:switch 1 0 0 0]]})))
(t/deftest ^:async swapped-subtree-update-main-shrunk-regression
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:resize 0 0 0 0] [:duplicate 0 0 0 0]
[:switch 2 0 0 0] [:update-main 2 0 0 0]]})))
(t/deftest ^:async main-head-detach-shrunk-regression
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 1 0 0 0] [:detach 0 0 0 0] [:delete 0 0 0 0]]})))
(t/deftest ^:async partial-sync-geometry-boundary-shrunk-regression
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:undo 0 0 0 0]
[:add-child 17 0 0 0] [:rotate 19 0 0 0] [:undo 0 0 0 0]]})))
(t/deftest ^:async update-main-after-nesting-a-rotated-text-copy-undoes-together
(await (replay {:world [2 0 {:text true}]
:ops [[:duplicate 0] [:nest 0] [:rotate 0 0 0]
[:edit 0 0 1 1] [:update-main 0]]})))
(t/deftest ^:async updating-main-then-swapping-a-child-restores-parent-state
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 5 0 0 0] [:update-main 0 0 0 0] [:swap 13 0 0 0]]})))
(t/deftest ^:async swapping-main-subtrees-with-several-children-undoes-their-order
(await (replay {:world [2 2 {:variants true :variant-diff false}]
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1]
[:update-main 0] [:sync 1] [:add-child 1 0 0 0]
[:add-child 1 0 0 0] [:add-child 1 0 0 0]
[:swap 0 0 0 0] [:swap 0 0 0 0] [:text 0 0 0 0]]})))
(t/deftest ^:async updating-a-rotated-outer-main-composes-authored-child-orientation
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:duplicate 0 0 0 0]
[:resize 0 0 0 0] [:nest 0 0 0 0] [:rotate 4 0 0 0]
[:detach 3 0 0 0] [:update-main 4 0 0 0]]})))
(t/deftest ^:async updating-a-duplicated-switched-copy-restores-the-main-order
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 1 0 0 0] [:duplicate 0 0 0 0]
[:update-main 0 0 0 0] [:redo 0 0 0 0]]})))
(t/deftest ^:async switching-a-detached-rotated-copy-keeps-its-resize
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:rotate 1 0 0 0] [:detach 1 0 0 0]
[:detach 2 0 0 0] [:resize 2 0 0 0] [:switch 2 0 0 0]]})))
(t/deftest ^:async minimum-size-parent-orientation-does-not-amplify-oracle-drift
(await (replay {:world [3 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:edit 4 0 0 0]
[:duplicate 3 0 0 0] [:nest 10 0 0 0] [:rotate 9 0 0 0]
[:update-main 7 0 0 0] [:delete 0 0 0 0]]})))
(t/deftest ^:async detaching-a-rotated-parent-inherits-geometry-in-the-copy-frame
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:resize 0 0 0 0]
[:delete 0 0 0 0] [:rotate 0 0 0 0] [:detach 0 0 0 0]]})))
(t/deftest ^:async switching-a-copy-of-a-swap-keeps-position-overrides-inherited
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 13 0 0 0] [:rotate 0 0 0 0] [:switch 2 0 0 0]]})))
(t/deftest ^:async rotated-swapped-head-keeps-its-position-anchor-with-a-different-content-size
(await (replay {:world [2 2 {:variants true :variant-diff false}]
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0]
[:undo] [:redo] [:reset 0] [:edit 0 1 1 1] [:update-main 0]
[:sync 1] [:swap 17 0 0 0] [:resize 0 0 0 0] [:rotate 0 0 0 0]]})))
(t/deftest ^:async pushing-a-swap-keeps-the-orientation-inherited-from-its-position
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:rotate 0 0 0 0] [:swap 13 0 0 0] [:update-main 1 0 0 0]]})))
(t/deftest ^:async resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
(await (replay {:world [3 0 {:text true :layout :flex}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:remove-child 0 0 0 0]
[:reset 0 0 0 0] [:nest 0 0 0 0] [:resize 0 0 0 0]]})))
(t/deftest ^:async updating-a-main-after-successive-rotations-keeps-child-flips
(await (replay {:world [2 1]
:ops [[:rotate 1 0 0 0] [:rotate 1 0 1 0] [:update-main 0 0 0 0]]})))
(t/deftest ^:async pushing-a-switched-child-uses-its-parents-pushed-frame
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 5 0 0 0] [:rotate 1 0 0 0] [:update-main 0 0 0 0]]})))
(t/deftest ^:async minimum-size-parent-placement-keeps-fixed-child-dimensions
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:resize 1 0 0 0]
[:nest 2 0 0 0] [:rotate 0 0 3 0] [:edit 0 0 0 0]]})))
(t/deftest ^:async generated-library-pushed-swap-authors-content-against-its-new-position
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 2 0 0 0] [:edit 8 0 0 0] [:switch 0 0 0 0]
[:update-main 7 0 0 0]]})))
(t/deftest ^:async generated-library-swap-back-syncs-the-near-positions-overrides
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 15 0 0 0] [:duplicate 16 0 0 0] [:edit 8 0 0 0]
[:switch 4 0 0 0] [:update-main 0 0 0 0]]})))
(t/deftest ^:async generated-library-move-restores-existing-component-nesting-on-undo
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 5 0 0 0] [:nest 8 0 0 0] [:nest 5 0 0 0]
[:restore 0 0 0 0]]})))
(t/deftest ^:async generated-library-authored-child-follows-its-parents-synced-turn
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:resize 0 0 0 0] [:switch 2 0 0 0] [:rotate 1 0 0 0]
[:remove-child 0 0 0 0]]})))
(t/deftest ^:async generated-library-switch-keeps-overrides-from-a-deleted-near-main
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:delete 0 0 0 0] [:delete 3 0 0 0] [:resize 0 0 0 0]
[:delete 9 0 0 0] [:switch 0 0 0 0]]})))
(t/deftest ^:async switching-an-untouched-rotated-copy-survives-full-sync
(await (replay {:world [3 2 {:variants true :variant-diff false}]
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
[:edit 11 0 0 0] [:rotate 2 0 0 0] [:nest 3 0 0 0] [:resize 3 0 0 0]
[:nest 3 0 0 0] [:swap 10 0 0 0] [:resize 3 0 0 0]
[:update-main 5 0 0 0] [:switch 0 0 0 0] [:detach 0 0 0 0]]})))
(t/deftest ^:async switch-keeps-retained-orientation-together-with-carried-size
(await (replay {:world [2 2 {:variants true :variant-diff false}]
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
[:swap 0 0 0 0] [:delete 0 0 0 0] [:rotate 0 0 0 0]
[:nest 0 0 0 0] [:detach 0 0 0 0] [:update-main 0 0 0 0]
[:switch 0 0 0 0]]})))
(t/deftest ^:async generated-library-duplicate-refuses-an-expanding-component-branch
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 5] [:nest 8] [:duplicate 1]]})))
(t/deftest ^:async generated-library-parent-turn-settles-swapped-placement
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 2 0 0 0] [:resize 0 0 0 0] [:rotate 2 0 0 0]]})))
(t/deftest ^:async generated-library-restored-variant-syncs-its-converted-name
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:delete 3 0 0 0] [:delete 1 0 0 0] [:restore 7 0 0 0]
[:delete 1 0 0 0] [:restore 0 0 0 0]]})))
(t/deftest ^:async generated-library-switch-inherits-a-swapped-near-mains-authored-fill
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:edit 12 0 0 0] [:nest 10 0 0 0]
[:switch 7 0 0 0] [:switch 2 0 0 0]]})))
(t/deftest ^:async generated-library-reset-places-new-children-after-clearing-rotation
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:edit 0 0 1 2] [:update-main 0] [:library-sync]
[:undo] [:redo] [:reset 0] [:sync 1]
[:switch 1 0 0 0] [:rotate 1 0 0 0] [:duplicate 0 0 0 0]
[:reset 2 0 0 0] [:redo 0 0 0 0]]})))
(t/deftest ^:async generated-update-main-settles-a-turned-swapped-position
(await (replay {:world [2 2 {:variants true :variant-diff false}]
:ops [[:edit 0 0 1 1] [:switch 0] [:switch 0] [:swap 0] [:undo] [:redo]
[:reset 0] [:edit 0 1 1 1] [:update-main 0] [:sync 1]
[:delete 8 0 0 0] [:swap 15 0 0 0] [:edit 14 0 0 0]
[:resize 4 0 0 0] [:resize 0 0 0 0] [:nest 4 0 0 0]
[:rotate 5 0 0 0] [:update-main 14 0 0 0] [:duplicate 0 0 0 0]]})))