;;; ebox-child-range-tests.el --- Persistent child sequence tests -*- lexical-binding: t; -*- (require 'ert) (require 'ebox-child-range) (require 'ebox) ;; This suite locks child-range and Elisp projection semantics. Native frame ;; execution is verified separately and must not replace the named fallback ;; strategies merely because a developer has built the optional module. (defvar ebox-native-reflow-module-path) (setq ebox-native-reflow-module-path nil) (defconst ebox-child-range-test--root (expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name))) "Ebox repository root for source audits.") (defun ebox-child-range-test--item (key) "Return one declarative test item for KEY." (list :key key :value key)) (defun ebox-child-range-test--node-key (item &optional source-index) "Return ITEM's key from SOURCE-INDEX or its low-level fixture field." (or (and (ebox-canonical-input-p item) (ebox-tree-node-author-key (ebox-test-source-index item) (ebox-test-root item))) (and source-index (ebox-tree-node-source-handle item) (ebox-tree-node-author-key source-index item)) (plist-get item :key))) (defalias 'ebox-child-range-test--key #'ebox-child-range-test--node-key) (defun ebox-child-range-test--input (items) "Combine canonical fixture ITEMS into one Range replacement input." (apply #'ebox-test-forest-input items)) (defun ebox-child-range-test--host-ref-table (state) "Return STATE's source-owned Host reference index." (ebox-source--index-host-ref-table-view (plist-get state :source-index))) (defun ebox-child-range-test--build (segments &optional hash-function) "Build low-level SEGMENTS with the fixture key contract." (ebox-child-range--build segments #'ebox-child-range-test--key hash-function)) (defun ebox-child-range-test--replace (sequence ref items) "Replace REF ITEMS using the fixture key contract." (ebox-child-range--replace sequence ref items #'ebox-child-range-test--key)) (defun ebox-child-range-test--fixture (static-count range-items &optional hash) "Build STATIC-COUNT static segments followed by RANGE-ITEMS." (ebox-child-range-test--build (append (cl-loop for index below static-count collect (cons nil (list (ebox-child-range-test--item (list 'static index))))) (list (cons 'range (mapcar #'ebox-child-range-test--item range-items)))) hash)) (ert-deftest ebox-child-range-reuse-map-rebinds-published-objects () "Stable slots use published objects; moved slots re-enter normal indexing." (let* ((sequence (ebox-child-range-test--fixture 0 '(one two))) (segment (ebox-child-range--lookup-ref sequence 'range)) (payload (ebox-child-range--segment-payload segment)) (first (aref payload 0)) (second (aref payload 1))) (plist-put first :node-id 1) (plist-put second :node-id 2) (plist-put first :ebox-sequence-location '(:parent-node-id 99 :segment-index 0 :offset 0)) (plist-put second :ebox-sequence-location '(:parent-node-id 99 :segment-index 0 :offset 1)) (pcase-let ((`(,items ,reuse-map) (ebox-incremental--candidate-canonical-range-reuse sequence 'range (list (copy-tree first) (copy-tree second)) '((0 . 0) (1 . 1)) 99 0))) (should (eq (nth 0 items) first)) (should (eq (nth 1 items) second)) (should (equal reuse-map '((0 . 0) (1 . 1))))) (pcase-let ((`(,items ,reuse-map) (ebox-incremental--candidate-canonical-range-reuse sequence 'range (list second first) '((0 . 1) (1 . 0)) 99 0))) (should-not (eq (nth 0 items) second)) (should-not (eq (nth 1 items) first)) (should-not (plist-get (nth 0 items) :node-id)) (should-not (plist-get (nth 1 items) :node-id)) (should-not reuse-map)) (should-error (ebox-incremental--candidate-canonical-range-reuse sequence 'range (list (copy-tree first)) '((1 . 0)) 99 0)))) (ert-deftest ebox-child-range-render-signature-snapshots-item-identities () "A cached Range signature must not follow later runtime identity mutation." (let* ((item-input (ebox-test-box (ebox-test-text "item") :key 'item)) (item (ebox-test-root item-input)) (source-index (ebox-test-source-index item-input)) (sequence (ebox-child-range--build (list (cons 'range (list item))) (lambda (node) (ebox-tree-node-author-key source-index node)))) (published (aref (ebox-child-range--segment-payload (ebox-child-range--lookup-ref sequence 'range)) 0)) (signature (ebox--render-cache-value-signature sequence))) (plist-put published :node-id 1) (plist-put published :region-id 10) (setq signature (ebox--render-cache-value-signature sequence)) (plist-put published :region-id 20) (should-not (equal signature (ebox--render-cache-value-signature sequence))) (should (equal 10 (plist-get (cadr signature) :region-id))))) (ert-deftest ebox-child-range-gate-a-exact-segment-and-payload-costs () "Point replacement cost follows trie height, never parent child count." (dolist (case '((0 0 1) (10 1 2) (100 2 3) (500 2 3))) (pcase-let* ((`(,static-count ,height ,bound) case) (old (ebox-child-range-test--fixture static-count '(old))) (old-flat (ebox-child-range--flatten old)) (result (ebox-child-range-test--replace old 'range (mapcar #'ebox-child-range-test--item '(new-a new-b)))) (new (car result)) (metrics (cdr result))) (should (= (ebox-child-range--sequence-count old) (1+ static-count))) (should (= (ebox-child-range--sequence-height old) height)) (should (= (ebox-child-range--segment-node-segment-count (ebox-child-range--sequence-root old)) (1+ static-count))) (should (= (ebox-child-range--segment-node-weight (ebox-child-range--sequence-root old)) (1+ static-count))) (should (= (ebox-child-range--prefix-weight old static-count) static-count)) (should (= (ebox-child-range--rank old static-count) static-count)) (pcase-let ((`(,nodes . ,edges) (ebox-child-range--segment-memory old))) (should (<= nodes (1+ (* (1+ static-count) (1+ height))))) (should (= edges (1- nodes)))) (pcase-let ((`(,vectors . ,slots) (ebox-child-range--segment-vector-memory old))) (should (= slots (* vectors 32))) (should (<= vectors (1+ (ceiling (/ (float (1+ static-count)) 32)))))) (should (<= (ebox-child-range--metrics-segment-visits metrics) bound)) (should (<= (ebox-child-range--metrics-segment-copies metrics) bound)) (should (= (ebox-child-range--metrics-old-affected-payload-visits metrics) 1)) (should (= (ebox-child-range--metrics-new-payload-visits metrics) 2)) (should (= (ebox-child-range--metrics-new-payload-validations metrics) 2)) (should (= (ebox-child-range--metrics-new-payload-copies metrics) 2)) (should (= (ebox-child-range--segment-node-weight (ebox-child-range--sequence-root new)) (+ static-count 2))) (should (= (ebox-child-range--rank new static-count) static-count)) (should (= (ebox-child-range--metrics-ref-index-visits metrics) 1)) (should (= (ebox-child-range--metrics-ref-index-copies metrics) 0)) (dolist (accessor '(ebox-child-range--metrics-unaffected-payload-visits ebox-child-range--metrics-unaffected-payload-validations ebox-child-range--metrics-unaffected-payload-copies)) (should (zerop (funcall accessor metrics)))) (should (equal (ebox-child-range--flatten old) old-flat)) (should (equal (mapcar (lambda (item) (plist-get item :key)) (ebox-child-range--flatten new)) (append (cl-loop for index below static-count collect (list 'static index)) '(new-a new-b))))))) (ert-deftest ebox-child-range-gate-a-key-bound-collisions-and-sharing () "Sparse hash work is bounded and unaffected segment branches remain shared." (let* ((old (ebox-child-range-test--fixture 100 '(old-a old-b) (lambda (_key) 0))) (result (ebox-child-range-test--replace old 'range (mapcar #'ebox-child-range-test--item '(new-a new-b new-c)))) (new (car result)) (metrics (cdr result)) (affected 2) (replacement 3) (old-key-root (ebox-child-range--sequence-key-root old)) (old-first (aref (ebox-child-range--segment-node-children (ebox-child-range--sequence-root old)) 0)) (new-first (aref (ebox-child-range--segment-node-children (ebox-child-range--sequence-root new)) 0))) (should (= (ebox-child-range--metrics-key-visits metrics) 35)) (should (= (ebox-child-range--metrics-key-copies metrics) 35)) (should (= (ebox-child-range--metrics-collision-visits metrics) 506)) (should (= (ebox-child-range--metrics-collision-copies metrics) 506)) (should (= (+ (ebox-child-range--metrics-key-visits metrics) (ebox-child-range--metrics-collision-visits metrics)) (+ 35 506))) (should (= (+ (ebox-child-range--metrics-key-copies metrics) (ebox-child-range--metrics-collision-copies metrics)) (+ 35 506))) (should (<= (+ (ebox-child-range--metrics-key-visits metrics) (ebox-child-range--metrics-collision-visits metrics)) (+ (* (+ affected replacement) 7) 506))) (should (<= (+ (ebox-child-range--metrics-key-copies metrics) (ebox-child-range--metrics-collision-copies metrics)) (+ (* (+ affected replacement) 7) 506))) (should (<= (ebox-child-range--metrics-key-visits metrics) (+ (* (+ affected replacement) 7) 506))) (should (<= (ebox-child-range--metrics-key-copies metrics) (+ (* (+ affected replacement) 7) 506))) (should (eq old-first new-first)) (should (eq (ebox-child-range--sequence-ref-index old) (ebox-child-range--sequence-ref-index new))) (should (eq (ebox-child-range--sequence-key-root old) old-key-root)) (should (equal (ebox-child-range--hash-lookup (ebox-child-range--sequence-key-root new) 'new-b 0) '(100 . 1))) (should-not (ebox-child-range--hash-lookup (ebox-child-range--sequence-key-root new) 'old-a 0)) (should (ebox-child-range--hash-lookup old-key-root 'old-a 0)) (pcase-let ((`(,nodes . ,edges) (ebox-child-range--hash-memory (ebox-child-range--sequence-key-root old)))) (should (<= nodes (1+ (* 7 102)))) (should (= edges (1- nodes)))))) (ert-deftest ebox-child-range-gate-a-empty-order-and-duplicates () "Empty ranges stay addressable and global ref/key uniqueness is exact." (let* ((empty (ebox-child-range-test--fixture 1 nil)) (one (car (ebox-child-range-test--replace empty 'range (list (ebox-child-range-test--item 'one))))) (many (car (ebox-child-range-test--replace one 'range (mapcar #'ebox-child-range-test--item '(two three))))) (empty-again (car (ebox-child-range-test--replace many 'range nil)))) (should (= (length (ebox-child-range--segment-payload (ebox-child-range--segment-at empty 1))) 0)) (should (equal (mapcar (lambda (item) (plist-get item :key)) (ebox-child-range--flatten many)) '((static 0) two three))) (should-not (cdr (ebox-child-range--flatten empty-again))) (should-error (ebox-child-range-test--build (list (cons 'same nil) (cons 'same nil)))) (should-error (ebox-child-range-test--build (list (cons nil nil)))) (should-error (ebox-child-range-test--build nil)) (should-error (ebox-child-range-test--build (list (cons nil (list (ebox-child-range-test--item 'a) (ebox-child-range-test--item 'b)))))) (should-error (ebox-child-range-test--build (list (cons nil (list (ebox-child-range-test--item 'duplicate))) (cons 'range (list (ebox-child-range-test--item 'duplicate)))))) (should-error (ebox-child-range-test--build (list (cons 'left (list (ebox-child-range-test--item 'duplicate))) (cons 'right (list (ebox-child-range-test--item 'duplicate)))))) (should-error (ebox-child-range-test--replace empty 'range (list (ebox-child-range--descriptor-create 'nested nil)))) (should-error (ebox-child-range--descriptor-create nil nil)) (should-error (ebox-child-range--descriptor-create 'bad '(one . two))) (should-error (ebox-child-range-test--replace empty 'range '(one . two))) (should-error (ebox-child-range-test--build (list (cons 'bad '(one . two))))) (should-error (ebox-child-range-test--replace empty 'missing nil)))) (ert-deftest ebox-child-range-gate-a-adjacent-refs-and-unkeyed-items () "First, middle, and last Range refs stay O(1)-addressed and accept no key." (let* ((segments (list (cons 'first (list (list :value 'unkeyed-first))) (cons nil (list (ebox-child-range-test--item 'static-a))) (cons 'middle (list (ebox-child-range-test--item 'middle-old))) (cons 'adjacent (list (ebox-child-range-test--item 'adjacent))) (cons nil (list (list :value 'unkeyed-static))) (cons 'last nil))) (base (ebox-child-range-test--build segments))) (dolist (entry '((first first-new) (middle middle-new) (last last-new))) (let* ((result (ebox-child-range-test--replace base (car entry) (list (ebox-child-range-test--item (cadr entry))))) (new (car result)) (metrics (cdr result))) (should (= (ebox-child-range--metrics-ref-index-visits metrics) 1)) (should (= (ebox-child-range--metrics-ref-index-copies metrics) 0)) (should (eq (ebox-child-range--sequence-ref-index base) (ebox-child-range--sequence-ref-index new))) (should (equal (ebox-child-range--hash-lookup (ebox-child-range--sequence-key-root new) (cadr entry) (ebox-child-range--stable-hash (cadr entry))) (cons (gethash (car entry) (ebox-child-range--sequence-ref-index base)) 0))))) (should (equal (plist-get (car (ebox-child-range--flatten base)) :value) 'unkeyed-first)) (should (eq (ebox-child-range--segment-ref (ebox-child-range--lookup-ref base 'middle)) 'middle)))) (ert-deftest ebox-child-range-integration-is-transparent-and-indexed () "Compiled Range segments render as direct children and retain empty refs." (let* ((static (ebox-test-box :key 'a :class "a" (ebox-test-text "A") :width '(40))) (one (ebox-test-box :key 'b :class "b" (ebox-test-text "B") :width '(40))) (two (ebox-test-box :key 'c :class "c" (ebox-test-text "C") :width '(40))) (descriptor (ebox-child-range--descriptor-create 'items (list one two))) (empty (ebox-child-range--descriptor-create 'empty nil)) (raw-items (ebox-child-range--descriptor-items descriptor)) (ranged (ebox-test-column static empty descriptor)) (direct (ebox-test-column (ebox-test-box :key 'a :class "a" (ebox-test-text "A") :width '(40)) (ebox-test-box :key 'b :class "b" (ebox-test-text "B") :width '(40)) (ebox-test-box :key 'c :class "c" (ebox-test-text "C") :width '(40)))) (left (generate-new-buffer " *ebox-range-direct*")) (right (generate-new-buffer " *ebox-range-compiled*"))) (unwind-protect (progn (ebox-render-to-buffer left direct) (ebox-render-to-buffer right ranged) (should (equal (with-current-buffer left (substring-no-properties (buffer-string))) (with-current-buffer right (substring-no-properties (buffer-string))))) (should (equal (mapcar #'ebox-string-pixel-width (ebox-string-lines (with-current-buffer left (buffer-string)))) (mapcar #'ebox-string-pixel-width (ebox-string-lines (with-current-buffer right (buffer-string)))))) (should (eq raw-items (ebox-child-range--descriptor-items descriptor))) (should (equal (mapcar #'ebox-child-range-test--node-key raw-items) '(b c))) (let* ((direct-state (ebox--buffer-render-state left)) (state (ebox--buffer-render-state right)) (range-table (plist-get state :range-ref-table)) (empty-record (gethash 'empty range-table)) (items-record (gethash 'items range-table)) (empty-resolved (ebox-incremental--range-ref-resolve state 'empty)) (items-resolved (ebox-incremental--range-ref-resolve state 'items))) (should empty-record) (should items-record) (should-not (plist-member empty-record :rank)) (should-not (plist-member empty-record :sequence)) (should (= (plist-get empty-resolved :rank) 1)) (should (= (plist-get items-resolved :rank) 1)) (should (= (ebox-runtime-index-size (plist-get state :node-table)) (ebox-runtime-index-size (plist-get direct-state :node-table)))) (should (= (hash-table-count (plist-get state :region-id-set)) (hash-table-count (plist-get direct-state :region-id-set)))) (should (= (hash-table-count (plist-get state :surface-node-object-table)) (hash-table-count (plist-get direct-state :surface-node-object-table)))) (should (equal (plist-get empty-record :parent-node-id) (plist-get (plist-get state :root-node) :node-id)))) (should (= (length (ebox-selector-query-buffer right ".a + .b")) 1)) (should (= (length (ebox-selector-query-buffer right ".a ~ .c")) 1))) (dolist (buffer (list left right)) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-integration-rejects-invalid-placement-atomically () "Range refs, keys, nesting, roots, and scalar slots fail before publication." (should-error (ebox-test-column (ebox-child-range--descriptor-create 'outer (list (ebox-child-range--descriptor-create 'nested nil))))) (should-error (ebox-test-column (ebox-test-box :key 'duplicate (ebox-test-text "a")) (ebox-child-range--descriptor-create 'range (list (ebox-test-box :key 'duplicate (ebox-test-text "b")))))) (let ((buffer (generate-new-buffer " *ebox-range-invalid*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-box :key 'root (ebox-test-text "old"))) (let ((before (with-current-buffer buffer (buffer-string))) (state (ebox--buffer-render-state buffer))) (dolist (root (list (ebox-child-range--descriptor-create 'root (list (ebox-test-box :key 'x (ebox-test-text "x")))) (list :ebox-type 'box :ebox-content-node (ebox-child-range--descriptor-create 'scalar nil)) (ebox-test-column (ebox-child-range--descriptor-create 'same nil) (ebox-test-column (ebox-child-range--descriptor-create 'same nil))))) (should-error (ebox-commit buffer root)) (should (eq (ebox--buffer-render-state buffer) state)) (should (equal (with-current-buffer buffer (buffer-string)) before))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-production-readers-use-tree-accessor () "Range-capable production consumers do not read node `:children' directly." (dolist (file '("ebox.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" "ebox-incremental.el" "ebox-native-reflow.el" "ebox-surface.el")) (with-temp-buffer (insert-file-contents (expand-file-name file ebox-child-range-test--root)) (should-not (re-search-forward "plist-get[[:space:]\n]+\\(?:node\\|flex-node\\)[[:space:]\n]+:children" nil t))))) (ert-deftest ebox-child-range-ecss-subjects-and-inheritance-are-transparent () "Ranges add no ECSS subject and preserve child/sibling cascade semantics." (let* ((ebox-style-stylesheet (ecss-stylesheet-create)) (direct-left (ebox-test-box :key 'left :source-identity 'left :class "left" (ebox-test-text "L") :width '(40))) (direct-right (ebox-test-box :key 'right :source-identity 'right :class "right" (ebox-test-text "R") :width '(40))) (range-left (ebox-test-box :key 'left :source-identity 'left :class "left" (ebox-test-text "L") :width '(40))) (range-right (ebox-test-box :key 'right :source-identity 'right :class "right" (ebox-test-text "R") :width '(40))) (left-range (ebox-child-range--descriptor-create 'left-range (list range-left))) (right-range (ebox-child-range--descriptor-create 'right-range (list range-right))) (direct (ebox-test-column :class "parent" direct-left direct-right)) (ranged (ebox-test-column :class "parent" left-range right-range)) (direct-buffer (generate-new-buffer " *ebox-range-ecss-direct*")) (range-buffer (generate-new-buffer " *ebox-range-ecss-range*")) (original (symbol-function 'ebox-style-compute-subject)) direct-subjects range-subjects) (ebox-style-add-rule ".parent > .left" '(:color "#123456")) (ebox-style-add-rule ".left + .right" '(:background-color "#ABCDEF")) (unwind-protect (progn (cl-letf (((symbol-function 'ebox-style-compute-subject) (lambda (&rest arguments) (cl-incf direct-subjects) (apply original arguments)))) (setq direct-subjects 0) (ebox-render-to-buffer direct-buffer direct)) (cl-letf (((symbol-function 'ebox-style-compute-subject) (lambda (&rest arguments) (cl-incf range-subjects) (apply original arguments)))) (setq range-subjects 0) (ebox-render-to-buffer range-buffer ranged)) (should (= direct-subjects range-subjects)) (should (= (length (ebox-selector-query-buffer range-buffer ".left + .right")) 1)) (let ((direct-faces (with-current-buffer direct-buffer (cl-loop for position from (point-min) below (point-max) collect (get-text-property position 'face)))) (range-faces (with-current-buffer range-buffer (cl-loop for position from (point-min) below (point-max) collect (get-text-property position 'face))))) (should (equal direct-faces range-faces))) (dolist (node (list range-left range-right)) (should-not (plist-get node :node-id)) (should-not (plist-get node :region-id)) (should-not (plist-get node :surface-object))) (dolist (ref '(left right)) (let* ((state (ebox--buffer-render-state range-buffer)) (node-id (gethash ref (ebox-child-range-test--host-ref-table state))) (node (ebox-runtime-index-get node-id (plist-get state :node-table)))) (should node-id) (should (equal (ebox-child-range-test--node-key node (plist-get state :source-index)) ref))))) (dolist (buffer (list direct-buffer range-buffer)) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-path-copy-replaces-compiled-material-child () "Tree path copying cannot ignore a child stored in a compiled sequence." (let* ((source (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "static")) (ebox-test-child-range 'items (ebox-test-box :key 'old (ebox-test-text "old"))))) (compiled (ebox-tree-copy-node-structure (ebox-test-root source))) (old-child (cadr (ebox-tree-layout-children compiled))) (new-input (ebox-test-box :key 'new (ebox-test-text "new"))) (new-child (ebox-test-root new-input)) (source-index (ebox-test-source-index (ebox-test-forest-input source new-input))) (unchanged (ebox-tree-copy-with-direct-child-replacements compiled nil)) (copy (ebox-tree-copy-with-direct-child-replacements compiled (list (cons old-child new-child)) (lambda (node) (ebox-child-range-test--node-key node source-index))))) (should (eq (plist-get unchanged :ebox-child-sequence) (plist-get compiled :ebox-child-sequence))) (should (equal (mapcar (lambda (node) (ebox-child-range-test--node-key node source-index)) (ebox-tree-layout-children copy)) '(static new))) (should (equal (mapcar (lambda (node) (ebox-child-range-test--node-key node source-index)) (ebox-tree-layout-children compiled)) '(static old))) (let* ((original (ebox-test-text "old")) (range (ebox-test-child-range 'typed original)) (typed-input (ebox-test-column range)) (typed (ebox-test-root typed-input)) (owned (car (ebox-box-node-children typed))) (replacement-input (ebox-test-text "new")) (replacement (ebox-test-root replacement-input)) (replaced (ebox-tree-copy-with-direct-child-replacements typed (list (cons owned replacement)))) (builder (ebox-source-builder-create)) (_typed-sources (ebox-source-builder-import builder (ebox-test-source-index typed-input))) (_replacement-sources (ebox-source-builder-import builder (ebox-test-source-index replacement-input))) (replaced-index (ebox-tree-source-builder-snapshot builder (list replaced))) direct) (ebox-tree-for-each-direct-child replaced (lambda (child) (push child direct))) (setq direct (nreverse direct)) (dolist (view (list (ebox-box-node-children replaced) direct (ebox-tree-layout-children replaced) (ebox-tree-semantic-children replaced))) (should (= (length view) 1)) (should (eq (car view) replacement))) (should (eq (ebox-tree-validate-declarative-root replaced nil nil replaced-index) replaced)) (should-error (ebox-tree-copy-with-direct-child-replacements typed (list (cons owned nil))) :type 'error)))) (ert-deftest ebox-child-range-empty-sequence-is-authoritative () "An all-empty sequence cannot fall through to legacy alias children." (let* ((sequence (ebox-child-range-test--build (list (cons 'empty nil)))) (legacy (ebox-test-box :key 'legacy (ebox-test-text "legacy"))) (node (list :ebox-type 'stack :ebox-child-sequence sequence :top legacy :bottom legacy))) (should-not (ebox-tree-layout-children node)))) (ert-deftest ebox-child-range-validator-covers-wrapper-and-scalar-slots () "Flat Range compilation cannot hide wrappers or admit scalar descriptors." (let* ((wrapper-input (ebox-test-box :key 'wrapper :source-identity 'wrapper (ebox-test-text "W"))) (item-input (ebox-test-box :key 'item :source-identity 'item (ebox-test-text "I"))) (wrapper (ebox-test-root wrapper-input)) (item (ebox-test-root item-input)) (descriptor (ebox-test-child-range 'items item-input)) (source (list :ebox-type 'flex :display '(block flex) :box wrapper :children (list descriptor))) (typed-input (ebox-test-column descriptor)) (typed (ebox-test-root typed-input))) (should-error (ebox-tree-copy-node-structure source) :type 'error) (ebox-tree-validate-declarative-root typed nil nil (ebox-test-source-index typed-input)) (let* ((copy (ebox-tree-copy-node-structure typed)) (copy-wrapper (car (ebox-tree-layout-children copy))) (copy-item (car (ebox-tree-layout-children copy)))) (should-not (eq item copy-wrapper)) (should-not (eq item copy-item)) (dolist (node (list wrapper item)) (should-not (plist-get node :node-id)) (should-not (plist-get node :region-id)) (should-not (plist-get node :surface-object)))) (should-error (ebox-tree-validate-declarative-root (list :ebox-type 'stack :display '(block column) :top (ebox-child-range--descriptor-create 'bad nil) :bottom (ebox-test-box :key 'bottom (ebox-test-text "B"))))) (should-error (ebox-tree-validate-declarative-root (list :ebox-type 'flex :display '(block flex) :box (ebox-test-box :key 'same :source-identity 'same (ebox-test-text "W")) :children (list (ebox-child-range--descriptor-create 'items (list (ebox-test-box :key 'item :source-identity 'same (ebox-test-text "I")))))))))) (ert-deftest ebox-child-range-public-candidate-splices-and-reports () "Public child Ranges support empty and repeated last-wins candidate splices." (let* ((buffer (generate-new-buffer " *ebox-public-range*")) (source (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "S")) (ebox-test-child-range 'items))) report) (unwind-protect (progn (ebox-render-to-buffer buffer source) (dolist (items (list (list (ebox-test-box :key 'one (ebox-test-text "1"))) (list (ebox-test-box :key 'two (ebox-test-text "2")) (ebox-test-box :key 'three (ebox-test-text "3"))) (list (ebox-test-box :key 'final (ebox-test-text "F"))) nil)) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input items)) (when items (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input items))) (setq report (ebox-commit buffer candidate)) (should (plist-get report :range-metrics)))) (should (gethash 'items (plist-get (ebox--buffer-render-state buffer) :range-ref-table)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-candidate-retains-proven-owned-items () "A fresh candidate-owned Range item is not copied twice before commit." (let* ((buffer (generate-new-buffer " *ebox-range-retained-items*")) (input (ebox-child-range-test--input (list (ebox-test-box :key 'new (ebox-test-text "new"))))) (item (ebox-test-root input))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "static")) (ebox-test-child-range 'items (ebox-test-box :key 'old (ebox-test-text "old"))))) (let* ((candidate (ebox-candidate-begin buffer)) entry) (ebox-candidate-replace-range-ref candidate 'items input nil t) (setq entry (car (ebox-candidate--range-replacements candidate))) (should (ebox-incremental--candidate-range-replacement-retain-item-identities-p entry)) (should (eq item (car (ebox-incremental--candidate-range-replacement-items entry)))) (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (record (gethash 'items (plist-get state :range-ref-table))) (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get state :node-table))) (segment (ebox-child-range--lookup-ref (plist-get parent :ebox-child-sequence) 'items)) (published (aref (ebox-child-range--segment-payload segment) 0))) (should (eq published item)) (should (plist-get published :node-id)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-candidate-retention-falls-back-on-runtime-identity () "A retention proof miss uses the ordinary identity-clearing copy path." (let* ((buffer (generate-new-buffer " *ebox-range-retention-fallback*")) (input (ebox-child-range-test--input (list (ebox-test-box :key 'new (ebox-test-text "new"))))) (item (ebox-test-root input))) (plist-put item :node-id 999) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-child-range 'items (ebox-test-box :key 'old (ebox-test-text "old"))))) (let* ((candidate (ebox-candidate-begin buffer)) entry retained) (ebox-candidate-replace-range-ref candidate 'items input nil t) (setq entry (car (ebox-candidate--range-replacements candidate)) retained (ebox-incremental--candidate-range-replacement-items entry)) (should-not (ebox-incremental--candidate-range-replacement-retain-item-identities-p entry)) (should-not (eq item (car retained))) (should-not (plist-get (car retained) :node-id)) (ebox-commit buffer candidate)) (should (string-match-p "new" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-candidate-retains-fresh-slots-around-reuse () "Mixed Range updates retain fresh slots while preserving reused slots." (let* ((buffer (generate-new-buffer " *ebox-range-mixed-retention*")) (input (ebox-child-range-test--input (list (ebox-test-box :key 'old-one (ebox-test-text "old-one")) (ebox-test-box :key 'new-two (ebox-test-text "new-two"))))) (fresh (nth 1 (ebox-test-forest input)))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-child-range 'items (ebox-test-box :key 'old-one (ebox-test-text "old-one")) (ebox-test-box :key 'old-two (ebox-test-text "old-two"))))) (let* ((candidate (ebox-candidate-begin buffer)) entry) (ebox-candidate-replace-range-ref candidate 'items input '((0 . 0)) t) (setq entry (car (ebox-candidate--range-replacements candidate))) (should (ebox-incremental--candidate-range-replacement-retain-item-identities-p entry)) (should (eq fresh (nth 1 (ebox-incremental--candidate-range-replacement-items entry)))) (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (record (gethash 'items (plist-get state :range-ref-table))) (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get state :node-table))) (segment (ebox-child-range--lookup-ref (plist-get parent :ebox-child-sequence) 'items)) (payload (ebox-child-range--segment-payload segment))) (should (eq fresh (aref payload 1))) (should (plist-get (aref payload 1) :node-id)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-stable-keyed-refresh-is-not-structural () "Stable keyed Range refreshes must not become root structure updates." (let ((buffer (generate-new-buffer " *ebox-stable-range-dirty*"))) (cl-labels ((rows (selected) (cl-loop for id from 1 to 12 for content = (ebox-test-text (format "row-%02d" id)) collect (if (= id selected) (ebox-test-box :key id :width '(420) :color "#2F6B43" content) (ebox-test-box :key id :width '(420) content)))) (root () (ebox-test-box :width '(900) :color "#172033" :wrap-mode 'none (ebox-test-flex :width '(900) (ebox-test-box :key 'list :width 'stretch :min-width 0 :wrap-mode 'none :min-height 24 :padding '(2 2) :border "#687386" :flex-grow 2 :flex-shrink 1 :flex-basis '(600) (ebox-test-column (apply #'ebox-test-child-range 'items (rows 1)))) (ebox-test-box :key 'detail :source-identity 'detail :width 'stretch :min-width 0 :min-height 24 :padding '(2 2) :border "#687386" :flex-grow 1 :flex-shrink 1 :flex-basis '(300) (ebox-test-text "detail-1")))))) (unwind-protect (progn (ebox-render-to-buffer buffer (root)) (dolist (selected '(2 1 2)) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (rows selected))) (ebox-candidate-replace-host-ref candidate 'detail (ebox-test-box :key 'detail :source-identity 'detail :width 'stretch :min-width 0 :min-height 24 :padding '(2 2) :border "#687386" :flex-grow 1 :flex-shrink 1 :flex-basis '(300) (ebox-test-text (format "detail-%d" selected)))) (let ((report (ebox-commit buffer candidate))) (ert-info ((format "selected=%S report=%S" selected report)) (should-not (memq 'structure (plist-get report :dirty-kinds))) (should (memq 'paint (plist-get report :dirty-kinds))) (should (memq (plist-get report :strategy) '(span-patch owner-rerender mixed-owner-reflow))) (should (> (plist-get report :tp-scope-count) 0)) (should-not (plist-get report :tp-full-root)) (should (= (plist-get report :created-objects) 0)) (should (= (plist-get report :removed-objects) 0)) (with-current-buffer buffer (should (string-match-p (format "detail-%d" selected) (buffer-string))))))))) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-does-not-reuse-disappeared-key-positionally () "A new keyed item must not inherit a removed peer's runtime identity." (let ((buffer (generate-new-buffer " *ebox-range-key-reentry*"))) (cl-labels ((rows (keys) (mapcar (lambda (key) (ebox-test-box :key key (ebox-test-text (format "row-%s" key)))) keys)) (row-id (key) (let* ((state (ebox--buffer-render-state buffer)) (resolved (ebox-incremental--range-ref-resolve state 'items)) (segment (ebox-child-range--segment-at (plist-get resolved :sequence) (plist-get resolved :segment-index))) (node (cl-find key (append (ebox-child-range--segment-payload segment) nil) :key (lambda (item) (ebox-child-range-test--node-key item (plist-get state :source-index)))))) (plist-get node :node-id)))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (apply #'ebox-test-child-range 'items (rows '(1 2 3 4))))) (let ((initial-1 (row-id 1)) (initial-4 (row-id 4))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (rows '(1 4)))) (ebox-commit buffer candidate)) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (rows '(1 2 3 4)))) (ebox-commit buffer candidate)) (should (= initial-1 (row-id 1))) (should (= initial-4 (row-id 4))) (should-not (= initial-4 (row-id 2))))) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-sole-child-keeps-public-layout-parent () "Keep row/column material parents for one empty or populated Range." (dolist (case '((ebox-test-row row) (ebox-test-column column))) (dolist (items (list nil (list (ebox-test-box :key 'one (ebox-test-text "1"))))) (let* ((range (apply #'ebox-test-child-range 'items items)) (parent-input (funcall (car case) range)) (parent (ebox-test-root parent-input))) (should-not (ebox-child-range--descriptor-p parent)) (should (ebox-box-node-p parent)) (should (eq (ebox-layout-config-kind (ebox-box-node-layout parent)) (cadr case))) (should (equal (ebox-box-node-children parent) (mapcar #'ebox-test-root items))) (should (equal (ebox-box-node-range-anchors parent) (list (list :ref 'items :before 0 :after (length items))))) (should (stringp (ebox-render parent-input))))))) (ert-deftest ebox-child-range-typed-box-preserves-transparent-range () "Typed Box should retain Range identity outside its visual child model." (let* ((before (ebox-test-text "before" :key 'before)) (first (ebox-test-text "A" :key 'first)) (second (ebox-test-text "B" :key 'second)) (after (ebox-test-text "after" :key 'after)) (range (ebox-test-child-range 'items first second)) (range-items (ebox-child-range--descriptor-items range)) (box-input (ebox-test-column before range after)) (box (ebox-test-root box-input)) (buffer (generate-new-buffer " *ebox-typed-range*"))) (unwind-protect (progn (should (equal (ebox-box-node-children box) (mapcar #'ebox-test-root (list before first second after)))) (cl-mapc (lambda (actual expected) (should (eq actual expected))) (ebox-box-node-children box) (mapcar #'ebox-test-root (list before (car range-items) (cadr range-items) after))) (should (equal (ebox-box-node-range-anchors box) '((:ref items :before 1 :after 3)))) (let* ((empty (ebox-test-child-range 'empty)) (empty-input (ebox-test-column before empty after)) (empty-box (ebox-test-root empty-input))) (should (equal (ebox-box-node-children empty-box) (mapcar #'ebox-test-root (list before after)))) (should (equal (ebox-box-node-range-anchors empty-box) '((:ref empty :before 1 :after 1))))) (should (equal (mapcar (lambda (line) (string-trim-right (substring-no-properties line))) (ebox-string-lines (ebox-render box-input))) '("before" "A" "B" "after"))) (ebox-render-to-buffer buffer box-input) (let* ((runtime-root (ebox--buffer-root-node buffer)) (snapshot (ebox-tree-runtime-identity-snapshot runtime-root))) (should (= (plist-get snapshot :node-count) 5)) (should (= (length (plist-get (plist-get snapshot :root) :children)) 4))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-text "C" :key 'replacement)))) (ebox-commit buffer candidate)) (should (equal (with-current-buffer buffer (mapcar (lambda (line) (string-trim-right (substring-no-properties line))) (ebox-string-lines (buffer-string)))) '("before" "C" "after"))) (let ((before-style-state (ebox--buffer-render-state buffer)) (before-style-text (with-current-buffer buffer (buffer-string))) (candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-text "B" :color "red")))) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (error "reject styled Range")))) (should-error (ebox-commit buffer candidate) :type 'error)) (should (eq before-style-state (ebox--buffer-render-state buffer))) (should (equal before-style-text (with-current-buffer buffer (buffer-string))))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-text "B" :color "red")))) (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (root (plist-get state :root-node)) (rendered (with-current-buffer buffer (buffer-string))) (position (let ((case-fold-search nil)) (string-match "B" rendered))) (face (and position (get-text-property position 'face rendered)))) (should (zerop (ebox-tree-author-style-count root))) (should-not (plist-get state :cascade-required-p)) (should (or (and (listp face) (keywordp (car face)) (equal (plist-get face :foreground) "red")) (cl-some (lambda (entry) (and (listp entry) (equal (plist-get entry :foreground) "red"))) face)))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-text "D" :key 'unstyled)))) (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (root (plist-get state :root-node)) (rendered (with-current-buffer buffer (buffer-string))) (position (string-match "D" rendered)) (face (and position (get-text-property position 'face rendered)))) (should (zerop (ebox-tree-author-style-count root))) (should-not (or (and (listp face) (keywordp (car face)) (equal (plist-get face :foreground) "red")) (cl-some (lambda (entry) (and (listp entry) (equal (plist-get entry :foreground) "red"))) face)))) (let ((before-invalid (with-current-buffer buffer (buffer-string))) (candidate (ebox-candidate-begin buffer))) (should-error (ebox-candidate-replace-range-ref candidate 'items (list (list :ebox-type 'box :content "legacy"))) :type 'error) (should (equal before-invalid (with-current-buffer buffer (buffer-string)))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-typed-render-reuses-material-child-list () "Typed Range rendering should not repeatedly flatten its child sequence." (let* ((items (cl-loop for index below 100 collect (ebox-test-text (number-to-string index) :key index))) (range (apply #'ebox-test-child-range 'items items)) (box-input (ebox-test-column range)) (box (ebox-test-root box-input)) (original (symbol-function 'ebox-child-range--flatten)) (calls 0)) (cl-letf (((symbol-function 'ebox-child-range--flatten) (lambda (sequence) (cl-incf calls) (funcall original sequence)))) (ebox-render box-input)) (should (zerop calls)) (let* ((replacement-input (ebox-test-text "replacement" :key 'new)) (replacement (ebox-test-root replacement-input)) (source-index (ebox-test-source-index (ebox-test-forest-input box-input replacement-input))) (updated (car (ebox-child-range--replace (plist-get box :ebox-child-sequence) 'items (list replacement) (lambda (node) (ebox-child-range-test--node-key node source-index))))) (candidate (copy-sequence box))) (ebox-tree--put-child-sequence candidate updated) (setq calls 0) (cl-letf (((symbol-function 'ebox-child-range--flatten) (lambda (sequence) (cl-incf calls) (funcall original sequence)))) (ebox-tree-subject-index candidate source-index) (ebox-tree-layout-children candidate)) (should (= calls 1))))) (ert-deftest ebox-child-range-empty-owner-falls-back-atomically () "Publish empty sole Range through full surface fallback and rollback safely." (let ((buffer (generate-new-buffer " *ebox-empty-range-owner*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-child-range 'items))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'one (ebox-test-text "1"))))) (cl-letf (((symbol-function 'accept-change-group) (lambda (&rest _) (error "injected empty accept")))) (should-error (ebox-commit buffer candidate) :type 'error))) (should (equal "" (with-current-buffer buffer (buffer-string)))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'one (ebox-test-text "1"))))) (let ((report (ebox-commit buffer candidate))) (should (plist-get report :empty-range-owner-fallback)))) (should (string-match-p "1" (with-current-buffer buffer (buffer-string)))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-test-forest-input)) (ebox-commit buffer candidate)) (should (equal "" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-candidate-derivation-never-flattens-parent () "Range logical-root and local-index derivation never flatten parent payloads." (let* ((buffer (generate-new-buffer " *ebox-range-no-flatten*")) (static (cl-loop for index below 100 collect (ebox-test-box :key (format "static-%d" index) (ebox-test-text (number-to-string index))))) (root (apply #'ebox-test-column (append static (list (ebox-test-child-range 'items (ebox-test-box :key 'old (ebox-test-text "old"))))))) candidate logical trace deltas) (unwind-protect (progn (ebox-render-to-buffer buffer root) (setq candidate (ebox-candidate-begin buffer)) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new (ebox-test-text "new"))))) (setq trace (make-hash-table :test 'equal)) (cl-letf (((symbol-function 'ebox-child-range--flatten) (lambda (&rest _) (error "candidate flattened Range")))) (let ((ebox-incremental--candidate-path-copy-trace trace) (ebox-incremental--candidate-range-index-deltas nil) (ebox-incremental--candidate-incoming-source-indexes (ebox-incremental--candidate-source-input-map candidate))) (setq logical (ebox-incremental--candidate-logical-root candidate nil) deltas ebox-incremental--candidate-range-index-deltas) (ebox-incremental--candidate-local-index-delta (ebox-candidate--base-state candidate) logical trace deltas))) (let ((metrics (car (ebox-candidate--range-metrics candidate)))) (should (= (plist-get metrics :segment-visits) 3)) (should (= (plist-get metrics :unaffected-payload-visits) 0)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-adjacent-key-move-is-two-phase () "A key can move between adjacent Ranges in one logical candidate." (let ((buffer (generate-new-buffer " *ebox-range-key-move*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-child-range 'left (ebox-test-box :key 'move (ebox-test-text "L"))) (ebox-test-child-range 'right (ebox-test-box :key 'stay (ebox-test-text "R"))))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'left (ebox-test-forest-input)) (ebox-candidate-replace-range-ref candidate 'right (ebox-child-range-test--input (list (ebox-test-box :key 'move (ebox-test-text "M")) (ebox-test-box :key 'stay (ebox-test-text "R"))))) (ebox-commit buffer candidate)) (should (string-match-p "M" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-public-gate-a-and-prepared-index () "Public Range commits retain logarithmic metrics and a prepared local index." (dolist (static-count '(10 100 500)) (let* ((buffer (generate-new-buffer " *ebox-range-public-gate*")) (root (apply #'ebox-test-column (append (cl-loop for index below static-count collect (ebox-test-box :key (format "static-%d" index) (ebox-test-text "s"))) (list (ebox-test-child-range 'items (ebox-test-box :key 'old (ebox-test-text "o")))))))) (unwind-protect (progn (ebox-render-to-buffer buffer root) (let* ((old-state (ebox--buffer-render-state buffer)) (old-table (plist-get old-state :range-ref-table)) (surface (with-current-buffer buffer ebox-surface--buffer-surface)) (revision (tp-surface-revision surface)) (candidate (ebox-candidate-begin buffer)) report) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new-a (ebox-test-text "a")) (ebox-test-box :key 'new-b (ebox-test-text "b"))))) (cl-letf (((symbol-function 'ebox--runtime-index) (lambda (&rest _) (error "unexpected full index")))) (setq report (ebox-commit buffer candidate))) (let* ((parent-metrics (car (plist-get (plist-get report :range-metrics) :parents))) (bound (if (= static-count 10) 2 3))) (should (<= (plist-get parent-metrics :segment-visits) bound)) (should (<= (plist-get parent-metrics :segment-copies) bound)) (should (= (plist-get parent-metrics :unaffected-payload-visits) 0)) (should (= (plist-get parent-metrics :new-payload-visits) 2))) (should (= (tp-surface-revision surface) (1+ revision))) (let* ((state (ebox--buffer-render-state buffer)) (resolved (ebox-incremental--range-ref-resolve state 'items)) (segment (ebox-child-range--segment-at (plist-get resolved :sequence) (plist-get resolved :segment-index)))) (should (eq old-table (plist-get state :range-ref-table))) (dotimes (offset (length (ebox-child-range--segment-payload segment))) (let* ((node (aref (ebox-child-range--segment-payload segment) offset)) (node-id (plist-get node :node-id)) (object (gethash node-id (plist-get state :surface-node-object-table)))) (should (eq node (ebox-runtime-index-get node-id (plist-get state :node-table)))) (should object) (should (eq object (plist-get node :surface-object)))))))) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-candidate-introduced-ref-is-next-turn-only () "A node replacement may introduce a Range address only for the next turn." (let ((buffer (generate-new-buffer " *ebox-range-introduced*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'a :source-identity 'a (ebox-test-text "a")) (ebox-test-box :key 'b (ebox-test-text "b")))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-host-ref candidate 'a (ebox-test-column (ebox-test-box :key 'a :source-identity 'a (ebox-test-text "a")) (ebox-test-child-range 'introduced))) (should-error (ebox-candidate-replace-range-ref candidate 'introduced (ebox-test-forest-input))) (ebox-commit buffer candidate)) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'introduced (ebox-child-range-test--input (list (ebox-test-box :key 'later (ebox-test-text "later"))))) (ebox-commit buffer candidate)) (should (string-match-p "later" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-coalescing-is-order-independent () "Host ancestors win Range updates while Ranges win descendant Host updates." (dolist (order '(host-first range-first)) (let ((buffer (generate-new-buffer " *ebox-range-coalesce*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-column :source-identity 'parent (ebox-test-box :key 'static (ebox-test-text "s")) (ebox-test-child-range 'items (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "old")))) (ebox-test-box :key 'peer (ebox-test-text "peer")))) (let ((candidate (ebox-candidate-begin buffer))) (dolist (operation (if (eq order 'host-first) '(host range) '(range host))) (pcase operation ('host (ebox-candidate-replace-host-ref candidate 'parent (ebox-test-box :key 'parent-new (ebox-test-text "HOST-WINS")))) ('range (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'range-new (ebox-test-text "range-loses")))))))) (ebox-commit buffer candidate)) (should (string-match-p "HOST-WINS" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-consecutive-host-updates-keep-point-location () "Retained Host identity preserves its sequence point location across turns." (let ((buffer (generate-new-buffer " *ebox-range-host-location*")) node-id) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "s")) (ebox-test-child-range 'items (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "0"))))) (dotimes (turn 2) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-host-ref candidate 'child (ebox-test-box :key 'child :source-identity 'child (ebox-test-text (number-to-string (1+ turn))))) (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (current-id (gethash 'child (ebox-child-range-test--host-ref-table state))) (node (ebox-runtime-index-get current-id (plist-get state :node-table)))) (when node-id (should (equal node-id current-id))) (setq node-id current-id) (should (plist-get node :ebox-sequence-location)))) (should (string-match-p "2" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-three-way-coalescing-keeps-host-descendant () "A losing Range cannot absorb a descendant Host under its winning ancestor." (let ((buffer (generate-new-buffer " *ebox-range-three-way*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-column :source-identity 'parent (ebox-test-box :key 'static (ebox-test-text "s")) (ebox-test-child-range 'items (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "old")))) (ebox-test-box :key 'peer (ebox-test-text "peer")))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-host-ref candidate 'parent (ebox-test-column :source-identity 'parent (ebox-test-box :key 'static (ebox-test-text "s2")) (ebox-test-child-range 'items (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "ancestor"))))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'range (ebox-test-text "RANGE-LOSES"))))) (ebox-candidate-replace-host-ref candidate 'child (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "DESCENDANT-WINS"))) (ebox-commit buffer candidate)) (let ((text (with-current-buffer buffer (buffer-string)))) (should (string-match-p "DESCENDANT-WINS" text)) (should-not (string-match-p "RANGE-LOSES" text)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-large-payload-never-promotes-to-full-index () "M=64 remains an affected-payload local delta under 500 static segments." (let* ((buffer (generate-new-buffer " *ebox-range-large-payload*")) (root (apply #'ebox-test-column (append (cl-loop for index below 500 collect (ebox-test-box :key (format "static-%d" index) (ebox-test-text "s"))) (list (ebox-test-child-range 'items))))) report) (unwind-protect (progn (ebox-render-to-buffer buffer root) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (cl-loop for index below 64 collect (ebox-test-box :key (format "new-%d" index) (ebox-test-text "n"))))) (cl-letf (((symbol-function 'ebox-incremental--candidate-full-index-preparation) (lambda (&rest _) (error "unexpected full preparation"))) ((symbol-function 'ebox--runtime-index) (lambda (&rest _) (error "unexpected full index")))) (setq report (ebox-commit buffer candidate)))) (let ((metrics (car (plist-get (plist-get report :range-metrics) :parents)))) (should (= (plist-get metrics :new-payload-visits) 64)) (should (<= (plist-get metrics :segment-visits) 3)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-wins-descendant-host-in-both-orders () "A surviving Range absorbs a descendant Host replacement independent of order." (dolist (order '(host-first range-first)) (let ((buffer (generate-new-buffer " *ebox-range-wins-host*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "s")) (ebox-test-child-range 'items (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "old"))))) (let ((candidate (ebox-candidate-begin buffer))) (dolist (operation (if (eq order 'host-first) '(host range) '(range host))) (if (eq operation 'host) (ebox-candidate-replace-host-ref candidate 'child (ebox-test-box :key 'child :source-identity 'child (ebox-test-text "HOST-LOSES"))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'range (ebox-test-text "RANGE-WINS"))))))) (ebox-commit buffer candidate)) (let ((text (with-current-buffer buffer (buffer-string)))) (should (string-match-p "RANGE-WINS" text)) (should-not (string-match-p "HOST-LOSES" text)))) (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest ebox-child-range-overlay-prunes-descendant-node-anchors () "Ancestor/descendant node anchors install nested Range refs exactly once." (let ((buffer (generate-new-buffer " *ebox-range-overlay-prune*"))) (unwind-protect (cl-labels ((child (static-text range-ref range-key range-text) (let ((node (ebox-test-column :key 'child :source-identity 'child (ebox-test-box :key 'child-static (ebox-test-text static-text)) (if range-key (ebox-test-child-range range-ref (ebox-test-box :key range-key (ebox-test-text range-text))) (ebox-test-child-range range-ref))))) node)) (ancestor (child-node peer-text) (let ((node (ebox-test-column :key 'ancestor :source-identity 'ancestor child-node (ebox-test-box :key 'ancestor-peer (ebox-test-text peer-text))))) node))) (ebox-render-to-buffer buffer (ebox-test-column (ancestor (child "s" 'old-ref nil nil) "p") (ebox-test-box :key 'root-peer (ebox-test-text "r")))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-host-ref candidate 'ancestor (ancestor (child "s2" 'new-ref 'ancestor-range "a") "p2")) (ebox-candidate-replace-host-ref candidate 'child (child "s3" 'new-ref 'descendant-range "d")) (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (table (plist-get state :range-ref-table)) (record (gethash 'new-ref table)) (child-id (gethash 'child (ebox-child-range-test--host-ref-table state)))) (should-not (gethash 'old-ref table)) (should record) (should (= (hash-table-count table) 1)) (should (equal (plist-get record :parent-node-id) child-id)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-disjoint-host-and-range-publish-once () "Disjoint Host and Range changes share one scoped TP publication." (let ((buffer (generate-new-buffer " *ebox-range-disjoint*")) (callbacks 0)) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'host :source-identity 'host (ebox-test-text "old-host")) (ebox-test-child-range 'items (ebox-test-box :key 'old-range (ebox-test-text "old-range"))))) (let* ((surface (with-current-buffer buffer ebox-surface--buffer-surface)) (revision (tp-surface-revision surface)) (candidate (ebox-candidate-begin buffer)) report) (ebox-candidate-replace-host-ref candidate 'host (ebox-test-box :key 'host :source-identity 'host (ebox-test-text "new-host"))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new-range (ebox-test-text "new-range"))))) (setq report (ebox-commit buffer candidate (lambda (_report) (cl-incf callbacks)))) (should (= callbacks 1)) (should (= (tp-surface-revision surface) (1+ revision))) (should (= (plist-get (plist-get report :range-metrics) :applied-count) 1)) (should-not (plist-get report :tp-scope-fallback)) (let ((text (with-current-buffer buffer (buffer-string)))) (should (string-match-p "new-host" text)) (should (string-match-p "new-range" text))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-final-accept-failure-restores-exact-generation () "A Range commit rejected by final accept restores all old identities." (let ((buffer (generate-new-buffer " *ebox-range-final-rollback*")) trace) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "s")) (ebox-test-child-range 'items (ebox-test-box :key 'old :source-identity 'old (ebox-test-text "old"))))) (let* ((old-state (ebox--buffer-render-state buffer)) (old-root (plist-get old-state :root-node)) (old-table (plist-get old-state :range-ref-table)) (old-id (gethash 'old (ebox-child-range-test--host-ref-table old-state))) (old-buffer (with-current-buffer buffer (buffer-string))) (candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new :source-identity 'new (ebox-test-text "new")))) nil t) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (error "reject final accept")))) (should-error (ebox-commit buffer candidate (lambda (_report) (push 'publish trace)) (lambda (_report) (push 'rollback trace))))) (should (equal trace '(rollback publish))) (should (eq (ebox--buffer-render-state buffer) old-state)) (should (eq (plist-get old-state :root-node) old-root)) (should (eq (plist-get old-state :range-ref-table) old-table)) (should (= (gethash 'old (ebox-child-range-test--host-ref-table old-state)) old-id)) (should (equal (with-current-buffer buffer (buffer-string)) old-buffer))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new (ebox-test-text "new"))))) (ebox-commit buffer candidate))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest ebox-child-range-framework-failure-rolls-back-and-retries () "A failed framework publish restores the exact old Range generation." (let ((buffer (generate-new-buffer " *ebox-range-rollback*"))) (unwind-protect (progn (ebox-render-to-buffer buffer (ebox-test-column (ebox-test-box :key 'static (ebox-test-text "s")) (ebox-test-child-range 'items (ebox-test-box :key 'old (ebox-test-text "old"))))) (let* ((old-state (ebox--buffer-render-state buffer)) (old-root (plist-get old-state :root-node)) (old-range-table (plist-get old-state :range-ref-table)) (old-buffer (with-current-buffer buffer (buffer-string))) (candidate (ebox-candidate-begin buffer)) trace) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new (ebox-test-text "new"))))) (should-error (ebox-commit buffer candidate (lambda (_report) (push 'publish trace) (error "reject")) (lambda (_report) (push 'rollback trace)))) (should (equal trace '(rollback publish))) (should (eq (ebox--buffer-render-state buffer) old-state)) (should (eq (plist-get old-state :root-node) old-root)) (should (eq (plist-get old-state :range-ref-table) old-range-table)) (should (equal (with-current-buffer buffer (buffer-string)) old-buffer))) (let ((candidate (ebox-candidate-begin buffer))) (ebox-candidate-replace-range-ref candidate 'items (ebox-child-range-test--input (list (ebox-test-box :key 'new (ebox-test-text "new"))))) (ebox-commit buffer candidate)) (should (string-match-p "new" (with-current-buffer buffer (buffer-string))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (provide 'ebox-child-range-tests) ;;; ebox-child-range-tests.el ends here