arthur/frontend/test/arthur/domain/channel_test.cljs
Your Name 815ce449ea Author corrections without baking them into motion
Constant, ramp, and return offsets now append ordinary channel layers to a lane or cel in explicit owner frames. The inspector exposes the commands as one undoable transaction, shows conflicts, and offers removal or retry while preserving generated bases through regeneration.

Ordered-stack compatibility is shared by validation, conflict reporting, and regeneration, including adjacent replacement coverage. Cel-sheet gaps and headers select their lane, so commands cannot fall through to another column's stale selection.

437 tests, 5,804 assertions; both browser flows; 56 Django tests; optimized frontend build.
2026-10-01 00:29:59 -04:00

452 lines
24 KiB
Clojure

(ns arthur.domain.channel-test
"The channel is the load-bearing claim of the whole model: analysis and a hand
produce the SAME data, and the only difference between them is a flag nothing
in the renderer reads. These assert the parts of that claim that could silently
stop being true."
(:require [cljs.test :refer [deftest is testing]]
[arthur.domain.channel :as ch]))
;; ---- the three shapes read the same way ----
(deftest framed-is-the-same-value-at-every-frame
(let [c (ch/framed :skin-dark)]
(is (= :framed (ch/describe c)))
(is (every? #(= :skin-dark (ch/value-at c % nil)) (range -5 20)))))
(deftest keyed-holds-until-the-next-key
;; Hold is the DEFAULT, not a special case: docs/design.md requires it of every
;; cut part, and a tweened mouth reads as puppet software.
(let [c (ch/keyed {0 :a, 4 :b, 12 :c} :hold)]
(is (= :keyed (ch/describe c)))
(is (= [:a :a :a :a :b :b :b :b :b :b :b :b :c :c]
(mapv #(ch/value-at c % nil) (range 0 14))))))
(deftest linear-vector-keys-interpolate-each-component
(let [c (ch/keyed {0 [0.4 0.6], 10 [0.6 0.4]} :linear)
frames [0 5 10 5 2]
cursor (ch/cursor c nil)]
(is (empty? (ch/problems c)))
(is (= [0.5 0.5] (ch/value-at c 5 nil)))
(is (= (mapv #(ch/value-at c % nil) frames)
(mapv #(ch/sample! cursor %) frames)))))
(deftest one-channel-can-cut-then-tween
(let [c (assoc (ch/keyed {0 [0 0], 4 [4 0], 8 [8 0]} :hold)
:segments {4 :linear})
cursor (ch/cursor c nil)]
(is (= [0 0] (ch/value-at c 2 nil)))
(is (= [4 0] (ch/value-at c 4 nil)))
(is (= [6 0] (ch/value-at c 6 nil)))
(is (= (mapv #(ch/value-at c % nil) [0 2 4 6 8 3 7])
(mapv #(ch/sample! cursor %) [0 2 4 6 8 3 7])))))
(deftest a-frame-before-the-first-key-reads-the-first-key
;; The JS activeKey clamps low, and that is kept: a channel's first key is the
;; pose the part starts in. Having NO value is a different question — it is a
;; state bit, not an empty region of the key map.
(let [c (ch/keyed {10 :a, 20 :b} :hold)]
(is (= :a (ch/value-at c 0 nil)))
(is (= :a (ch/value-at c 9 nil)))
(is (= :b (ch/value-at c 999 nil)) "and clamps high by holding the last key")))
(deftest keys-are-a-map-so-frame-order-in-the-literal-cannot-matter
;; Transit and JSON both lose sortedness, so the sorted index is built at read
;; time. A resolver that trusted insertion order would work in the REPL and
;; fail after a round trip through the server, which is the worst possible way
;; to find out.
(let [forward (ch/keyed (array-map 0 :a, 4 :b, 12 :c) :hold)
backward (ch/keyed (array-map 12 :c, 4 :b, 0 :a) :hold)
shuffled (ch/keyed (array-map 4 :b, 12 :c, 0 :a) :hold)]
(doseq [c [backward shuffled]]
(is (= (mapv #(ch/value-at forward % nil) (range 0 16))
(mapv #(ch/value-at c % nil) (range 0 16)))))))
(deftest dense-reads-one-value-per-frame-out-of-a-typed-array
(let [store {"blk" {:data (js/Int16Array. #js [0 0, 10 20, 30 40, 50 60]) :state nil}}
c {:animated? true :interp :hold
:dense {:store "blk" :offset 0 :stride 2 :frames 4}
:generated {:by :roto/lips-outer :analysis "sha256:test"}}]
(is (= :dense (ch/describe c)))
(is (= [[0 0] [10 20] [30 40] [50 60]]
(mapv (fn [f] (let [v (ch/value-at c f store)]
[(ch/component v 0) (ch/component v 1)]))
(range 4))))))
(deftest a-dense-value-is-a-view-not-a-copy
;; Fixed topology is what makes this possible — the frame's data is a
;; rectangular slice at a known offset — and a copy per node per frame is
;; exactly the allocation the model exists to avoid.
(let [data (js/Int16Array. #js [1 2 3 4])
store {"blk" {:data data :state nil}}
c {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 2}}
v (ch/value-at c 1 store)]
(is (= (.-buffer data) (.-buffer v)) "shares the block's buffer")))
(deftest a-dense-read-clamps-rather-than-running-off-the-end
;; A time map with an offset deliberately reads the future — mouth lead is the
;; entire reason :offset exists — so the last frames of a leading track ask for
;; frames past the end on every take. Clamping holds the final pose; the
;; alternative blanks the mouth at the end of every clip.
(let [store {"blk" {:data (js/Int16Array. #js [7 8 9]) :state nil}}
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}]
(is (= 7 (ch/value-at c -4 store)))
(is (= 9 (ch/value-at c 99 store)))))
(deftest a-dense-channel-whose-store-is-missing-says-so
(let [c {:animated? true :dense {:store "gone" :offset 0 :stride 1 :frames 1}}]
(is (thrown-with-msg? ExceptionInfo #"store key is not in the store"
(ch/value-at c 0 {})))))
;; ---- presence is not visibility ----
(deftest the-mask-carries-absence-and-vis-carries-hiding
;; An occluded subject has NO VALUE on a frame. A part being switched off is a
;; different question and it is `[:vis]`, a channel like any other. Two
;; mechanisms for one question is how you get a part hidden by one and shown by
;; the other, so the mask carries absence only.
(let [state (js/Uint8Array. #js [ch/present ch/absent-bit ch/present])
store {"blk" {:data (js/Int16Array. #js [1 2 3]) :state state}}
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}]
(is (= 1 (ch/value-at c 0 store)))
(is (= ch/absent (ch/value-at c 1 store)))
(is (= 3 (ch/value-at c 2 store)))
(is (ch/nothing? ch/absent))
(is (not (ch/nothing? 0)) "zero is a value, not an absence")
(is (not (ch/nothing? false)) "and so is false")))
(deftest the-mask-belongs-to-the-block-s-slice-and-not-to-frame-zero
;; A block is NODE-MAJOR, so one block holds several nodes' tracks and therefore
;; several mask regions. Indexing the mask by the frame alone reads the FIRST
;; track's absence for every track in the block — which is not a subtly wrong
;; pose, it is every part in the block vanishing on the frames where one of them
;; was occluded, and `demo/swarm` drew nothing at all for its first thirty-four
;; frames on account of it.
(let [nf 3
;; Track 0 absent on frame 1, track 1 absent on frame 2.
state (js/Uint8Array. #js [ch/present ch/absent-bit ch/present
ch/present ch/present ch/absent-bit])
store {"blk" {:data (js/Int16Array. #js [10 11, 20 21, 30 31,
40 41, 50 51, 60 61])
:state state}}
track (fn [i] {:animated? true
:dense {:store "blk" :offset (* i nf 2) :stride 2 :frames nf}})
read (fn [i f] (let [v (ch/value-at (track i) f store)]
(if (ch/nothing? v) :absent [(ch/component v 0) (ch/component v 1)])))]
(is (= [[10 11] :absent [30 31]] (mapv #(read 0 %) (range nf))))
(is (= [[40 41] [50 51] :absent] (mapv #(read 1 %) (range nf))))))
(deftest a-block-with-no-mask-is-present-throughout
;; The mask is optional: a generator that cannot fail to detect has nothing to
;; say, and allocating a zeroed byte per frame to say it would be noise.
(let [store {"blk" {:data (js/Int16Array. #js [1 2 3]) :state nil}}
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}]
(is (= [1 2 3] (mapv #(ch/value-at c % store) (range 3))))))
;; ---- the cursor is the playback path and must agree exactly ----
(defn- via-cursor
"Sample one cursor at each of `fs` in the order given, which is the point: a
cursor carries state between calls."
[c fs]
(let [cur (ch/cursor c nil)]
(mapv #(ch/sample! cur %) fs)))
(deftest the-cursor-agrees-with-the-specification-in-any-frame-order
;; This is the assertion the cursor exists for. A cursor that drifts produces
;; the WRONG POSE rather than an error, so nothing would report it: the mouth
;; would simply be a beat behind on some frames and not others, which reads as
;; a bad take.
(doseq [[label c] [["sparse" (ch/keyed {0 :a, 4 :b, 12 :c, 13 :d, 40 :e} :hold)]
["one key" (ch/keyed {7 :only} :hold)]
["dense-ish" (ch/keyed (into {} (map (juxt identity #(* 10 %))) (range 40)) :hold)]
["framed" (ch/framed :static)]
;; A stacked channel has a reading head per key map, and a
;; layer's head is asked for nothing at all across the long
;; stretches outside its support — then asked again. That is
;; the drift this test exists to catch, now squared.
["corrected"
(assoc (ch/keyed {0 0, 20 200, 44 440} :linear)
:over [(ch/layer :a [10 20] :offset
(ch/keyed {10 1, 19 90} :linear))])]
["corrected twice, and over a constant"
(assoc (ch/keyed {0 0, 30 300} :linear)
:over [(ch/layer :a [5 15] :offset (ch/framed 7))
(ch/layer :b [12 25] :replace
(ch/keyed {12 -1, 24 -12} :linear))
(ch/layer :c [20 44] :offset
(ch/keyed {20 0, 43 23} :linear))])]
["a corrected framed base"
(assoc (ch/framed 3)
:over [(ch/layer :a [8 36] :offset
(ch/keyed {8 0, 35 27} :linear))])]]]
(let [spec #(ch/value-at c % nil)
forward (range 0 45)
back (reverse forward)
jumpy [0 44 1 43 12 12 13 3 40 7 0 22 22 21 44]]
(testing label
(doseq [[order-name fs] [["forward" forward] ["backward" back] ["random access" jumpy]]]
(is (= (mapv spec fs) (via-cursor c fs))
(str label " / " order-name)))))))
(deftest a-fixed-point-block-decodes-through-its-own-scale
;; `:scale` is in the block HEADER and not agreed by convention, because a block
;; in image-height units and a block in stage pixels need different ones to fill
;; an Int16 usefully. It is what lets the block in memory be byte for byte the
;; block on the wire, which a handle naming a sha256 requires.
(let [store {"blk" {:data (js/Int16Array. #js [16384 -8192, 4096 32767]) :state nil}}
wide {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 2
:scale 16384}}
thin {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 4
:scale 16384}}]
(is (= [[1.0 -0.5] [0.25 (/ 32767 16384)]]
(mapv (fn [f] (let [v (ch/value-at wide f store)]
[(ch/component v 0) (ch/component v 1)]))
(range 2))))
(is (= [1.0 -0.5 0.25] (mapv #(ch/value-at thin % store) (range 3)))
"a stride-1 block decodes to a number and needs no buffer")
(is (seq (ch/problems (assoc-in wide [:dense :scale] 0)))
"a scale of zero divides every value in the block by zero")
(is (seq (ch/problems (assoc-in wide [:dense :scale] -16384))))))
(deftest the-cursor-decodes-into-a-buffer-it-owns-and-agrees-with-the-spec
;; Decoding costs the subarray view, so the reader owns one destination per
;; channel — the same bargain the resolver makes with its point buffers, and it
;; carries the same contract: a value has to be CONSUMED before the next frame
;; is asked for, because the next read overwrites it.
(let [store {"blk" {:data (js/Int16Array. #js [16384 0, 0 16384, -16384 0]) :state nil}}
c {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 3
:scale 16384}}
cur (ch/cursor c store)
pair (fn [v] [(ch/component v 0) (ch/component v 1)])]
(is (= (mapv #(pair (ch/value-at c % store)) (range 3))
(mapv #(pair (ch/sample! cur %)) (range 3))))
(let [held (ch/sample! cur 0)]
(ch/sample! cur 2)
(is (= [-1.0 0.0] (pair held))
"the buffer is reused, which is the contract and not a bug"))))
(deftest the-cursor-reads-a-dense-block-too
(let [store {"blk" {:data (js/Float32Array. #js [1 2 3 4 5]) :state nil}}
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 5}}
cur (ch/cursor c store)]
(is (= [1 2 3 4 5] (mapv #(ch/sample! cur %) (range 5))))
(is (= [5 1] (mapv #(ch/sample! cur %) [4 0])) "and seeks")))
;; ---- correction layers ----
(defn- corrected
"A base channel with layers over it."
[base & layers]
(assoc base :over (vec layers)))
(deftest a-correction-applies-only-over-its-support
;; THE property. A range says where an edit applies, so outside it the
;; underlying animation must evaluate exactly as it did before — which is what
;; makes a bounded correction different from inserting boundary keys, and the
;; reason the stack exists at all.
(let [base (ch/keyed {0 0, 10 100} :linear)
c (corrected base (ch/layer :nudge [3 6] :offset (ch/framed 5)))
plain (mapv #(ch/value-at base % nil) (range 11))
with (mapv #(ch/value-at c % nil) (range 11))]
(is (= (concat (take 3 plain) [35 45 55] (drop 6 plain)) with))
(is (= (assoc plain 3 35 4 45 5 55) with) "and nothing else moved at all")))
(deftest a-return-motion-is-a-keyed-layer-and-a-constant-is-a-framed-one
;; The three commands the lane model asks for over one selected range, and
;; none of them needs a new way to say what a value is over time.
(let [base (ch/keyed {0 0} :hold)
at (fn [c] (mapv #(ch/value-at c % nil) (range 6)))]
(is (= [0 0 10 10 10 0]
(at (corrected base (ch/layer :flat [2 5] :offset (ch/framed 10)))))
"a constant adjustment")
(is (= [0 0 0 5 10 0]
(at (corrected base (ch/layer :ramp [2 5] :offset
(ch/keyed {2 0, 4 10} :linear)))))
"a ramp")
(is (= [0 0 0 10 0 0]
(at (corrected base (ch/layer :return [2 5] :offset
(ch/keyed {2 0, 3 10, 4 0} :linear)))))
"and a return motion, which is three samples and no new mechanism")))
(deftest the-stack-is-ordered-and-replace-wins-where-it-covers
(let [base (ch/keyed {0 1} :hold)
two (fn [a b] (mapv #(ch/value-at (corrected base a b) % nil) (range 4)))
add2 (ch/layer :a [0 4] :offset (ch/framed 2))
put9 (ch/layer :b [1 3] :replace (ch/framed 9))]
(is (= [3 9 9 3] (two add2 put9)) "the later layer sees the earlier one's result")
(is (= [3 11 11 3] (two put9 add2)) "and order therefore matters")))
(deftest validation-follows-the-shape-produced-by-an-ordered-stack
(let [base (ch/framed [0 0])
put3 (ch/layer :put3 [0 2] :replace (ch/framed [1 2 3]))
add2 (ch/layer :add2 [0 2] :offset (ch/framed [1 1]))
add3 (ch/layer :add3 [0 2] :offset (ch/framed [1 1 1]))]
(is (seq (ch/problems (corrected base put3 add2)))
"validation rejects a stack that would throw while reading")
(is (empty? (ch/problems (corrected base put3 add3)))
"a covering replacement establishes the shape seen by later layers")
(is (= [2 3 4] (ch/value-at (corrected base put3 add3) 0 nil)))))
(deftest stack-compatibility-follows-adjacent-replacements-and-skips-conflicts
(let [base (ch/framed [0 0])
left (ch/layer :left [0 2] :replace (ch/framed [1 2 3]))
right (ch/layer :right [2 4] :replace (ch/framed [4 5 6]))
add3 (ch/layer :add [0 4] :offset (ch/framed [1 1 1]))
valid (corrected base left right add3)
broken (corrected base (assoc left :conflict "skip it") right add3)]
(is (empty? (ch/problems valid))
"adjacent replacements jointly prevent the base shape reaching the offset")
(is (seq (ch/problems broken))
"a conflicted replacement is absent from the effective stack")
(is (empty? (ch/conflicts valid)))
(is (= [:left :add] (mapv :id (ch/conflicts broken))))))
(deftest reconciliation-recomputes-the-complete-stack-in-order
(let [old (assoc (ch/framed [0 0]) :over
[(assoc (ch/layer :put [0 3] :replace (ch/framed [1 2 3]))
:conflict "old")
(ch/layer :add [0 3] :offset (ch/framed [1 1 1]))])
layers (ch/reconcile old)]
(is (nil? (:conflict (first layers))) "a stale mark is cleared")
(is (nil? (:conflict (second layers))) "the later offset sees that replacement")
(is (= [2 3 4] (ch/value-at (assoc old :over layers) 1 nil)))))
(deftest generated-base-time-and-authored-correction-time-can-differ
(let [c (corrected (ch/keyed {0 0, 2 20} :hold)
(ch/layer :nudge [1 2] :offset (ch/framed 3)))
cursor (ch/cursor c nil)]
(is (= 3 (ch/value-at c 0 1 nil)))
(is (= 3 (ch/sample! cursor 0 1)))
(is (= 23 (ch/value-at c 2 1 nil)))
(is (= 20 (ch/sample! cursor 2 0)))))
(deftest replace-can-supply-a-value-where-offset-has-nothing-to-add-to
;; An absent value is not zero. `replace` states a pose; `offset` cannot
;; offset a pose that was never measured, and must not invent one.
(let [gone (ch/keyed {} :hold)]
(is (ch/nothing? (ch/value-at gone 0 nil)))
(is (ch/nothing? (ch/value-at (corrected gone (ch/layer :o [0 2] :offset (ch/framed 5))) 0 nil)))
(is (= 5 (ch/value-at (corrected gone (ch/layer :r [0 2] :replace (ch/framed 5))) 0 nil)))
;; And outside the support it is still absent, not the layer's value.
(is (ch/nothing? (ch/value-at (corrected gone (ch/layer :r [0 2] :replace (ch/framed 5))) 7 nil)))))
(deftest a-correction-offsets-geometry-component-wise-over-a-dense-base
;; The base a correction matters most for is generated, and a dense value is a
;; VIEW onto the block: offsetting must not write into it.
(let [data (js/Int16Array. #js [10 20, 30 40])
store {"blk" {:data data :state nil}}
base {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 2}}
c (corrected base (ch/layer :nudge [1 2] :offset (ch/framed [5 -5])))]
(is (= [[10 20] [35 35]]
(mapv (fn [f] (let [v (ch/value-at c f store)]
[(ch/component v 0) (ch/component v 1)]))
(range 2))))
(is (= [10 20 30 40] (vec data)) "the block itself is untouched")
(is (thrown-with-msg?
ExceptionInfo #"different shape"
(ch/value-at (corrected base (ch/layer :bad [0 2] :offset (ch/framed 1))) 0 store))
"a correction of the wrong shape is loud, not silently dropped")))
(deftest a-malformed-correction-is-reported-rather-than-read
(let [ok (ch/layer :a [0 2] :offset (ch/framed 1))
base (ch/keyed {0 1} :hold)]
(is (empty? (ch/problems (corrected base ok))))
(is (seq (ch/problems (assoc base :over (list ok)))) "an ordered stack is a vector")
(doseq [[label bad] [["no id" (dissoc ok :id)]
["backwards support" (assoc ok :support [5 2])]
["open-ended support" (assoc ok :support [0 ##Inf])]
["a support that is not a pair" (assoc ok :support 3)]
["an unknown op" (assoc ok :op :multiply)]
["no values" (dissoc ok :values)]
["values that are not a channel" (assoc ok :values {:keys {0 1}})]
["layers under a layer"
(assoc ok :values (corrected base ok))]]]
(is (seq (ch/problems (corrected base bad))) label))))
(deftest an-empty-over-is-fine-and-is-what-scenes-carry
(is (empty? (ch/problems (ch/keyed {0 1} :hold))))
(is (= 1 (ch/value-at (ch/keyed {0 1} :hold) 0 nil))))
;; ---- shape validation ----
(deftest problems-names-the-ways-a-channel-is-malformed
(is (empty? (ch/problems (ch/framed 1))))
(is (empty? (ch/problems (ch/keyed {0 1} :hold))))
(testing "keys as a vector is the mistake most worth catching"
(is (seq (ch/problems {:animated? true :keys [[0 1]]}))))
(is (seq (ch/problems {:value 1})) "no :animated?")
(is (seq (ch/problems {:animated? true})) "animated with nothing to read")
(is (seq (ch/problems {:animated? true :keys {0 1}
:dense {:store "x" :offset 0 :stride 1 :frames 1}}))
"one shape at a time")
(is (empty? (ch/problems (ch/keyed {0 0.0, 10 1.0} :linear)))
"scalar controls can interpolate")
(is (seq (ch/problems (ch/keyed {0 :a, 10 :b} :linear)))
"a cut or tone cannot interpolate"))
(deftest numeric-channels-can-ramp-between-keys
(let [c (ch/keyed {0 0.0, 10 1.0} :linear)
cursor (ch/cursor c nil)]
(is (= [0.0 0.5 1.0 1.0]
(mapv #(ch/value-at c % nil) [0 5 10 15])))
(is (= [0.0 0.5 1.0 0.2]
(mapv #(ch/sample! cursor %) [0 5 10 2])))))
(deftest component-reads-vectors-and-typed-arrays-the-same-way
(is (= 3 (ch/component [3 4] 0)))
(is (= 4 (ch/component [3 4] 1)))
(is (= 3 (ch/component (js/Int16Array. #js [3 4]) 0)))
(is (= 4 (ch/component (js/Int16Array. #js [3 4]) 1))))
(deftest generated-is-provenance-and-nothing-reads-it
;; The flag lives on the CHANNEL, not the node, because a mouth wants a
;; rotoscoped [:geom :pts] and a hand-animated [:xform :pos] at the same time.
;; What this asserts is that sampling does not depend on it: strip :generated
;; and every frame is identical.
(let [store {"blk" {:data (js/Int16Array. #js [1 2 3]) :state nil}}
base {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}
with (assoc base :generated {:by :roto/lips-outer :params {:verts 8}})]
(is (= (mapv #(ch/value-at base % store) (range 3))
(mapv #(ch/value-at with % store) (range 3))))))
(deftest a-correction-the-base-outgrew-is-a-conflict-and-not-a-problem
;; What a topology change does: a re-freeze gives the mouth a different number
;; of points, and a correction that was right when it was made can no longer
;; be added component by component. That is not a broken document — it is a
;; decision waiting for a person.
(let [pts (fn [n] {:animated? true :interp :hold :keys {0 (vec (repeat n 1))}})
nudge (ch/layer :nudge [0 4] :offset (ch/framed [1 1 1 1]))
fits (assoc (pts 4) :over [nudge])
outgrown (assoc (pts 6) :over [nudge])]
(is (nil? (ch/conflict-with (pts 4) nudge)))
(is (empty? (ch/conflicts fits)))
(is (empty? (ch/problems fits)))
(is (re-find #"different shape" (ch/conflict-with (pts 6) nudge)))
;; Unrecorded, it is an authoring bug and says so.
(is (seq (ch/problems outgrown)))
(is (= [:nudge] (mapv :id (ch/conflicts outgrown))))
;; Recorded, the document is sound and the correction is simply not applied.
(let [marked (assoc (pts 6) :over [(assoc nudge :conflict "outgrown")])]
(is (empty? (ch/problems marked)) "a recorded conflict is not a reason not to load")
(is (= [:nudge] (mapv :id (ch/conflicts marked))))
(is (= [1 1 1 1 1 1] (vec (ch/value-at marked 0 nil)))
"the base alone — neither misapplied nor silently dropped")
(is (= (ch/value-at marked 0 nil) (first (via-cursor marked [0])))
"and the cursor skips it too"))))
(deftest replace-never-conflicts-and-an-opaque-value-cannot-be-offset
(let [flag (ch/keyed {0 true} :hold)]
(is (nil? (ch/conflict-with flag (ch/layer :r [0 2] :replace (ch/framed false))))
"replace states a whole value, so it has nothing to agree with")
(is (re-find #"not a number" (ch/conflict-with flag (ch/layer :o [0 2] :offset (ch/framed 1)))))
(is (nil? (ch/conflict-with (ch/keyed {} :hold) (ch/layer :o [0 2] :offset (ch/framed 1))))
"an empty key map is not a disagreement")))
(deftest the-shape-of-a-channels-values-is-readable-without-sampling-it
(is (= :scalar (ch/value-shape (ch/framed 3))))
(is (= 2 (ch/value-shape (ch/framed [1 2]))))
(is (= :scalar (ch/value-shape {:animated? true :dense {:stride 1}})))
(is (= 40 (ch/value-shape {:animated? true :dense {:stride 40}})))
(is (= 2 (ch/value-shape (ch/keyed {0 [1 2], 4 [3 4]} :linear))))
(is (= :opaque (ch/value-shape (ch/keyed {0 :a} :hold))))
(is (nil? (ch/value-shape (ch/keyed {} :hold))) "nothing to read it off"))