;;; tp-surface-tests.el --- Tests for TP retained surfaces -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Geekinney ;;; Commentary: ;; Contract tests for TP 1.0 plans, objects, mounts, and publication. ;;; Code: (require 'ert) (require 'tp-surface) (defmacro tp-surface-test--with-buffer (&rest body) "Run BODY in a temporary live buffer." (declare (indent 0) (debug t)) `(let ((buffer (generate-new-buffer " *tp-surface-test*"))) (unwind-protect (with-current-buffer buffer ,@body) (when (buffer-live-p buffer) (kill-buffer buffer))))) (defun tp-surface-test--leaf (key text &optional props) "Return a content leaf with KEY, TEXT, and PROPS." (tp-surface-plan-create :key key :kind 'text :text text :props props :capability 'content)) (ert-deftest tp-surface-test-native-callback-replacement-preserves-identity () "Different equal-shaped callbacks publish through text and contribution paths." (dolist (as-text '(nil t)) (tp-surface-test--with-buffer (let* ((factory (eval '(lambda () (let ((n 0)) (lambda (_w _o _p) (setq n (1+ n))))) t)) (first (funcall factory)) (second (funcall factory)) (plan (lambda (callback) (tp-surface-test--leaf 'root (if as-text (propertize "text" 'help-echo callback) "text") (unless as-text (list 'help-echo callback))))) (surface (tp-surface-mount (current-buffer) (funcall plan first) '(:capability content)))) (should (equal first second)) (should-not (eq first second)) (tp-surface-update surface (funcall plan second)) (should (= (tp-surface-revision surface) 2)) (should (eq (get-text-property (point-min) 'help-echo) second)) (should (= (funcall first nil nil nil) 1)) (should (= (funcall second nil nil nil) 1)))))) (ert-deftest tp-surface-test-native-keymap-prompts-publish-and-respect-scope () "Copied maps allow local publication; changed nested prompts stay in scope." (dolist (as-text '(nil t)) (dolist (nested '(nil t)) (tp-surface-test--with-buffer (let ((prompt "Before") (middle "B") middle-object) (let* ((make-map (lambda () (let ((map (make-sparse-keymap prompt))) (define-key map (kbd "RET") #'ignore) (if nested (let ((outer (make-sparse-keymap "Outer"))) (define-key outer [prefix] map) outer) map)))) (producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (let ((map (funcall make-map))) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'middle middle) (tp-surface-test--leaf 'right (if as-text (propertize "C" 'keymap map) "C") (unless as-text (list 'keymap map)))))))) (surface (tp-surface-mount buffer producer '(:capability content)))) (setq middle "M") (tp-surface-update-scoped surface (list middle-object) producer) (should (equal (buffer-substring-no-properties 1 3) "MC")) (setq prompt "After" middle "N") (should-error (tp-surface-update-scoped surface (list middle-object) producer) :type 'tp-scope-mismatch) (should (= (tp-surface-revision surface) 2)) (should (equal (buffer-substring-no-properties 1 3) "MC")) (tp-surface-update surface producer) (let* ((map (get-text-property 2 'keymap)) (target (if nested (lookup-key map [prefix]) map))) (should (equal (keymap-prompt target) "After"))))))))) (ert-deftest tp-surface-test-native-hover-group-survives-local-publication () "Scoped text and property edits preserve groups without merging equal faces." (dolist (scoped '(nil t)) (tp-surface-test--with-buffer (let ((middle "b") (help "old") logical) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'text)) (hover (list :background "blue")) (text (concat (propertize (concat "a" middle "c") 'mouse-face hover) (propertize "z" 'mouse-face (copy-sequence hover))))) (setq logical (tp-object-ensure context root 'logical 'item)) (tp-object-attach-content-range context logical root 1 (1+ (length middle))) (put-text-property 1 (1+ (length middle)) 'help-echo help text) (tp-surface-test--leaf 'root text)))) (surface (tp-surface-mount buffer producer '(:capability content)))) (dolist (operation '(property text rollback)) (let ((before (buffer-string)) (revision (tp-surface-revision surface))) (setq help (symbol-name operation)) (when (eq operation 'text) (setq middle "longer")) (cl-labels ((update () (if scoped (tp-surface-update-scoped surface (list logical) producer) (tp-surface-update surface producer)))) (if (eq operation 'rollback) (let ((tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'property) (error "Reject hover"))))) (should-error (update)) (should (= revision (tp-surface-revision surface))) (dotimes (position (length before)) (should (eq (get-text-property position 'mouse-face before) (get-text-property (1+ position) 'mouse-face))))) (update)))) (let ((hover (get-text-property 1 'mouse-face)) (last (+ 2 (length middle)))) (cl-loop for position from 1 to last do (should (eq hover (get-text-property position 'mouse-face)))) (should (equal hover (get-text-property (1+ last) 'mouse-face))) (should-not (eq hover (get-text-property (1+ last) 'mouse-face)))))))))) (ert-deftest tp-surface-test-scoped-hover-cannot-regroup-outside-groups () "Equal paint does not authorize merging or splitting groups outside scope." (dolist (initial '(nil t)) (tp-surface-test--with-buffer (let ((unify initial) logical) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'text)) (hover (list :background "blue")) (text (concat (propertize "ab" 'mouse-face hover) (propertize "c" 'mouse-face (if unify hover (copy-sequence hover)))))) (setq logical (tp-object-ensure context root 'logical 'item)) (tp-object-attach-content-range context logical root 1 2) (put-text-property 1 2 'help-echo (if unify "new" "old") text) (tp-surface-test--leaf 'root text)))) (surface (tp-surface-mount buffer producer '(:capability content))) (left (get-text-property 1 'mouse-face)) (right (get-text-property 3 'mouse-face))) (setq unify (not initial)) (should-error (tp-surface-update-scoped surface (list logical) producer) :type 'tp-scope-mismatch) (should (= (tp-surface-revision surface) 1)) (should (eq left (get-text-property 1 'mouse-face))) (should (eq right (get-text-property 3 'mouse-face)))))))) (ert-deftest tp-surface-test-scoped-hover-checks-entire-outside-gap () "A remote same-paint group change is outside scope even with fresh snapshots." (dolist (initial '(nil t)) (tp-surface-test--with-buffer (let ((unify initial) logical) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'text)) (hover (list :background "blue")) (text (concat "abc" (propertize "d" 'mouse-face hover) (propertize "e" 'mouse-face (if unify hover (copy-sequence hover)))))) (setq logical (tp-object-ensure context root 'logical 'item)) (tp-object-attach-content-range context logical root 1 2) (put-text-property 1 2 'help-echo (if unify "new" "old") text) (tp-surface-test--leaf 'root text)))) (surface (tp-surface-mount buffer producer '(:capability content)))) (setq unify (not initial)) (should-error (tp-surface-update-scoped surface (list logical) producer) :type 'tp-scope-mismatch) (should (= (tp-surface-revision surface) 1))))))) (ert-deftest tp-surface-test-scoped-hover-does-not-join-separated-outside-gaps () "A scope with no hover separates equal outside faces regardless of identity." (tp-surface-test--with-buffer (let (separate logical) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'text)) (hover (list :background "blue")) (text (concat (propertize "a" 'mouse-face hover) (propertize "b" 'help-echo (if separate "new" "old")) (propertize "c" 'mouse-face (if separate (copy-sequence hover) hover))))) (setq logical (tp-object-ensure context root 'logical 'item)) (tp-object-attach-content-range context logical root 1 2) (tp-surface-test--leaf 'root text)))) (surface (tp-surface-mount buffer producer '(:capability content)))) (setq separate t) (tp-surface-update-scoped surface (list logical) producer) (should (= (tp-surface-revision surface) 2)) (should (equal (get-text-property 2 'help-echo) "new")) (should-not (get-text-property 2 'mouse-face)) (should (equal (get-text-property 1 'mouse-face) (get-text-property 3 'mouse-face))))))) (defun tp-surface-test--producer (signal) "Return a retained content producer reading SIGNAL." (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-result-create (tp-surface-test--leaf 'root (number-to-string (tp-signal-read signal))) (list :value (tp-signal-peek signal))))) (defun tp-surface-test--capture-condition (function) "Call FUNCTION and return its error or quit condition." (condition-case condition (progn (funcall function) nil) (error condition) (quit condition))) (defun tp-surface-test--commit-batch-update (surface root batch client-state before-stage &optional project-coordinates) "Update SURFACE through BATCH after calling BEFORE-STAGE with its entry. ROOT is the retained object and CLIENT-STATE is transferred by its active prepare context. PROJECT-COORDINATES asks TP to map existing mounts through the batch instead of retaining their unchanged coordinates." (let ((execute (symbol-function 'tp--publication-batch-execute-stage))) (cl-letf (((symbol-function 'tp--publication-batch-execute-stage) (lambda (candidate) (let* ((entry (car (tp-publication-batch-candidate-entries candidate))) (prepared (aref (tp-publication-target-entry-rollback-snapshot entry) 0))) (funcall before-stage entry prepared) (funcall execute candidate))))) (tp-surface-update surface (lambda (context) (tp-object-reuse-subtree context root) (tp-commit-batch-result-create context batch :client-state client-state :reuse-mount-projection (not project-coordinates))))))) (defun tp-surface-test--replacement-batch (surface replacement) "Return an equal-extent batch replacing SURFACE's middle character." (tp-commit-batch-create :base-revision (tp-surface-revision surface) :target-revision (1+ (tp-surface-revision surface)) :base-extent 3 :target-extent 3 :patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 2 :replacement replacement)) :coordinate-patches '((:old-start 1 :old-end 2 :new-start 1 :new-end 2)))) (defun tp-surface-test--shadow-apply-commit-batch-oracle (string batch) "Return the former exact replay result for STRING and BATCH." (let ((result (copy-sequence string))) (dolist (patch (reverse (tp-commit-batch-patches batch))) (setq result (concat (substring result 0 (plist-get patch :old-start)) (plist-get patch :replacement) (substring result (plist-get patch :old-end))))) result)) (defun tp-surface-test--source-function (name) "Return interpreted NAME from the current tp-surface.el source." (let ((source (expand-file-name "tp-surface.el" (file-name-directory (or (locate-library "tp-surface") (error "Cannot locate tp-surface"))))) definition) (with-temp-buffer (insert-file-contents source) (condition-case nil (while (not definition) (let ((form (read (current-buffer)))) (when (and (eq (car-safe form) 'defun) (eq (cadr form) name)) (setq definition form)))) (end-of-file nil))) (unless definition (error "Cannot find source definition for %S" name)) (eval `(lambda ,(nth 2 definition) ,@(nthcdr 3 definition)) t))) (defun tp-surface-test--update-mode (mode surface object plan signal value) "Update SURFACE in MODE with PLAN while touching SIGNAL to VALUE." (tp-with-transaction (tp-signal-set signal value) (if (eq mode 'full) (tp-surface-update surface plan) (tp-surface-update-scoped surface (list object) plan)))) (defvar-local tp-surface-test--corrupt-next nil "When non-nil, the test after-change hook corrupts one inserted range.") (defun tp-surface-test--corrupt-after-change (beg end _old-length) "Corrupt one inserted range for publication rollback tests." (when (and tp-surface-test--corrupt-next (< beg end)) (setq-local tp-surface-test--corrupt-next nil) (with-silent-modifications (put-text-property beg end 'face 'corrupt)))) (ert-deftest tp-surface-test-commit-batch-is-atomic-and-revision-bound () "A precomputed batch commits text/properties/state once and rolls back." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount (current-buffer) (tp-surface-test--leaf 'root "abc" '(face bold)) '(:capability content))) (replacement (propertize "XY" 'face 'italic)) (batch (tp-commit-batch-create :base-revision (tp-surface-revision surface) :target-revision (1+ (tp-surface-revision surface)) :base-extent 3 :target-extent 4 :patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 3 :replacement replacement)) :coordinate-patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 3)) :client-state '(:value next)))) (tp-surface-commit-batch surface batch) (should (equal "aXYc" (buffer-string))) (should (eq 'italic (get-text-property 2 'face))) (should (= 2 (tp-surface-revision surface))) (should (equal '(:value next) (tp-surface-client-state surface))) (should (plist-get (tp-surface-report surface) :commit-batch)) (let ((failing (tp-commit-batch-create :base-revision 2 :target-revision 3 :base-extent 4 :target-extent 4 :patches (list (list :old-start 1 :old-end 3 :new-start 1 :new-end 3 :replacement "ZZ")) :client-state '(:value rejected)))) (add-hook 'after-change-functions #'tp-surface-test--corrupt-after-change nil t) (setq-local tp-surface-test--corrupt-next t) (unwind-protect (should-error (tp-surface-commit-batch surface failing)) (remove-hook 'after-change-functions #'tp-surface-test--corrupt-after-change t)) (should (equal "aXYc" (buffer-string))) (should (= 2 (tp-surface-revision surface))) (should (equal '(:value next) (tp-surface-client-state surface)))) (should-error (tp-surface-commit-batch surface batch))))) (ert-deftest tp-surface-test-commit-batch-composes-patch-properties () "Patch-local contributions use the retained-content property composer." (let* ((replacement (propertize "abcd" 'face '(bold) 'help-echo "baseline")) (before (copy-sequence replacement)) (contributions '((:start 0 :end 4 :props (face (:background "#112233"))) (:start 1 :end 3 :props (face italic help-echo "inner")) (:start 2 :end 3 :props (face nil help-echo nil)))) (patch (list :old-start 1 :old-end 5 :new-start 1 :new-end 5 :replacement replacement :property-contributions contributions)) (expected (tp--compose-relative-property-contributions replacement contributions)) (batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 6 :target-extent 6 :patches (list patch))) (stored (car (tp-commit-batch-patches batch)))) (should (equal-including-properties before replacement)) (should (eq replacement (plist-get patch :replacement))) (should (eq contributions (plist-get patch :property-contributions))) (should (equal-including-properties expected (plist-get stored :replacement))) (should-not (eq replacement (plist-get stored :replacement))) (should-not (plist-member stored :property-contributions)))) (ert-deftest tp-surface-test-commit-batch-freezes-property-policy-at-construction () "Composition snapshots inputs and evaluates custom merge policy exactly once." (let ((tp--property-policies (copy-hash-table tp--property-policies)) (tp--property-policy-order (copy-sequence tp--property-policy-order)) (merge-calls 0)) (tp-define-property-policy 'text/tp-batch-score :equality #'equal :merge (lambda (old new) (cl-incf merge-calls) (+ old new)) :projector (lambda (value) (list 'tp-batch-score value))) (let* ((replacement (propertize "AB" 'tp-batch-score 10 'face '(:weight bold))) (before (copy-sequence replacement)) (before-props (copy-tree (text-properties-at 0 replacement))) (contributions (list (list :start 0 :end 2 :props (list 'tp-batch-score 1)) (list :start 1 :end 2 :props (list 'tp-batch-score 2)))) (before-contributions (copy-tree contributions)) (patch (list :old-start 0 :old-end 2 :new-start 0 :new-end 2 :replacement replacement :property-contributions contributions)) (expected (tp--compose-relative-property-contributions replacement contributions)) batch) (setq merge-calls 0 batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 2 :target-extent 2 :patches (list patch))) (should (= merge-calls 2)) (should (equal-including-properties before replacement)) (should (equal before-props (text-properties-at 0 replacement))) (should (equal before-contributions contributions)) (should (eq contributions (plist-get patch :property-contributions))) (should (eq replacement (plist-get patch :replacement))) (tp-define-property-policy 'text/tp-batch-score :equality #'equal :merge (lambda (_old _new) (error "Policy was re-evaluated after construction")) :projector (lambda (value) (list 'tp-batch-score value))) (tp-surface-test--with-buffer (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "zz") '(:capability content)))) (tp-surface-commit-batch surface batch) (should (equal-including-properties expected (buffer-string))) (should (= (get-text-property 1 'tp-batch-score) 11)) (should (= (get-text-property 2 'tp-batch-score) 13)) (should (= merge-calls 2))))))) (ert-deftest tp-surface-test-commit-batch-isolates-mutable-property-inputs () "Composition owns mutable inputs while preserving opaque function identity." (let ((tp--property-policies (copy-hash-table tp--property-policies)) (tp--property-policy-order (copy-sequence tp--property-policy-order))) (tp-define-property-policy 'text/tp-batch-destructive :merge (lambda (old new) (setcar old 'merged-old) (setcar new 'merged-new) (list old new))) (let* ((callback (lambda () 'opaque)) (baseline (list 'baseline)) (incoming (list 'incoming)) (mutable (vector (list (copy-sequence "caller")) callback)) (replacement (propertize "X" 'tp-batch-destructive baseline 'tp-batch-mutable 'existing)) (contributions (list (list :start 0 :end 1 :props (list 'tp-batch-mutable mutable 'tp-batch-destructive incoming)))) (patch (list :old-start 0 :old-end 1 :new-start 0 :new-end 1 :replacement replacement :property-contributions contributions)) (batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 1 :target-extent 1 :patches (list patch))) (stored (plist-get (car (tp-commit-batch-patches batch)) :replacement)) (stored-mutable (get-text-property 0 'tp-batch-mutable stored))) (should (equal baseline '(baseline))) (should (equal incoming '(incoming))) (should (equal (aref mutable 0) '("caller"))) (should (eq callback (aref mutable 1))) (should (eq baseline (get-text-property 0 'tp-batch-destructive replacement))) (should (eq 'existing (get-text-property 0 'tp-batch-mutable replacement))) (should (eq replacement (plist-get patch :replacement))) (should (eq contributions (plist-get patch :property-contributions))) (should (eq callback (aref stored-mutable 1))) (aset (car (aref mutable 0)) 0 ?X) (setcdr (aref mutable 0) '(changed)) (aset mutable 1 #'ignore) (setcar baseline 'changed-baseline) (setcar incoming 'changed-incoming) (should (equal (aref stored-mutable 0) '("caller"))) (should (eq callback (aref stored-mutable 1))) (should (equal (get-text-property 0 'tp-batch-destructive stored) '((merged-old) (merged-new))))))) (ert-deftest tp-surface-test-commit-batch-validates-local-contributions () "Contribution bounds and property shape are validated inside each patch." (dolist (case '((tp-invalid-content-range ((:start -1 :end 1 :props (face bold)))) (tp-invalid-content-range ((:start 0 :end 3 :props (face bold)))) (tp-invalid-content-range ((:start 2 :end 1 :props (face bold)))) (tp-invalid-content-range ((:start "0" :end 1 :props (face bold)))) (tp-invalid-content-range ((:start 0 :props (face bold)))) (tp-invalid-content-range not-a-list) (tp-invalid-surface-plan ((:start 0 :end 1 :props (face)))))) (ert-info ((format "invalid patch contribution=%S" (cadr case))) (should-error (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 6 :target-extent 6 :patches (list (list :old-start 2 :old-end 4 :new-start 2 :new-end 4 :replacement "XY" :property-contributions (cadr case)))) :type (car case))))) (ert-deftest tp-surface-test-commit-batch-composed-properties-roll-back-and-retry () "Direct and retained batches atomically publish composed properties and state." (dolist (mode '(direct retained)) (ert-info ((format "composed batch mode=%S" mode)) (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-result-create (tp-surface-test--leaf 'root "abc" '(face bold help-echo "base")) '(:value old))) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (before (buffer-string)) (old-state (tp-surface-client-state surface)) (mounts (tp--surface-mounts surface)) (index (tp--surface-mount-index surface)) (ids (tp--live-mount-ids surface)) (next-state (list :value 'next)) (replacement (propertize "X" 'face 'bold 'help-echo "base")) (contributions '((:start 0 :end 1 :props (face (:background "#112233") help-echo nil)))) (expected (concat (substring before 0 1) (tp--compose-relative-property-contributions replacement contributions) (substring before 2))) (batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 3 :patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 2 :replacement replacement :property-contributions contributions)) :client-state next-state))) (cl-labels ((update () (if (eq mode 'direct) (tp-surface-commit-batch surface batch) (tp-surface-test--commit-batch-update surface root batch next-state #'ignore)))) (cl-letf (((symbol-function 'accept-change-group) (lambda (_) (error "Reject composed batch")))) (should (equal (should-error (update)) '(error "Reject composed batch")))) (should (= (tp-surface-revision surface) 1)) (should (eq old-state (tp-surface-client-state surface))) (should (eq mounts (tp--surface-mounts surface))) (should (eq index (tp--surface-mount-index surface))) (should (equal ids (tp--live-mount-ids surface))) (should (equal-including-properties before (buffer-string))) (update)) (should (= (tp-surface-revision surface) 2)) (should (eq (tp-surface-client-state surface) (if (eq mode 'direct) (tp-commit-batch-client-state batch) next-state))) (should (eq mounts (tp--surface-mounts surface))) (should (eq index (tp--surface-mount-index surface))) (should (equal ids (tp--live-mount-ids surface))) (should (equal-including-properties expected (buffer-string)))))))) (ert-deftest tp-surface-test-retained-batch-shadow-matches-commit-and-rollback () "Implicit retained mounts have exact success and rollback shadow artifacts." (dolist (coordinate-shift '(nil t)) (dolist (phase '(rollback commit)) (ert-info ((format "retained batch phase=%S coordinate-shift=%S" phase coordinate-shift)) (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "a") (tp-surface-test--leaf 'right "bc"))) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (mounts (tp--surface-mounts surface)) (index (tp--surface-mount-index surface)) (ids (tp--live-mount-ids surface)) (batch (if coordinate-shift (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 3 :patches '((:old-start 0 :old-end 3 :new-start 0 :new-end 3 :replacement "XYZ")) :coordinate-patches '((:old-start 0 :old-end 1 :new-start 0 :new-end 2) (:old-start 1 :old-end 3 :new-start 2 :new-end 3))) (tp-surface-test--replacement-batch surface "X"))) entry) (cl-labels ((update () (tp-surface-test--commit-batch-update surface root batch '(:owned next) (lambda (target prepared) (setq entry target) (should (tp--prepared-surface-retained-mount-state-p prepared)) (should (eq (not coordinate-shift) (null (tp--prepared-surface-mount-specs prepared)))) (should (eq (not coordinate-shift) (zerop (length (tp--prepared-surface-mount-coordinate-updates prepared)))))) coordinate-shift))) (if (eq phase 'rollback) (let ((tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'client-state) (error "Reject retained batch shadow"))))) (should (equal (should-error (update)) '(error "Reject retained batch shadow")))) (update))) (should (equal (buffer-string) (if (eq phase 'rollback) "abc" (if coordinate-shift "XYZ" "aXc")))) (should (eq mounts (tp--surface-mounts surface))) (should (eq index (tp--surface-mount-index surface))) (should (equal ids (tp--live-mount-ids surface))) (let ((shadow (funcall (tp-publication-target-entry-shadow-validator entry) entry phase))) (ert-info ((format "expected mount count=%S actual mount count=%S" (length (plist-get (plist-get shadow :expected) :mounts)) (length (plist-get (plist-get shadow :actual) :mounts)))) (should (plist-get shadow :equivalent)))) (should (eq phase (plist-get tp--last-shadow-proof :phase))) (should (plist-get tp--last-shadow-proof :equivalent)))))))) (ert-deftest tp-surface-test-publication-entry-adopts-prepared-authority () "A real surface entry references its authenticated prepared values exactly." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount (current-buffer) (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (tp-surface-test--replacement-batch surface "X")) (client-state (list :owned (list 'next))) entry prepared) (tp-surface-test--commit-batch-update surface root batch client-state (lambda (target candidate) (setq entry target prepared candidate))) (should (eq (tp-publication-target-entry-client-state entry) (tp--prepared-surface-client-state prepared))) (should (eq (tp-publication-target-entry-diff entry) (tp--prepared-surface-commit-batch prepared))) (should (eq (tp-surface-client-state surface) client-state)) (should (equal (buffer-string) "aXc"))))) (ert-deftest tp-surface-test-publication-entry-rejects-binding-tamper () "Prepared batch, revision, extent, and client identity drift fail before write." (dolist (fault '(batch client-state context-inactive context-surface base-revision target-revision base-extent target-extent)) (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount (current-buffer) (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (tp-surface-test--replacement-batch surface "X")) (client-state (list :owned (list 'next))) captured-context) (should-error (tp-surface-test--commit-batch-update surface root batch client-state (lambda (entry prepared) (setq captured-context (tp--prepared-surface-context prepared)) (pcase fault ('batch (setf (tp--prepared-surface-commit-batch prepared) (tp-surface-test--replacement-batch surface "X"))) ('client-state (setf (tp--prepared-surface-client-state prepared) (copy-tree client-state))) ('context-inactive (setf (tp--context-active captured-context) nil)) ('context-surface (setf (tp--context-surface captured-context) nil)) ('base-revision (setf (tp-commit-batch-base-revision batch) 0)) ('target-revision (setf (tp-commit-batch-target-revision batch) 9)) ('base-extent (setf (tp-commit-batch-base-extent batch) 2)) ('target-extent (setf (tp-commit-batch-target-extent batch) 4))) (should (tp-publication-target-entry-p entry)))) :type 'tp-publication-binding-error) (should (equal (buffer-string) "abc")) (should (= (tp-surface-revision surface) 1)) (should-not (tp-surface-client-state surface)) (should-not (tp--context-active captured-context)))))) (ert-deftest tp-surface-test-publication-entry-patch-tamper-stays-proven () "Existing full shadow proof diagnoses in-place patch payload mutation." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount (current-buffer) (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (tp-surface-test--replacement-batch surface "X"))) (tp-surface-test--commit-batch-update surface root batch '(:owned next) (lambda (_entry _prepared) (setf (plist-get (car (tp-commit-batch-patches batch)) :replacement) "Z"))) (should (equal (buffer-string) "aZc")) (should-not (plist-get tp--last-shadow-proof :equivalent)) (should (cl-some (lambda (entry) (and (eq (car entry) 'shadow-proof) (eq (cadr entry) 'commit))) tp--last-transaction-diagnostics))))) (ert-deftest tp-surface-test-retained-batch-avoids-full-plan-and-output-replay () "Revision-bearing batches bypass duplicate plan walks, even for no-op text." (dolist (replacement '("X" "b" nil)) (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (if replacement (tp-surface-test--replacement-batch surface replacement) (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 3)))) (cl-letf (((symbol-function 'tp--plan-equal-p) (lambda (&rest _) (ert-fail "Retained plan was traversed"))) ((symbol-function 'tp--shadow-apply-commit-batch-to-string) (lambda (&rest _) (ert-fail "Full output was rebuilt")))) (tp-surface-test--commit-batch-update surface root batch '(:owned next) (lambda (entry _prepared) (should (tp--shadow-batch-output-p (plist-get (plist-get (tp-publication-target-entry-shadow-expected entry) :commit) :output)))))) (should (= (tp-surface-revision surface) 2)) (should (equal (buffer-string) (if (equal replacement "X") "aXc" "abc"))) (should (plist-get tp--last-shadow-proof :equivalent)))))) (ert-deftest tp-surface-test-batch-writes-only-changed-characters () "Direct and retained batches retain anchors and write only the changed digit." (dolist (retained '(nil t)) (tp-surface-test--with-buffer (let* ((text (propertize "prefix 00 suffix" 'help-echo "old")) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root text) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (prefix-anchor (copy-marker 4)) (suffix-anchor (copy-marker 14)) changes) (unwind-protect (progn (goto-char 4) (add-hook 'before-change-functions (lambda (start end) (push (- end start) changes)) nil t) (let ((batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 16 :target-extent 16 :patches (list (list :old-start 0 :old-end 16 :new-start 0 :new-end 16 :replacement (propertize "prefix 01 suffix" 'help-echo "new")))))) (if retained (tp-surface-test--commit-batch-update surface root batch nil #'ignore) (tp-surface-commit-batch surface batch))) (should (= (point) 4)) (should (= (marker-position prefix-anchor) 4)) (should (= (marker-position suffix-anchor) 14)) (should (= (apply #'+ changes) 1)) (should (= (plist-get (tp-surface-report surface) :text-operations) 1)) (should (> (plist-get (tp-surface-report surface) :property-operations) 0)) (should (equal (buffer-string) "prefix 01 suffix")) (dotimes (position 16) (should (equal (get-text-property (1+ position) 'help-echo) "new")))) (set-marker prefix-anchor nil) (set-marker suffix-anchor nil)))))) (ert-deftest tp-surface-test-batch-property-only-final-accept-rolls-back () "Property-only batches journal unchanged characters for both public paths." (dolist (retained '(nil t)) (tp-surface-test--with-buffer (let* ((first (lambda (&rest _) "first")) (second (lambda (&rest _) "second")) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abc" 'help-echo first)) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (tp-surface-test--replacement-batch surface (propertize "b" 'help-echo second)))) (goto-char 2) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (error "Reject property batch")))) (should-error (if retained (tp-surface-test--commit-batch-update surface root batch nil #'ignore) (tp-surface-commit-batch surface batch)))) (should (= (point) 2)) (should (eq first (get-text-property 2 'help-echo))) (should (= (tp-surface-revision surface) 1)) (dolist (expected-property-operations '(1 0)) (let ((next (tp-surface-test--replacement-batch surface (propertize "b" 'help-echo second)))) (if retained (tp-surface-test--commit-batch-update surface root next nil #'ignore) (tp-surface-commit-batch surface next))) (should (= (plist-get (tp-surface-report surface) :text-operations) 0)) (should (= (plist-get (tp-surface-report surface) :property-operations) expected-property-operations))) (should (= (tp-surface-revision surface) 3)))))) (ert-deftest tp-surface-test-retained-batch-shadow-detects-unrecorded-outside-writes () "Outside writes remain diagnosed with undo and change hooks disabled." (dolist (side '(1 3)) (dolist (fault '(text property callback keymap)) (tp-surface-test--with-buffer (let* ((factory (eval '(lambda () (let ((n 0)) (lambda (&rest _) (setq n (1+ n))))) t)) (first (funcall factory)) (second (funcall factory)) (map (make-sparse-keymap "Before")) (_binding (define-key map [return] first)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abc" 'help-echo first 'keymap map)) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (tp-surface-test--replacement-batch surface (propertize "X" 'help-echo first 'keymap map))) (tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'client-state) (let ((inhibit-modification-hooks t) (buffer-undo-list t)) (pcase fault ('text (subst-char-in-region side (1+ side) (char-after side) ?Z)) ('property (put-text-property side (1+ side) 'alien t)) ('callback (put-text-property side (1+ side) 'help-echo second)) ('keymap (let ((other (make-sparse-keymap "After"))) (define-key other [return] second) (put-text-property side (1+ side) 'keymap other))))))))) (should (equal first second)) (should-not (eq first second)) (tp-surface-test--commit-batch-update surface root batch nil #'ignore) (should (= (tp-surface-revision surface) 2)) (should-not (plist-get tp--last-shadow-proof :equivalent))))))) (ert-deftest tp-surface-test-retained-shadow-patch-evidence-is-detached () "Staged string and native map mutations cannot rewrite expected evidence." (dolist (fault '(character callback keymap)) (tp-surface-test--with-buffer (let* ((callback (lambda (&rest _) "first")) (replacement-map (make-sparse-keymap "Before")) (_binding (define-key replacement-map [return] callback)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (batch (tp-surface-test--replacement-batch surface (propertize (copy-sequence "X") 'help-echo callback 'keymap replacement-map)))) (tp-surface-test--commit-batch-update surface root batch nil (lambda (_entry _prepared) (let ((replacement (plist-get (car (tp-commit-batch-patches batch)) :replacement))) (pcase fault ('character (aset replacement 0 ?Z)) ('callback (put-text-property 0 1 'help-echo #'ignore replacement)) ('keymap (define-key (get-text-property 0 'keymap replacement) [return] #'ignore)))))) (should-not (plist-get tp--last-shadow-proof :equivalent)))))) (ert-deftest tp-surface-test-retained-shadow-detects-same-paint-regrouping () "Retained proof rejects outside group changes without relying on undo hooks." (dolist (phase '(client-state cleanup)) (dolist (tamper '(nil gap seam)) (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abcde" 'mouse-face (list :background "blue"))) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (hover (get-text-property 1 'mouse-face)) (batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 5 :target-extent 5 :patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 2 :replacement (propertize "X" 'mouse-face hover))))) (mutate (lambda (step _state) (when (and tamper (eq step phase)) (let ((inhibit-modification-hooks t) (buffer-undo-list t) (start (if (eq tamper 'gap) 4 2))) (put-text-property start (1+ start) 'mouse-face (copy-sequence hover)))))) (tp--surface-publication-step-function mutate) (tp--surface-precommit-step-function mutate)) (if tamper (progn (should-error (tp-surface-test--commit-batch-update surface root batch nil #'ignore) :type 'tp-scope-mismatch) (should (= (tp-surface-revision surface) 1)) (should (equal (buffer-string) "abcde")) (dotimes (offset 5) (should (eq hover (get-text-property (1+ offset) 'mouse-face)))) (should (eq (plist-get tp--last-shadow-proof :phase) 'rollback))) (tp-surface-test--commit-batch-update surface root batch nil #'ignore) (should (= (tp-surface-revision surface) 2))) (should (plist-get tp--last-shadow-proof :equivalent))))))) (ert-deftest tp-surface-test-retained-batch-final-accept-restores-native-properties () "Final-accept error and quit restore native identity and retained side state." (dolist (injected '((error "Retained accept failure") (quit))) (tp-surface-test--with-buffer (let* ((first (lambda (&rest _) "first")) (second (lambda (&rest _) "second")) (map (make-sparse-keymap "Before")) (_binding (define-key map [return] #'ignore)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abc" 'help-echo first 'keymap map)) '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (mounts (tp--surface-mounts surface)) (index (tp--surface-mount-index surface)) (output (buffer-string)) (batch (tp-surface-test--replacement-batch surface (propertize "X" 'help-echo second 'keymap map))) failure) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (signal (car injected) (cdr injected))))) (setq failure (tp-surface-test--capture-condition (lambda () (tp-surface-test--commit-batch-update surface root batch '(:owned next) #'ignore))))) (should (equal failure injected)) (should (tp--text-property-semantic-equal-p output (buffer-string))) (should (eq first (get-text-property 2 'help-echo))) (should (eq mounts (tp--surface-mounts surface))) (should (eq index (tp--surface-mount-index surface))) (should (= (tp-surface-revision surface) 1)) (should-not (tp-surface-client-state surface)) (should (eq (plist-get tp--last-shadow-proof :phase) 'rollback)) (should (plist-get tp--last-shadow-proof :equivalent)) (tp-surface-test--commit-batch-update surface root batch '(:owned next) #'ignore) (should (eq second (get-text-property 2 'help-echo))) (should (= (tp-surface-revision surface) 2)) (should (plist-get tp--last-shadow-proof :equivalent)))))) (ert-deftest tp-surface-test-retained-shadow-range-proof-matches-full-replay () "Patch evidence covers all gaps, insertions, removals, and property boundaries." (dolist (specs '(((0 0 "α") (1 3 "XY") (6 6 "尾")) ((0 6 "") (6 6 "新值")) ((1 2 "") (2 2 "二") (4 5 "四五六")) ((2 2 "一") (2 2 "二")) nil)) (let ((base (concat (propertize "abc" 'face '(bold)) (propertize "def" 'face '(italic)))) (old-position 0) (new-position 0) patches) (dolist (spec specs) (pcase-let* ((`(,start ,end ,text) spec) (new-start (+ new-position (- start old-position))) (new-end (+ new-start (length text)))) (push (list :old-start start :old-end end :new-start new-start :new-end new-end :replacement (propertize text 'face '(underline))) patches) (setq old-position end new-position new-end))) (setq patches (nreverse patches)) (let* ((extent (+ new-position (- (length base) old-position))) (batch (tp-commit-batch-create :base-revision 0 :target-revision 1 :base-extent 6 :target-extent extent :patches patches :coordinate-patches (mapcar (lambda (patch) (list :old-start (plist-get patch :old-start) :old-end (plist-get patch :old-end) :new-start (plist-get patch :new-start) :new-end (plist-get patch :new-end))) patches))) (expected (tp--shadow-batch-output-create base batch)) (actual (tp-surface-test--shadow-apply-commit-batch-oracle base batch))) (should (tp--shadow-batch-output-equal-p expected actual)) (with-temp-buffer (insert "prefix" actual "suffix") (should (tp--shadow-batch-output-equal-p expected (current-buffer) 7 (+ 7 extent)))) (dotimes (position extent) (let ((changed (copy-sequence actual))) (put-text-property position (1+ position) 'alien t changed) (should-not (tp--shadow-batch-output-equal-p expected changed)) (with-temp-buffer (insert "prefix" changed "suffix") (should-not (tp--shadow-batch-output-equal-p expected (current-buffer) 7 (+ 7 extent)))))) (should-not (tp--shadow-batch-output-equal-p expected (concat actual "!"))))))) (ert-deftest tp-surface-test-retained-shadow-reuses-only-verified-output () "Verified evidence avoids copying output; opaque mismatches retain diagnostics." (tp-surface-test--with-buffer (let* ((factory (eval '(lambda () (let ((n 0)) (lambda (&rest _) (setq n (1+ n))))) t)) (first (funcall factory)) (second (funcall factory)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abc" 'help-echo first)) '(:capability content :coordinate-mounts t))) (expected (tp--shadow-batch-output-create (buffer-string) (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 3)))) (cl-letf (((symbol-function 'tp--shadow-surface-output) (lambda (_) (ert-fail "Verified output was copied again")))) (should (eq expected (plist-get (tp--shadow-current-artifact surface expected) :output)))) (should (equal first second)) (should-not (eq first second)) (let ((inhibit-modification-hooks t) (buffer-undo-list t)) (put-text-property 3 4 'help-echo second)) (let* ((actual (tp--shadow-current-artifact surface expected)) (target (plist-put (copy-sequence actual) :output expected))) (should (stringp (plist-get actual :output))) (should (eq second (get-text-property 2 'help-echo (plist-get actual :output)))) (should-not (tp--shadow-artifact-equal-p target actual)))))) (ert-deftest tp-surface-test-retained-shadow-verifies-custom-policy-and-buffer-tick () "Custom policy equality is honored without reusing evidence across its writes." (tp-surface-test--with-buffer (let* ((property (make-symbol "shadow-custom-policy")) mutate (_policy (tp-define-property-policy (tp-text-property-id property) :equality (lambda (a b) (when mutate (let ((inhibit-modification-hooks t) (buffer-undo-list t)) (put-text-property 1 2 'alien t buffer))) (equal (downcase a) (downcase b))))) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abc" property "OLD")) '(:capability content :coordinate-mounts t))) (expected (tp--shadow-batch-output-create (buffer-string) (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 3)))) (let ((inhibit-modification-hooks t)) (put-text-property 1 4 property "old")) (let* ((actual (tp--shadow-current-artifact surface expected)) (target (plist-put (copy-sequence actual) :output expected))) (should (stringp (plist-get actual :output))) (should (tp--shadow-artifact-equal-p target actual))) (setq mutate t) (let* ((actual (tp--shadow-current-artifact surface expected)) (target (plist-put (copy-sequence actual) :output expected))) (should (stringp (plist-get actual :output))) (should (get-text-property 0 'alien (plist-get actual :output))) (should-not (tp--shadow-artifact-equal-p target actual)))))) (ert-deftest tp-surface-test-retained-shadow-isolates-custom-policy-evidence () "A custom policy cannot alias actual evidence by retaining its expected value." (dolist (when-to-mutate '(during after)) (tp-surface-test--with-buffer (let* ((property (make-symbol "shadow-retained-evidence")) captured (_policy (tp-define-property-policy (tp-text-property-id property) :equality (lambda (a b) (prog1 (equal (downcase a) (downcase b)) (setq captured a) (when (eq when-to-mutate 'during) (aset a 0 ?X)))))) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root (propertize "abc" property (copy-sequence "OLD"))) '(:capability content :coordinate-mounts t))) (expected (tp--shadow-batch-output-create (buffer-string) (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 3)))) (let ((inhibit-modification-hooks t)) (put-text-property 1 4 property "old")) (let* ((tick (buffer-modified-tick)) (actual (tp--shadow-current-artifact surface expected))) (should (stringp (plist-get actual :output))) (should captured) (when (eq when-to-mutate 'after) (aset captured 0 ?X)) (should (= tick (buffer-modified-tick))) (should (equal (get-text-property 0 property (plist-get actual :output)) "old")) (should-not (tp--shadow-artifact-equal-p (plist-put (copy-sequence actual) :output expected) actual))))))) (ert-deftest tp-surface-test-shadow-batch-replay-preserves-exact-properties () "Shadow replay preserves Unicode, properties, order, and input ownership." (let* ((opaque (make-symbol "owner")) (base (propertize (concat "A" "乙丙丁戊") 'owner opaque 'face '(bold))) (insertion (propertize (concat "a" "β") 'owner opaque 'face '(italic))) (replacement (propertize (copy-sequence "Q") 'owner opaque 'display '(space :width 2))) (batch (tp-commit-batch-create :base-revision 0 :target-revision 1 :base-extent 5 :target-extent 5 :patches (list (list :old-start 0 :old-end 0 :new-start 0 :new-end 2 :replacement insertion) (list :old-start 1 :old-end 3 :new-start 3 :new-end 3 :replacement "") (list :old-start 4 :old-end 5 :new-start 4 :new-end 5 :replacement replacement)))) (expected (concat insertion (substring base 0 1) (substring base 3 4) replacement)) (result (tp--shadow-apply-commit-batch-to-string base batch))) (should (equal-including-properties expected result)) (should (eq opaque (get-text-property 0 'owner result))) (aset base 0 ?X) (aset insertion 0 ?z) (aset replacement 0 ?R) (should (equal-including-properties expected result)))) (ert-deftest tp-surface-test-shadow-batch-replay-preserves-adjacent-inserts () "Zero-width patches at one old coordinate retain declared list order." (let* ((first (propertize "α" 'face 'bold)) (second (propertize "乙" 'face 'italic)) (batch (tp-commit-batch-create :base-revision 0 :target-revision 1 :base-extent 0 :target-extent 2 :patches (list (list :old-start 0 :old-end 0 :new-start 0 :new-end 1 :replacement first) (list :old-start 0 :old-end 0 :new-start 1 :new-end 2 :replacement second)) :coordinate-patches '((:old-start 0 :old-end 0 :new-start 0 :new-end 1) (:old-start 0 :old-end 0 :new-start 1 :new-end 2))))) (let ((result (tp--shadow-apply-commit-batch-to-string "" batch))) (should (equal-including-properties (concat first second) result)) (should (eq (get-text-property 0 'face result) 'bold)) (should (eq (get-text-property 1 'face result) 'italic))))) (ert-deftest tp-surface-test-shadow-batch-replay-no-patches-copies-input () "No-patch shadow replay returns an independently mutable string." (let* ((base (copy-sequence "value")) (batch (tp-commit-batch-create :base-revision 0 :target-revision 1 :base-extent 5 :target-extent 5)) (result (tp--shadow-apply-commit-batch-to-string base batch))) (should (equal base result)) (should-not (eq base result)) (aset result 0 ?X) (should (equal base "value")))) (ert-deftest tp-surface-test-shadow-batch-replay-matches-oracle () "Single-pass replay matches the former exact algorithm over varied patches." (let ((cases '(("abcdef" ((0 0 "α") (1 3 "XY") (6 6 "尾"))) ("abcdef" ((0 6 "") (6 6 "新值"))) ("abcdef" ((1 2 "") (2 2 "二") (4 5 "四五六")))))) (dolist (case cases) (let ((old-cursor 0) (new-cursor 0) patches) (dolist (spec (cadr case)) (pcase-let ((`(,old-start ,old-end ,replacement) spec)) (let ((new-start (+ new-cursor (- old-start old-cursor))) (new-end (+ new-cursor (- old-start old-cursor) (length replacement)))) (push (list :old-start old-start :old-end old-end :new-start new-start :new-end new-end :replacement replacement) patches) (setq old-cursor old-end new-cursor new-end)))) (setq patches (nreverse patches)) (let* ((base (car case)) (target-extent (+ new-cursor (- (length base) old-cursor))) (coordinates (mapcar (lambda (patch) (list :old-start (plist-get patch :old-start) :old-end (plist-get patch :old-end) :new-start (plist-get patch :new-start) :new-end (plist-get patch :new-end))) patches)) (batch (tp-commit-batch-create :base-revision 0 :target-revision 1 :base-extent (length base) :target-extent target-extent :patches patches :coordinate-patches coordinates))) (should (equal-including-properties (tp-surface-test--shadow-apply-commit-batch-oracle base batch) (tp--shadow-apply-commit-batch-to-string base batch)))))))) (ert-deftest tp-surface-test-shadow-batch-replay-work-is-linear () "Replay slices the base once and concatenates exactly the target extent." (let* ((patch-count 512) (base (make-string (* patch-count 2) ?a)) patches (old 0) (new 0)) (dotimes (index patch-count) (push (list :old-start old :old-end (1+ old) :new-start new :new-end (+ new 2) :replacement (if (zerop (% index 2)) "甲乙" "αβ")) patches) (setq old (+ old 2) new (+ new 3))) (let* ((patches (nreverse patches)) (target-extent (+ new (- (length base) old))) (coordinates (mapcar (lambda (patch) (list :old-start (plist-get patch :old-start) :old-end (plist-get patch :old-end) :new-start (plist-get patch :new-start) :new-end (plist-get patch :new-end))) patches)) (batch (tp-commit-batch-create :base-revision 0 :target-revision 1 :base-extent (length base) :target-extent target-extent :patches patches :coordinate-patches coordinates)) (source-function (tp-surface-test--source-function 'tp--shadow-apply-commit-batch-to-string)) (real-substring (symbol-function 'substring)) (real-concat (symbol-function 'concat)) (substring-count 0) (substring-output 0) (concat-count 0) (concat-input 0) recording) (cl-letf (((symbol-function 'substring) (lambda (string start &optional end) (let ((result (funcall real-substring string start end))) (when recording (cl-incf substring-count) (cl-incf substring-output (length result))) result))) ((symbol-function 'concat) (lambda (&rest sequences) (when recording (cl-incf concat-count) (cl-incf concat-input (apply #'+ (mapcar #'length sequences)))) (apply real-concat sequences)))) (let ((result (progn (setq recording t) (prog1 (funcall source-function base batch) (setq recording nil))))) (should (= (length result) target-extent)))) (should (= substring-count (1+ patch-count))) (should (<= substring-output (length base))) (should (= concat-count 1)) (should (= concat-input target-extent))))) (ert-deftest tp-surface-test-producer-can-return-equal-coordinate-batch () "A retained producer can publish a strict batch through normal TP phases." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount (current-buffer) (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) root) (maphash (lambda (_path object) (unless (tp--surface-object-parent object) (setq root object))) (tp--surface-objects surface)) (let ((old-mounts (copy-sequence (tp--surface-mounts surface))) (old-index (tp--surface-mount-index surface)) (batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 3 :target-extent 4 :patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 3 :replacement (propertize "XY" 'face 'bold))) :coordinate-patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 3)) :client-state '(:batch committed)))) (tp-surface-update surface (lambda (context) (tp-object-reuse-subtree context root) (tp-commit-batch-result-create context batch))) (should (equal "aXYc" (buffer-string))) (should (eq 'bold (get-text-property 2 'face))) (should (= 2 (tp-surface-revision surface))) (should (equal '(:batch committed) (tp-surface-client-state surface))) (should (eq old-index (tp--surface-mount-index surface))) (should (cl-every #'identity (cl-mapcar #'eq old-mounts (tp--surface-mounts surface)))) (should (cl-every (lambda (mount) (<= (tp--mount-position (tp--surface-mount-end mount)) (point-max))) (tp--surface-mounts surface))) (should (plist-get (tp-surface-report surface) :commit-batch)) (let ((before-mounts (tp-object-mounts root)) (before-live (copy-sequence (tp--surface-mounts surface))) (failing (tp-commit-batch-create :base-revision 2 :target-revision 3 :base-extent 4 :target-extent 5 :patches (list (list :old-start 1 :old-end 3 :new-start 1 :new-end 4 :replacement "XYZ")) :coordinate-patches (list (list :old-start 1 :old-end 3 :new-start 1 :new-end 4))))) (let ((tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'client-state) (error "Injected retained mount rollback"))))) (should-error (tp-surface-update surface (lambda (context) (tp-object-reuse-subtree context root) (tp-commit-batch-result-create context failing))))) (should (equal "aXYc" (buffer-string))) (should (= 2 (tp-surface-revision surface))) (should (equal before-mounts (tp-object-mounts root))) (should (cl-every #'identity (cl-mapcar #'eq before-live (tp--surface-mounts surface))))))))) (ert-deftest tp-surface-test-commit-batch-promotes-exact-target-mounts () "A shrinking batch publishes producer-supplied target mounts atomically." (tp-surface-test--with-buffer (let ((owned-state (list :batch 'exact-mounts)) root logical) (let* ((producer (lambda (context) (setq root (tp-object-ensure context nil 'root 'text) logical (tp-object-ensure context root 'logical 'item)) (tp-object-attach-content-range context logical root 4 6 '(:slot old)) (tp-surface-test--leaf 'root "abcdef"))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t)))) (tp-surface-update surface (lambda (context) (tp-object-reuse-subtree context root) (tp-commit-batch-result-create context (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 6 :target-extent 2 :patches (list (list :old-start 0 :old-end 6 :new-start 0 :new-end 2 :replacement "XY")) :coordinate-patches (list (list :old-start 0 :old-end 6 :new-start 0 :new-end 2))) :mount-specs (list (list :object root :start 0 :end 2 :tags nil) (list :object logical :start 0 :end 2 :tags '(:slot new))) :client-state owned-state))) (should (equal (buffer-string) "XY")) (should (= (tp-surface-revision surface) 2)) (should (eq (tp-surface-client-state surface) owned-state)) (should (plist-get (tp-surface-report surface) :commit-batch)) (should (equal (tp-object-mounts logical) '((:start 1 :end 3 :tags (:slot new))))))))) (ert-deftest tp-surface-test-commit-batch-reuses-proven-mount-projection () "A producer proof can retain mounts despite nonidentity text coordinates." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "abcd") '(:capability content :coordinate-mounts t))) (root (tp-object-resolve surface '(root))) (mounts (tp--surface-mounts surface)) (mount-index (tp--surface-mount-index surface)) (batch (tp-commit-batch-create :base-revision 1 :target-revision 2 :base-extent 4 :target-extent 4 :patches (list (list :old-start 1 :old-end 3 :new-start 1 :new-end 3 :replacement "XY")) :coordinate-patches (list (list :old-start 1 :old-end 2 :new-start 1 :new-end 1) (list :old-start 3 :old-end 3 :new-start 2 :new-end 3))))) (tp-surface-update surface (lambda (context) (tp-object-reuse-subtree context root) (tp-commit-batch-result-create context batch :reuse-mount-projection t))) (should (equal (buffer-string) "aXYd")) (should (eq mounts (tp--surface-mounts surface))) (should (eq mount-index (tp--surface-mount-index surface))) (should (plist-get (tp-surface-report surface) :retained-mount-state))))) (ert-deftest tp-surface-test-plan-validates-and-defensively-copies () "Plans reject duplicate keys and own their caller-provided values." (let* ((callback (byte-compile (lambda (_window _object _position) "help"))) (props (list 'help-echo callback)) (text (copy-sequence "A")) (child (tp-surface-test--leaf 'child text props)) (plan (tp-surface-plan-create :key 'root :kind 'group :children (list child) :capability 'content))) (setcar props 'face) (aset text 0 ?Z) (let ((rendered (tp-surface-materialize-string plan))) (should (equal (substring-no-properties rendered) "A")) (should (eq (get-text-property 0 'help-echo rendered) callback))) (should-error (tp-surface-plan-create :key 'root :kind 'group :children (list (tp-surface-test--leaf 'same "A") (tp-surface-test--leaf 'same "B")) :capability 'content) :type 'tp-duplicate-object-key))) (ert-deftest tp-surface-test-plan-snapshots-nested-values () "Nested plan values remain defensive snapshots across producer boundaries." (let* ((props (list 'face 'bold)) (tags (list :role 'leaf)) (text (copy-sequence "A")) (child (tp-surface-plan-create :key 'child :kind 'text :text text :props props :tags tags :capability 'content)) (children (list child)) (parent (tp-surface-plan-create :key 'root :kind 'group :children children :capability 'content)) (result (tp-surface-result-create parent)) (normalized-from-result (car (tp--producer-result result nil nil nil)))) (tp-surface-test--with-buffer (let* ((surface (tp--create-surface buffer 'content '(:capability content))) (normalized-from-plan (car (tp--producer-result parent surface nil nil)))) (let* ((result-plan (tp-surface-result-plan result)) (result-child (car (tp-surface-plan-children result-plan)))) (setf (tp-surface-plan-children result-plan) nil (tp-surface-plan-text result-child) "result-mutated") (setcar (tp-surface-plan-tags result-child) :result-mutated)) (setcar props 'help-echo) (setcar tags :mutated) (aset text 0 ?Z) (setcar children nil) (setf (tp-surface-plan-text (car (tp-surface-plan-children parent))) "mutated") (should-not (eq (car (tp-surface-plan-children parent)) child)) (should-not (eq (tp-surface-result-plan result) parent)) (should-not (eq normalized-from-result parent)) (should-not (eq normalized-from-plan parent)) (let ((rendered (tp-surface-materialize-string normalized-from-result))) (should (equal (substring-no-properties rendered) "A")) (should (eq (get-text-property 0 'face rendered) 'bold))) (should (equal (tp-surface-plan-tags child) '(:role leaf))))))) (ert-deftest tp-surface-test-retained-content-composes-property-contributions () "A content surface should layer property contributions without text work." (tp-surface-test--with-buffer (let* ((plan (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create :key 'fragments :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'text "abcd")))))) (surface (tp-surface-mount buffer plan '(:capability content))) (producer (lambda (contributions) (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (fragments (tp-object-ensure context root 'fragments 'group)) (_text (tp-object-ensure context fragments 'text 'text)) (owned-plan (tp-surface-plan-create-owned :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create-owned :key 'fragments :kind 'group :capability 'content :children (list (tp-surface-plan-create-owned :key 'text :kind 'text :text "abcd" :capability 'content))))))) (tp-surface-retained-content-result-create context owned-plan "abcd" nil nil t contributions))))) (base (list :start 0 :end 4 :props '(face (:foreground "white" :background "black")))) (specific (list :start 1 :end 3 :props '(face (:foreground "red"))))) (tp-surface-update surface (funcall producer (list base specific))) (should (equal (buffer-string) "abcd")) (let ((outer (get-text-property 1 'face)) (inner (get-text-property 2 'face))) (should (equal (plist-get outer :foreground) "white")) (should (equal (plist-get outer :background) "black")) (should (equal (plist-get inner :foreground) "red")) (should (equal (plist-get inner :background) "black"))) (let* ((next-base (list :start 0 :end 4 :props '(face (:foreground "#EEEEEE" :background "#111111")))) (report (tp-surface-update surface (funcall producer (list next-base specific))))) (should (equal (buffer-string) "abcd")) (should (= 0 (plist-get report :text-operations))) (should (> (plist-get report :property-operations) 0)) (let ((outer (get-text-property 1 'face)) (inner (get-text-property 2 'face))) (should (equal (plist-get outer :foreground) "#EEEEEE")) (should (equal (plist-get outer :background) "#111111")) (should (equal (plist-get inner :foreground) "red")) (should (equal (plist-get inner :background) "#111111"))))))) (ert-deftest tp-surface-test-retained-content-rejects-invalid-contribution () "Invalid contribution ranges must fail before mutating published content." (tp-surface-test--with-buffer (let* ((plan (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create :key 'fragments :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'text "safe")))))) (surface (tp-surface-mount buffer plan '(:capability content)))) (should-error (tp-surface-update surface (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (fragments (tp-object-ensure context root 'fragments 'group)) (_text (tp-object-ensure context fragments 'text 'text)) (owned-plan (tp-surface-plan-create-owned :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create-owned :key 'fragments :kind 'group :capability 'content :children (list (tp-surface-plan-create-owned :key 'text :kind 'text :text "safe" :capability 'content))))))) (tp-surface-retained-content-result-create context owned-plan "safe" nil nil t '((:start 0 :end 9 :props (face bold))))))) :type 'tp-invalid-content-range) (should (equal (buffer-string) "safe"))))) (ert-deftest tp-surface-test-owned-plan-result-transfers-candidate-tree () "Owned plan and result constructors skip a duplicate candidate snapshot." (tp-surface-test--with-buffer (let* ((surface (tp--create-surface buffer 'content '(:capability content))) (context (tp--make-context surface))) (let ((tp--current-prepare-context context)) (let* ((text (copy-sequence "owned")) (child (tp-surface-plan-create-owned :key 'child :kind 'text :text text :capability 'content)) (parent (tp-surface-plan-create-owned :key 'root :kind 'group :children (list child) :capability 'content)) (result (tp-surface-result-create-owned context parent)) (copies 0) (original (symbol-function 'tp--copy-surface-plan))) (cl-letf (((symbol-function 'tp--copy-surface-plan) (lambda (&rest arguments) (cl-incf copies) (apply original arguments)))) (should (eq (car (tp--producer-result result surface nil context)) parent))) (should (= copies 0)) (should (tp-surface-result-plan-owned-p result)) (should (tp-surface-result-consumed-p result)) (should-not (tp-surface-result-plan result)) (should-not (tp-surface-result-client-state result)) (should-not (tp-surface-result-owner-context result)) (should-error (tp--producer-result result surface nil context) :type 'tp-owned-result-error) (should (eq (tp-surface-plan-text child) text))))))) (ert-deftest tp-surface-test-owned-result-rejects-inactive-or-wrong-context () "Rejected owned results remain unconsumed for inactive or wrong contexts." (tp-surface-test--with-buffer (let* ((surface (tp--create-surface buffer 'content '(:capability content))) (context (tp--make-context surface)) (other-context (tp--make-context surface)) (plan (tp-surface-plan-create-owned :key 'root :kind 'text :text "owned" :capability 'content))) (let ((tp--current-prepare-context context)) (let ((result (tp-surface-result-create-owned context plan))) (setf (tp--context-active context) nil) (should-error (tp--producer-result result surface nil context) :type 'tp-owned-result-error) (should-not (tp-surface-result-consumed-p result)) (should (eq (tp-surface-result-plan result) plan)) (setf (tp--context-active context) t) (let ((tp--current-prepare-context other-context)) (should-error (tp--producer-result result surface nil other-context) :type 'tp-owned-result-error)) (should-not (tp-surface-result-consumed-p result))))))) (ert-deftest tp-surface-test-direct-plan-prepare-snapshots-once () "Direct plan prepare does not snapshot its already-copied plan twice." (let ((plan (tp-surface-test--leaf 'root "direct")) (copies 0) (original (symbol-function 'tp--copy-surface-plan))) (cl-letf (((symbol-function 'tp--copy-surface-plan) (lambda (&rest args) (cl-incf copies) (apply original args)))) (should (equal (substring-no-properties (tp-surface-materialize-string plan)) "direct")) (should (= copies 1))))) (ert-deftest tp-surface-test-render-propagation-reuses-prepared-values () "Rendering a prepared plan does not recursively copy inherited properties." (let* ((inherited-value (list :space :width 1)) (parent (list 'display inherited-value)) (child (list 'face 'bold)) (render-copies 0) (copy-function (symbol-function 'tp--copy-property-value))) (cl-letf (((symbol-function 'tp--copy-property-value) (lambda (&rest arguments) (cl-incf render-copies) (apply copy-function arguments)))) (let ((result (tp--plist-overlay parent child t))) (should (equal (plist-get result 'face) 'bold)) (should (eq (plist-get result 'display) inherited-value)) (should-not (eq result parent)))) (should (= render-copies 0)))) (ert-deftest tp-surface-test-owned-render-transfers-property-values () "Owned producer plans transfer candidate property values to rendered text." (let (candidate-value) (let ((rendered (tp-surface-materialize-string (lambda (context) (setq candidate-value (list :space :width 1)) (tp-object-ensure context nil 'root 'text) (tp-surface-result-create-owned context (tp-surface-plan-create-owned :key 'root :kind 'text :text "owned" :props (list 'display candidate-value) :capability 'content)))))) (should (equal (substring-no-properties rendered) "owned")) (should (eq (get-text-property 0 'display rendered) candidate-value))))) (ert-deftest tp-surface-test-plan-copy-obeys-value-identity-rules () "Plan data is copied while opaque records and functions keep identity." (with-temp-buffer (let* ((caller-string (copy-sequence "tag")) (caller-vector (vector (copy-sequence "nested"))) (record (tp--make-native-range (current-buffer) :buffer 1 1)) (calls 0) (callback (lambda () (cl-incf calls))) (table (make-hash-table :test #'equal)) (marker (copy-marker (point-min))) (tags (list caller-string caller-vector record callback table marker (current-buffer))) (plan (tp-surface-plan-create :key 'root :kind 'text :text "x" :tags tags :capability 'content)) (copy (tp-surface-plan-tags plan))) (should-not (eq copy tags)) (should-not (eq (nth 0 copy) caller-string)) (should-not (eq (nth 1 copy) caller-vector)) (should-not (eq (aref (nth 1 copy) 0) (aref caller-vector 0))) (should (eq (nth 2 copy) record)) (should (eq (nth 3 copy) callback)) (should (eq (nth 4 copy) table)) (should (eq (nth 5 copy) marker)) (should (eq (nth 6 copy) (current-buffer))) (should (= calls 0))))) (ert-deftest tp-surface-test-plan-copy-shares-source-identity-across-children () "One plan snapshot reuses one copy for repeated source identities globally." (let* ((shared (list :shared t)) (equal-but-distinct (list :shared t)) (child-a (tp--make-surface-plan :key 'a :kind 'text :tags (list shared))) (child-b (tp--make-surface-plan :key 'b :kind 'text :tags (list shared equal-but-distinct))) (plan (tp--make-surface-plan :key 'root :kind 'group :children (list child-a child-b))) (copy (tp--copy-surface-plan plan)) (copy-a (tp-surface-plan-tags (car (tp-surface-plan-children copy)))) (copy-b (tp-surface-plan-tags (cadr (tp-surface-plan-children copy))))) (should (eq (car copy-a) (car copy-b))) (should-not (eq (car copy-b) (cadr copy-b))) (should-not (eq (car copy-b) equal-but-distinct)) (should-not (eq (cadr copy-b) equal-but-distinct)) (should-not (eq (car copy-a) shared)))) (ert-deftest tp-surface-test-retained-options-own-mutable-containers () "Surface options copy data containers without cloning opaque identities." (tp-surface-test--with-buffer (let* ((caller-string (copy-sequence "state")) (caller-vector (vector (copy-sequence "nested"))) (record (tp--make-native-range buffer :buffer 1 1)) (calls 0) (callback (lambda () (cl-incf calls))) (table (make-hash-table :test #'equal)) (start (copy-marker (point-min))) (end (copy-marker (point-max) t)) (client-state (list caller-string caller-vector record callback table buffer)) (options (list :capability 'content :start start :end end :client-state client-state)) (surface (tp--create-surface buffer 'content options)) (stored-options (tp--surface-options surface)) (stored-state (plist-get stored-options :client-state))) (should-not (eq stored-state client-state)) (should-not (eq (nth 0 stored-state) caller-string)) (should-not (eq (nth 1 stored-state) caller-vector)) (should-not (eq (aref (nth 1 stored-state) 0) (aref caller-vector 0))) (should (eq (nth 2 stored-state) record)) (should (eq (nth 3 stored-state) callback)) (should (eq (nth 4 stored-state) table)) (should (eq (nth 5 stored-state) buffer)) (should (eq (plist-get stored-options :start) start)) (should (eq (plist-get stored-options :end) end)) (should (= calls 0)) (aset caller-string 0 ?S) (aset (aref caller-vector 0) 0 ?N) (should (equal (nth 0 stored-state) "state")) (should (equal (nth 1 stored-state) ["nested"]))))) (ert-deftest tp-surface-test-report-copy-is-deep-for-data-values () "Public reports cannot mutate retained data and preserve opaque identities." (with-temp-buffer (let* ((report-string (copy-sequence "report")) (report-vector (vector (copy-sequence "nested"))) (record (tp--make-native-range (current-buffer) :buffer 1 1)) (calls 0) (callback (lambda () (cl-incf calls))) (table (make-hash-table :test #'equal)) (marker (copy-marker (point-min))) (payload (list report-string report-vector record callback table marker (current-buffer))) (surface (tp--make-surface :report (list :payload payload))) (first-payload (plist-get (tp-surface-report surface) :payload))) (should-not (eq (nth 0 first-payload) report-string)) (should-not (eq (nth 1 first-payload) report-vector)) (should-not (eq (aref (nth 1 first-payload) 0) (aref report-vector 0))) (should (eq (nth 2 first-payload) record)) (should (eq (nth 3 first-payload) callback)) (should (eq (nth 4 first-payload) table)) (should (eq (nth 5 first-payload) marker)) (should (eq (nth 6 first-payload) (current-buffer))) (should (= calls 0)) (aset (nth 0 first-payload) 0 ?R) (aset (aref (nth 1 first-payload) 0) 0 ?N) (let ((second-payload (plist-get (tp-surface-report surface) :payload))) (should (equal (nth 0 second-payload) "report")) (should (equal (nth 1 second-payload) ["nested"])))))) (ert-deftest tp-surface-test-report-summary-contains-only-commit-scalars () "Report summary exposes Ebox metrics without exposing nested report data." (let ((surface (tp--make-surface :report '(:transaction-id 7 :text-operations 2 :property-operations 3 :full-root nil :scope-count 4 :scope-range-count 5 :scope-fallback t :reconciled-objects 6 :created-objects 7 :removed-objects 8 :moved-objects 9 :payload (:mutable value))))) (should (equal (tp-surface-report-summary surface) '(:transaction-id 7 :text-operations 2 :property-operations 3 :full-root nil :scope-count 4 :scope-range-count 5 :scope-fallback t :reconciled-objects 6 :created-objects 7 :removed-objects 8 :moved-objects 9))))) (ert-deftest tp-surface-test-materialize-producer-is-ephemeral () "Pure materialization leaves no live object, binding, or subscription." (let ((signal (tp-signal-create 7)) object binding) (let ((rendered (tp-surface-materialize-string (lambda (context) (setq object (tp-object-ensure context nil 'root 'text) binding (tp-bind object '(test . value) (lambda () (tp-signal-read signal)))) (tp-surface-test--leaf 'root (number-to-string (tp-binding-read binding))))))) (should (equal rendered "7"))) (should-not (tp-object-live-p object)) (should-not (tp-binding-live-p binding)) (should (= (tp-signal-subscriber-count signal) 0)))) (ert-deftest tp-surface-test-content-mount-retains-keyed-identity () "A content update reuses keyed objects and publishes a minimal result." (tp-surface-test--with-buffer (let* ((first (tp-surface-test--leaf 'root "old" '(face bold))) (surface (tp-surface-mount buffer first '(:capability content))) (object (tp-object-resolve surface '(root)))) (should (equal (buffer-string) "old")) (cl-letf (((symbol-function 'buffer-list) (lambda (&rest _) (error "Unexpected buffer scan"))) ((symbol-function 'text-property-search-forward) (lambda (&rest _) (error "Unexpected property scan")))) (tp-surface-update surface (tp-surface-test--leaf 'root "new" '(face italic)))) (should (equal (buffer-string) "new")) (should (eq object (tp-object-resolve surface '(root)))) (should (eq (get-text-property 1 'face buffer) 'italic)) (should (= (plist-get (tp-surface-report surface) :text-operations) 1))))) (ert-deftest tp-surface-test-coordinate-content-mounts-keep-public-ranges () "Coordinate content mounts avoid marker churn without changing the API." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "abc") '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root))) (mount (car (gethash object (tp--surface-mount-index surface))))) (should (integerp (tp--surface-mount-start mount))) (should (integerp (tp--surface-mount-end mount))) (should (tp-object-mounted-p object)) (should (equal (car (tp-object-mounts object)) '(:start 1 :end 4 :tags nil))) (tp-surface-update surface (tp-surface-test--leaf 'root "abcd")) (should (equal (car (tp-object-mounts object)) '(:start 1 :end 5 :tags nil)))))) (ert-deftest tp-surface-test-batched-content-ranges-preserve-order () "Batched content ranges retain attachment order and caller isolation." (tp-surface-test--with-buffer (let ((tags (list :slot 'first))) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-ranges context object (list (list :object object :start 0 :end 1 :tags tags) (list :object object :start 1 :end 3 :tags '(:slot second)))) (tp-surface-plan-create :key 'root :kind 'text :text "abc" :capability 'content)))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root)))) (setcar tags :mutated) (should (equal (tp-object-mounts object) '((:start 1 :end 4 :tags nil) (:start 1 :end 2 :tags (:slot first)) (:start 2 :end 4 :tags (:slot second))))))))) (ert-deftest tp-surface-test-batched-content-ranges-reuse-identical-tags () "A bulk attach shares one snapshot when the same TAGS object is reused." (tp-surface-test--with-buffer (let ((tags (list :slot 'same))) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-ranges context object (list (list :object object :start 0 :end 1 :tags tags) (list :object object :start 1 :end 2 :tags tags))) (tp-surface-plan-create :key 'root :kind 'text :text "ab" :capability 'content)))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root))) (tagged (cl-remove-if-not (lambda (mount) (equal (tp--surface-mount-tags mount) '(:slot same))) (gethash object (tp--surface-mount-index surface))))) (should (= (length tagged) 2)) (should (eq (tp--surface-mount-tags (car tagged)) (tp--surface-mount-tags (cadr tagged)))) (let ((public-tags (plist-get (cadr (tp-object-mounts object)) :tags))) (setcar public-tags :public-mutated) (should (equal (plist-get (cadr (tp-object-mounts object)) :tags) '(:slot same)))) (setcar tags :mutated) (should (equal (tp-object-mounts object) '((:start 1 :end 3 :tags nil) (:start 1 :end 2 :tags (:slot same)) (:start 2 :end 3 :tags (:slot same))))))))) (ert-deftest tp-surface-test-batched-content-ranges-keep-opaque-tags-separate () "Equal opaque tag values keep independent bulk snapshots." (tp-surface-test--with-buffer (let ((left-tags (copy-sequence "same")) (right-tags (copy-sequence "same"))) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-ranges context object (list (list :object object :start 0 :end 1 :tags left-tags) (list :object object :start 1 :end 2 :tags right-tags))) (tp-surface-plan-create :key 'root :kind 'text :text "ab" :capability 'content)))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root))) (tagged (cl-remove-if-not (lambda (mount) (stringp (tp--surface-mount-tags mount))) (gethash object (tp--surface-mount-index surface))))) (should (= (length tagged) 2)) (should-not (eq (tp--surface-mount-tags (car tagged)) (tp--surface-mount-tags (cadr tagged)))) (aset left-tags 0 ?X) (should (equal (mapcar (lambda (mount) (tp--surface-mount-tags mount)) tagged) '("same" "same"))))))) (ert-deftest tp-surface-test-singular-content-range-snapshots-each-call () "Separate singular attachments snapshot mutable tags independently." (tp-surface-test--with-buffer (let ((tags (list :slot 'before))) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-range context object object 0 1 tags) (setcar (cdr tags) 'after) (tp-object-attach-content-range context object object 1 2 tags) (tp-surface-plan-create :key 'root :kind 'text :text "abc" :capability 'content)))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root)))) (should (equal (tp-object-mounts object) '((:start 1 :end 4 :tags nil) (:start 1 :end 2 :tags (:slot before)) (:start 2 :end 3 :tags (:slot after))))))))) (ert-deftest tp-surface-test-owned-batched-content-ranges-transfer-tags () "Owned bulk ranges retain freshly allocated TAGS without another copy." (tp-surface-test--with-buffer (let ((tags (list :slot 'owned))) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-ranges-owned context object (list (list :object object :start 0 :end 1 :tags tags))) (tp-surface-plan-create :key 'root :kind 'text :text "a" :capability 'content)))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root))) (mount (cadr (tp-object-mounts object)))) (should (eq tags (tp--surface-mount-tags (cadr (gethash object (tp--surface-mount-index surface)))))) (should (equal (plist-get mount :tags) '(:slot owned))))))) (ert-deftest tp-surface-test-owned-batched-content-ranges-reuse-tags-object () "Owned bulk ranges reuse the same transferred TAGS object." (tp-surface-test--with-buffer (let ((tags (list :slot 'owned-same))) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-ranges-owned context object (list (list :object object :start 0 :end 1 :tags tags) (list :object object :start 1 :end 2 :tags tags))) (tp-surface-plan-create :key 'root :kind 'text :text "ab" :capability 'content)))) (surface (tp-surface-mount buffer producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root))) (tagged (cl-remove-if-not (lambda (mount) (equal (tp--surface-mount-tags mount) '(:slot owned-same))) (gethash object (tp--surface-mount-index surface))))) (should (= (length tagged) 2)) (should (eq (tp--surface-mount-tags (car tagged)) tags)) (should (eq (tp--surface-mount-tags (car tagged)) (tp--surface-mount-tags (cadr tagged)))))))) (ert-deftest tp-surface-test-owned-batched-content-ranges-roll-back-tags () "A failed owned range publication restores the previous live TAGS." (tp-surface-test--with-buffer (let ((text "a") (tags (list :slot 'live))) (cl-labels ((producer (context) (let ((object (tp-object-ensure context nil 'root 'text))) (tp-object-attach-content-ranges-owned context object (list (list :object object :start 0 :end 1 :tags tags))) (tp-surface-plan-create :key 'root :kind 'text :text text :capability 'content)))) (let* ((surface (tp-surface-mount buffer #'producer '(:capability content :coordinate-mounts t))) (object (tp-object-resolve surface '(root))) (revision (tp-surface-revision surface)) (live-tags (tp--surface-mount-tags (cadr (gethash object (tp--surface-mount-index surface))))) (candidate-tags (list :slot 'candidate))) (setq text "b" tags candidate-tags) (let ((tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'client-state) (error "Injected owned range rollback"))))) (should-error (tp-surface-update surface #'producer))) (should (= (tp-surface-revision surface) revision)) (should (equal (buffer-string) "a")) (should (eq object (tp-object-resolve surface '(root)))) (should (eq live-tags (tp--surface-mount-tags (cadr (gethash object (tp--surface-mount-index surface))))))))))) (ert-deftest tp-surface-test-content-publication-preserves-point () "Content publication writes properties without losing the point." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old value" '(face bold)) '(:capability content))) (report (progn (goto-char 6) (tp-surface-update surface (tp-surface-test--leaf 'root "new value" '(face italic)))))) (should (equal (buffer-string) "new value")) (should (eq (get-text-property 1 'face buffer) 'italic)) (should (= (point) 6)) (should (= (plist-get report :text-operations) 1)) (should (= (plist-get report :property-operations) 1))))) (ert-deftest tp-surface-test-incremental-content-update-never-leaves-point-at-patch () "Incremental text publication restores point after a length-changing patch." (tp-surface-test--with-buffer (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "prefix old suffix" '(face bold)) '(:capability content)))) (goto-char 8) (let ((point-before (point))) (tp-surface-update surface (tp-surface-test--leaf 'root "prefix much-longer suffix" '(face italic))) (should (= (point) point-before)))))) (ert-deftest tp-surface-test-scoped-content-update-preserves-point-after-patch () "A scoped update after point does not move the current cursor." (tp-surface-test--with-buffer (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "before target after" '(face bold)) '(:capability content)))) (goto-char 4) (let ((point-before (point))) (tp-surface-update surface (tp-surface-test--leaf 'root "before changed-target after" '(face italic))) (should (= (point) point-before)))))) (ert-deftest tp-surface-test-content-publication-reapplies-hook-corruption () "A hook that changes inserted properties must be corrected before commit." (tp-surface-test--with-buffer (let (surface before revision) (add-hook 'after-change-functions #'tp-surface-test--corrupt-after-change nil t) (unwind-protect (progn (setq surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old" '(face bold)) '(:capability content))) (setq before (buffer-substring 1 4) revision (tp-surface-revision surface) tp-surface-test--corrupt-next t) (tp-surface-update surface (tp-surface-test--leaf 'root "new" '(face italic))) (should (equal (buffer-string) "new")) (should (= (tp-surface-revision surface) (1+ revision))) (should (eq (get-text-property 1 'face buffer) 'italic)) (should-not (equal-including-properties (buffer-substring 1 4) before))) (when (and surface (tp-surface-live-p surface)) (tp-surface-unmount surface)) (remove-hook 'after-change-functions #'tp-surface-test--corrupt-after-change t))))) (ert-deftest tp-surface-test-scoped-content-update-publishes-one-object () "A scoped update should replace only its retained object's mounted text." (tp-surface-test--with-buffer (let ((middle "B") middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (tp-object-ensure context root 'left 'text) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "A") (tp-surface-test--leaf 'middle middle) (tp-surface-test--leaf 'right "C"))))) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface))) (setq middle "LONG") (let ((report (tp-surface-update-scoped surface (list middle-object) producer))) (should (equal (buffer-string) "ALONGC")) (should (= (tp-surface-revision surface) (1+ revision))) (should-not (plist-get report :full-root)) (should (= (plist-get report :scope-count) 1)) (should (= (plist-get report :scope-range-count) 1)) (should (= (plist-get report :touched-characters) 4))))))) (ert-deftest tp-surface-test-scoped-update-can-skip-report-snapshot () "A scoped caller may skip an unused defensive report snapshot." (tp-surface-test--with-buffer (let ((middle "B") middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (setq middle-object (tp-object-ensure context root 'middle 'text))) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'middle middle))))) (surface (tp-surface-mount buffer producer '(:capability content))) (copies 0) (original (symbol-function 'tp-surface-report))) (setq middle "LONG") (cl-letf (((symbol-function 'tp-surface-report) (lambda (&rest arguments) (cl-incf copies) (apply original arguments)))) (should-not (tp-surface-update-scoped surface (list middle-object) producer '(:return-report nil)))) (should (= copies 0)) (should (= (tp-surface-revision surface) 2)))))) (ert-deftest tp-surface-test-scoped-content-property-only-keeps-text-stable () "A scoped property-only change must not replace its character range." (tp-surface-test--with-buffer (let ((middle-face 'bold) middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (tp-object-ensure context root 'left 'text) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "A") (tp-surface-test--leaf 'middle "B" (list 'face middle-face)) (tp-surface-test--leaf 'right "C"))))) (surface (tp-surface-mount buffer producer '(:capability content))) (marker (copy-marker 3 t))) (setq middle-face 'italic) (cl-letf (((symbol-function 'delete-region) (lambda (&rest _) (error "Scoped property update replaced text")))) (let ((report (tp-surface-update-scoped surface (list middle-object) producer))) (should (= (plist-get report :text-operations) 0)) (should (> (plist-get report :property-operations) 0)))) (should (equal (buffer-string) "ABC")) (should (eq (get-text-property 2 'face) 'italic)) (should (= (marker-position marker) 3)) (set-marker marker nil))))) (ert-deftest tp-surface-test-scoped-update-supports-disjoint-object-mounts () "One logical scope should update all of its disjoint mounts atomically." (tp-surface-test--with-buffer (let ((left "A") (right "C") logical) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (left-object (tp-object-ensure context root 'left 'text)) (gap-object (tp-object-ensure context root 'gap 'text)) (right-object (tp-object-ensure context root 'right 'text))) (setq logical (tp-object-ensure context root 'logical 'item)) (tp-object-retain context logical) (tp-object-attach-fragment context logical left-object 'left) (tp-object-attach-fragment context logical right-object 'right) (ignore gap-object)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left left) (tp-surface-test--leaf 'gap "|") (tp-surface-test--leaf 'right right))))) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface))) (setq left "LEFT" right "RIGHT") (let ((report (tp-surface-update-scoped surface (list logical) producer))) (should (equal (buffer-string) "LEFT|RIGHT")) (should (= (tp-surface-revision surface) (1+ revision))) (should (= (plist-get report :scope-count) 1)) (should (= (plist-get report :scope-range-count) 2)) (should (= (plist-get report :text-operations) 2))))))) (ert-deftest tp-surface-test-scoped-update-adds-and-removes-owned-output () "A retained scope should add or remove its owned output between stable text." (tp-surface-test--with-buffer (let ((visible nil) logical) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (left (tp-object-ensure context root 'left 'text)) (middle (and visible (tp-object-ensure context root 'middle 'text)))) (tp-object-ensure context root 'gap 'text) (tp-object-ensure context root 'right 'text) (setq logical (tp-object-ensure context root 'logical 'item)) (tp-object-retain context logical) (when middle (tp-object-attach-fragment context logical middle 'owned)) (ignore left)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (delq nil (list (tp-surface-test--leaf 'left "A") (and visible (tp-surface-test--leaf 'middle "B")) (tp-surface-test--leaf 'gap "|") (tp-surface-test--leaf 'right "C")))))) (surface (tp-surface-mount buffer producer '(:capability content)))) (should (equal (buffer-string) "A|C")) (setq visible t) (let ((report (tp-surface-update-scoped surface (list logical) producer))) (should (equal (buffer-string) "AB|C")) (should (= (plist-get report :text-operations) 1))) (setq visible nil) (let ((report (tp-surface-update-scoped surface (list logical) producer))) (should (equal (buffer-string) "A|C")) (should (= (plist-get report :text-operations) 1))))))) (ert-deftest tp-surface-test-scoped-update-survives-outer-transaction () "A scoped request should remain attached until its outer transaction flushes." (tp-surface-test--with-buffer (let ((middle "B") middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (tp-object-ensure context root 'left 'text) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "A") (tp-surface-test--leaf 'middle middle) (tp-surface-test--leaf 'right "C"))))) (surface (tp-surface-mount buffer producer '(:capability content)))) (setq middle "LONG") (tp-with-transaction (tp-surface-update-scoped surface (list middle-object) producer)) (should (equal (buffer-string) "ALONGC")) (should-not (plist-get (tp-surface-report surface) :full-root)) (setq middle "NEXT") (tp-surface-update surface producer) (should (equal (buffer-string) "ANEXTC")) (should (plist-get (tp-surface-report surface) :full-root)))))) (ert-deftest tp-surface-test-scoped-properties-update-publishes-one-object () "A properties scope should leave another retained range untouched." (tp-surface-test--with-buffer (insert "left|right") (let* ((left-anchor (tp-range-anchor-create buffer 1 5)) (right-anchor (tp-range-anchor-create buffer 6 11)) (left-face 'bold) left-object (producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (setq left-object (tp-object-ensure context root 'left 'range)) (tp-object-attach-range context left-object left-anchor) (let ((right-object (tp-object-ensure context root 'right 'range))) (tp-object-attach-range context right-object right-anchor))) (tp-surface-plan-create :key 'root :kind 'group :capability 'properties :children (list (tp-surface-plan-create :key 'left :kind 'range :props (list 'face left-face) :capability 'properties) (tp-surface-plan-create :key 'right :kind 'range :props '(face italic) :capability 'properties))))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (setq left-face 'underline) (let ((report (tp-surface-update-scoped surface (list left-object) producer))) (should (eq (get-text-property 2 'face) 'underline)) (should (eq (get-text-property 7 'face) 'italic)) (should-not (plist-get report :full-root)) (should (= (plist-get report :scope-range-count) 1)) (should (= (plist-get report :property-operations) 1)))))) (ert-deftest tp-surface-test-sparse-property-update-does-not-scan-between-anchors () "A sparse properties update inspects only owned anchor intervals." (tp-surface-test--with-buffer (insert (make-string 120 ?x)) (let* ((left-anchor (tp-range-anchor-create buffer 2 3)) (right-anchor (tp-range-anchor-create buffer 100 101)) (left-face 'bold) (producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (let ((left (tp-object-ensure context root 'left 'range)) (right (tp-object-ensure context root 'right 'range))) (tp-object-attach-range context left left-anchor) (tp-object-attach-range context right right-anchor))) (tp-surface-plan-create :key 'root :kind 'group :capability 'properties :children (list (tp-surface-plan-create :key 'left :kind 'range :props (list 'face left-face) :capability 'properties) (tp-surface-plan-create :key 'right :kind 'range :props '(face italic) :capability 'properties))))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (setq left-face 'underline) (let ((original (symbol-function 'next-single-property-change))) (cl-letf (((symbol-function 'next-single-property-change) (lambda (position property &optional object limit) (unless (or (and (>= position 2) (< position 3)) (and (>= position 100) (< position 101))) (error "Unexpected sparse scan at %s for %s" position property)) (funcall original position property object limit)))) (tp-surface-update surface producer))) (should (eq (get-text-property 2 'face) 'underline)) (should (eq (get-text-property 100 'face) 'italic))))) (ert-deftest tp-surface-test-scoped-update-can-explicitly-fall-back-to-root () "A scoped mismatch should publish the root only when explicitly requested." (tp-surface-test--with-buffer (let ((middle "B") (right "C") middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (tp-object-ensure context root 'left 'text) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "A") (tp-surface-test--leaf 'middle middle) (tp-surface-test--leaf 'right right))))) (surface (tp-surface-mount buffer producer '(:capability content)))) (setq middle "M" right "OUTSIDE") (let ((report (tp-surface-update-scoped surface (list middle-object) producer '(:on-mismatch root)))) (should (equal (buffer-string) "AMOUTSIDE")) (should (plist-get report :full-root)) (should (plist-get report :scope-fallback))))))) (ert-deftest tp-surface-test-scope-ignores-property-plist-order-outside-owner () "Keep a scoped update when unchanged outside text reorders properties." (let ((old (copy-sequence "ABC")) (new (copy-sequence "AMC"))) (add-text-properties 0 1 '(face bold help-echo "left") old) (add-text-properties 2 3 '(face italic help-echo "right") old) ;; Same semantic properties, deliberately inserted in reverse order. (add-text-properties 0 1 '(help-echo "left" face bold) new) (add-text-properties 2 3 '(help-echo "right" face italic) new) (let ((analysis (tp--scope-replacement-analysis old new '((1 . 2)) '((1 . 2))))) (should analysis) (should (= 1 (length (plist-get analysis :patches)))) (should (= 1 (plist-get (car (plist-get analysis :patches)) :old-start))) (should (tp--text-property-semantic-equal-p (substring old 0 1) (substring new 0 1))) (should (tp--text-property-semantic-equal-p (substring old 2 3) (substring new 2 3)))))) (ert-deftest tp-surface-test-scoped-update-rejects-outside-change () "A scoped update should fail before publication when another range changes." (tp-surface-test--with-buffer (let ((middle "B") (right "C") middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (tp-object-ensure context root 'left 'text) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "A") (tp-surface-test--leaf 'middle middle) (tp-surface-test--leaf 'right right))))) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface))) (setq middle "M" right "OUTSIDE") (should-error (tp-surface-update-scoped surface (list middle-object) producer) :type 'tp-scope-mismatch) (should (= (tp-surface-revision surface) revision)) (should (equal (buffer-string) "ABC")))))) (ert-deftest tp-surface-test-scoped-update-rolls-back-buffer-and-mounts () "A failed scoped publication should restore text, revision, and mounts." (tp-surface-test--with-buffer (let ((middle "B") middle-object) (let* ((producer (lambda (context) (let ((root (tp-object-ensure context nil 'root 'group))) (tp-object-ensure context root 'left 'text) (setq middle-object (tp-object-ensure context root 'middle 'text)) (tp-object-ensure context root 'right 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left "A") (tp-surface-test--leaf 'middle middle) (tp-surface-test--leaf 'right "C"))))) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface)) (mounts (tp-object-mounts middle-object))) (setq middle "LONG") (let ((tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'client-state) (error "Injected scoped failure"))))) (should-error (tp-surface-update-scoped surface (list middle-object) producer))) (should (= (tp-surface-revision surface) revision)) (should (equal (buffer-string) "ABC")) (should (eq middle-object (tp-object-resolve surface '(root middle)))) (should (equal (tp-object-mounts middle-object) mounts)))))) (ert-deftest tp-surface-test-failed-candidate-does-not-leak-object () "A failed update preserves the live tree and invalidates new handles." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (root (tp-object-resolve surface '(root))) candidate) (should-error (tp-surface-update surface (lambda (context) (tp-object-ensure context nil 'root 'text) (setq candidate (tp-object-ensure context root 'orphan 'text)) (tp-surface-test--leaf 'root "new"))) :type 'tp-orphan-object) (should (equal (buffer-string) "old")) (should (eq root (tp-object-resolve surface '(root)))) (should-not (tp-object-live-p candidate))))) (ert-deftest tp-surface-test-logical-object-can-own-disjoint-fragments () "One retained object can resolve several output fragments without scans." (tp-surface-test--with-buffer (let ((first "A") (second "BC")) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (logical (tp-object-ensure context root 'logical 'item)) (left (tp-object-ensure context root 'left 'fragment)) (right (tp-object-ensure context root 'right 'fragment))) (tp-object-retain context logical) (tp-object-attach-fragment context logical left '(:slot left)) (tp-object-attach-fragment context logical right '(:slot right))) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left first) (tp-surface-test--leaf 'right second))))) (surface (tp-surface-mount buffer producer '(:capability content))) (logical (tp-object-resolve surface '(root logical)))) (should (tp-object-live-p logical)) (should (equal (tp-object-mounts logical) '((:start 1 :end 2 :tags (:slot left)) (:start 2 :end 4 :tags (:slot right))))) (should (memq logical (tp-surface-at-point 1 buffer))) (should (memq logical (tp-surface-at-point 3 buffer))) (setq first "AA" second "BBB") (cl-letf (((symbol-function 'buffer-list) (lambda (&rest _) (error "Unexpected buffer scan"))) ((symbol-function 'text-property-search-forward) (lambda (&rest _) (error "Unexpected property scan")))) (tp-surface-update surface producer)) (should (eq logical (tp-object-resolve surface '(root logical)))) (should (equal (tp-object-mounts logical) '((:start 1 :end 3 :tags (:slot left)) (:start 3 :end 6 :tags (:slot right))))))))) (ert-deftest tp-surface-test-retain-subtree-preserves-omitted-objects-and-bindings () "Retaining a live subtree preserves its objects and default bindings." (tp-surface-test--with-buffer (let ((value "before") logical child binding) (cl-labels ((producer (context) (let ((root (tp-object-ensure context nil 'root 'group))) (if logical (tp-object-retain-subtree context logical) (setq logical (tp-object-ensure context root 'logical 'group) child (tp-object-ensure context logical 'child 'item) binding (tp-bind logical '(test . value) (lambda () value))) (tp-object-retain context logical) (tp-object-retain context child)) (tp-object-ensure context root 'text 'text) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'text value)))))) (let* ((surface (tp-surface-mount buffer #'producer '(:capability content))) (revision (tp-surface-revision surface))) (setq value "after") (tp-surface-update surface #'producer) (should (= (tp-surface-revision surface) (1+ revision))) (should (eq logical (tp-object-resolve surface '(root logical)))) (should (eq child (tp-object-resolve surface '(root logical child)))) (should (tp-binding-live-p binding)) (should (equal (buffer-string) "after"))))))) (ert-deftest tp-surface-test-reuse-subtree-skips-unchanged-reconcile () "Reusing a proven subtree keeps its objects without reconciling them." (tp-surface-test--with-buffer (let ((value "before") logical child binding) (cl-labels ((producer (context) (let ((root (tp-object-ensure context nil 'root 'group))) (if logical (tp-object-reuse-subtree context logical) (setq logical (tp-object-ensure context root 'logical 'group) child (tp-object-ensure context logical 'child 'item) binding (tp-bind logical '(test . value) (lambda () value))) (tp-object-retain context logical) (tp-object-retain context child)) (tp-object-ensure context root 'text 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'text value))))) (let* ((surface (tp-surface-mount buffer #'producer '(:capability content))) (revision (tp-surface-revision surface))) (setq value "after") (tp-surface-update surface #'producer) (should (= (tp-surface-revision surface) (1+ revision))) (should (= (plist-get (tp-surface-report surface) :reconciled-objects) 2)) (should (eq logical (tp-object-resolve surface '(root logical)))) (should (eq child (tp-object-resolve surface '(root logical child)))) (should (tp-binding-live-p binding)) (should (equal (buffer-string) "after"))))))) (ert-deftest tp-surface-test-reuse-one-preserves-only-proven-object () "Single-object reuse retains that identity without retaining a removed peer." (tp-surface-test--with-buffer (let ((value "before") kept removed) (cl-labels ((producer (context) (let ((root (tp-object-ensure context nil 'root 'group))) (if kept (tp-object-reuse context kept) (setq kept (tp-object-ensure context root 'kept 'item) removed (tp-object-ensure context root 'removed 'item)) (tp-object-retain context kept) (tp-object-retain context removed)) (tp-object-ensure context root 'text 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'text value))))) (let ((surface (tp-surface-mount buffer #'producer '(:capability content)))) (setq value "after") (tp-surface-update surface #'producer) (should (eq kept (tp-object-resolve surface '(root kept)))) (should-not (tp-object-live-p removed)) (should (equal (buffer-string) "after"))))))) (ert-deftest tp-surface-test-ensure-at-reuses-anonymous-slot-without-prefix-replay () "Compiled topology can address one anonymous slot directly." (tp-surface-test--with-buffer (let (second) (cl-labels ((producer (context) (let ((root (tp-object-ensure context nil 'root 'group))) (if second (progn (tp-object-reuse context (tp-object-resolve (tp--context-surface context) '(root (:position 0 :kind item)))) (setq second (tp-object-ensure-at context root nil 'item 1))) (tp-object-retain context (tp-object-ensure context root nil 'item)) (setq second (tp-object-ensure context root nil 'item)) (tp-object-retain context second)) (tp-object-retain context second)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content))) (let ((surface (tp-surface-mount buffer #'producer '(:capability content))) (identity second)) (tp-surface-update surface #'producer) (should (eq identity second))))))) (ert-deftest tp-surface-test-logical-object-owns-leaf-local-ranges () "One logical object can own multiple ranges in one content leaf." (tp-surface-test--with-buffer (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (left (tp-object-ensure context root 'left 'item)) (right (tp-object-ensure context root 'right 'item)) (leaf (tp-object-ensure context root 'leaf 'text))) (tp-object-attach-content-range context left leaf 0 1 '(:slot left)) (tp-object-attach-content-range context left leaf 3 4 '(:slot right)) (tp-object-attach-content-range context right leaf 1 3 '(:slot middle))) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create :key 'leaf :kind 'text :text "ABCD" :capability 'content))))) (surface (tp-surface-mount buffer producer '(:capability content))) (left (tp-object-resolve surface '(root left))) (right (tp-object-resolve surface '(root right)))) (should (equal (buffer-string) "ABCD")) (should (equal (tp-object-mounts left) '((:start 1 :end 2 :tags (:slot left)) (:start 4 :end 5 :tags (:slot right))))) (should (equal (tp-object-mounts right) '((:start 2 :end 4 :tags (:slot middle)))))))) (ert-deftest tp-surface-test-leaf-local-ranges-validate-before-publication () "Leaf-local ranges reject malformed and out-of-bounds attachments." (tp-surface-test--with-buffer (let ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (owner (tp-object-ensure context root 'owner 'item)) (leaf (tp-object-ensure context root 'leaf 'text))) (tp-object-attach-content-range context owner leaf 0 0 '(:slot empty))) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create :key 'leaf :kind 'text :text "AB" :capability 'content)))))) (should-error (tp-surface-mount buffer producer '(:capability content)) :type 'tp-invalid-content-range) (let ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (owner (tp-object-ensure context root 'owner 'item)) (leaf (tp-object-ensure context root 'leaf 'text))) (tp-object-attach-content-range context owner leaf 0 3 '(:slot too-long))) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-plan-create :key 'leaf :kind 'text :text "AB" :capability 'content)))))) (should-error (tp-surface-mount buffer producer '(:capability content)) :type 'tp-invalid-content-range))))) (ert-deftest tp-surface-test-explicitly-retained-object-may-be-unmounted () "An explicitly retained logical object may have no rendered characters." (tp-surface-test--with-buffer (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (hidden (tp-object-ensure context root 'hidden 'item))) (tp-object-retain context hidden) (tp-object-ensure context root 'visible 'text)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'visible "x"))))) (surface (tp-surface-mount buffer producer '(:capability content))) (hidden (tp-object-resolve surface '(root hidden)))) (should (tp-object-live-p hidden)) (should-not (tp-object-mounts hidden))))) (ert-deftest tp-surface-test-prepared-mount-ids-preserve-signature-order () "Mount IDs follow first-unused EQ identities and equal tags in spec order." (let* ((tp--mount-id-counter 1000) (object (tp--make-surface-object :id 1)) (equal-object (copy-sequence object)) (other-object (tp--make-surface-object :id 2)) (anchor (tp--make-range-anchor :id 1)) (equal-anchor (copy-sequence anchor)) (index (make-hash-table :test #'eq)) (records (list (list nil object 'content anchor '(:slot unused)) (list 11 object 'content anchor '(:slot duplicate)) (list 12 object 'properties anchor '(:slot duplicate)) (list nil object 'content anchor '(:slot duplicate)) (list 13 object 'content equal-anchor '(:slot duplicate)) (list 14 object 'content anchor '(:slot other)) (list 21 equal-object 'content anchor '(:slot duplicate)) (list 31 other-object 'content nil '(:slot other)))) (live (cl-loop for (id owner capability attachment tags) in records for position from 1 collect (tp--make-surface-mount :id id :object owner :capability capability :anchor attachment :tags (copy-tree tags) :start position :end (1+ position)))) (spec-signatures (list (list object anchor '(:slot other)) (list equal-object anchor '(:slot duplicate)) (list object anchor '(:slot duplicate)) (list object equal-anchor '(:slot duplicate)) (list object anchor '(:slot duplicate)) (list object anchor '(:slot duplicate)) (list equal-object equal-anchor '(:slot duplicate)) (list equal-object anchor '(:slot duplicate)) (list other-object nil '(:slot other)) (list object anchor '(:slot absent)))) (specs (cl-loop for (owner attachment tags) in spec-signatures for position from 101 collect (list :object owner :anchor attachment :tags (copy-tree tags) :mount-id -1 :start position :end (+ position 2)))) (surface (tp--make-surface :capability 'content :mounts live :mount-index index)) (prepared (tp--make-prepared-surface :surface surface :mount-specs specs)) saved-buckets) (should (equal object equal-object)) (should-not (eq object equal-object)) (should (equal anchor equal-anchor)) (should-not (eq anchor equal-anchor)) (dolist (mount live) (let ((owner (tp--surface-mount-object mount))) (puthash owner (append (gethash owner index) (list mount)) index))) ;; Preserve the matcher's single-use and identity checks even when a ;; bucket repeats a record or contains an entry owned by another object. (puthash object (append (gethash object index) (list (nth 1 live) (nth 6 live))) index) (maphash (lambda (owner bucket) (push (list owner bucket (copy-sequence bucket) (cl-loop for tail on bucket collect tail)) saved-buckets)) index) (should (eq prepared (tp--assign-prepared-mount-ids prepared))) (should (equal (mapcar (lambda (spec) (plist-get spec :mount-id)) (tp--prepared-surface-mount-specs prepared)) '(14 21 11 13 1001 1002 1003 1004 31 1005))) (should (= tp--mount-id-counter 1005)) ;; An unmatched live mount must not receive an ID while an index is built. (should-not (tp--surface-mount-id (car live))) (should (= (tp--surface-mount-id (nth 3 live)) 1001)) (should (eq live (tp--surface-mounts surface))) (should (eq index (tp--surface-mount-index surface))) (should (= (hash-table-count index) 3)) (dolist (saved saved-buckets) (let ((bucket (gethash (nth 0 saved) index))) (should (eq (nth 1 saved) bucket)) (should (equal (nth 2 saved) bucket)) (should (cl-every #'identity (cl-mapcar #'eq (nth 3 saved) (cl-loop for tail on bucket collect tail)))))) (cl-loop for spec in (tp--prepared-surface-mount-specs prepared) for (owner attachment tags) in spec-signatures for position from 101 do (should (eq owner (plist-get spec :object))) (should (eq attachment (plist-get spec :anchor))) (should (equal tags (plist-get spec :tags))) (should (= position (plist-get spec :start))) (should (= (+ position 2) (plist-get spec :end)))))) (ert-deftest tp-surface-test-prepared-mount-ids-bound-candidate-scans () "Growing duplicate mounts must not repeatedly scan their used prefix." (let ((assign (tp-surface-test--source-function 'tp--assign-prepared-mount-ids)) (find-if (symbol-function 'cl-find-if)) samples) (dolist (count '(16 32 64)) (let* ((tp--mount-id-counter 1000) (object (tp--make-surface-object :id 1)) (live (cl-loop for id from 1 to count collect (tp--make-surface-mount :id id :object object :capability 'content :tags (list :slot 'same) :start id :end (1+ id)))) (index (make-hash-table :test #'eq)) (surface (tp--make-surface :capability 'content :mounts live :mount-index index)) (prepared (tp--make-prepared-surface :surface surface :mount-specs (cl-loop for position from 101 below (+ 101 count) collect (list :object object :tags (list :slot 'same) :start position :end (1+ position))))) (visits 0)) (puthash object live index) ;; Count actual predicate visits, including candidates rejected by the ;; used-prefix check before comparing their matching signature. ;; This bounds candidate scans, not the complete assignment algorithm. (cl-letf (((symbol-function 'cl-find-if) (lambda (predicate sequence &rest options) (apply find-if (lambda (mount) (cl-incf visits) (funcall predicate mount)) sequence options)))) (funcall assign prepared)) (should (equal (mapcar (lambda (spec) (plist-get spec :mount-id)) (tp--prepared-surface-mount-specs prepared)) (number-sequence 1 count))) (should (= tp--mount-id-counter 1000)) (push (cons count visits) samples))) (ert-info ((format "mount count / candidate visits: %S" (reverse samples))) (dolist (sample samples) (should (<= (cdr sample) (* 2 (car sample)))))))) (ert-deftest tp-surface-test-disjoint-mount-index-rolls-back-atomically () "Failed publication restores every mount of a retained logical object." (tp-surface-test--with-buffer (let ((value "A")) (let* ((producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (logical (tp-object-ensure context root 'logical 'item)) (left (tp-object-ensure context root 'left 'fragment)) (right (tp-object-ensure context root 'right 'fragment))) (tp-object-retain context logical) (tp-object-attach-fragment context logical left 'left) (tp-object-attach-fragment context logical right 'right)) (tp-surface-plan-create :key 'root :kind 'group :capability 'content :children (list (tp-surface-test--leaf 'left value) (tp-surface-test--leaf 'right value))))) (surface (tp-surface-mount buffer producer '(:capability content))) (logical (tp-object-resolve surface '(root logical))) (mounts (tp-object-mounts logical)) (live (tp--surface-mounts surface)) (index (tp--surface-mount-index surface)) (bucket (gethash logical index)) (mount-ids (mapcar #'tp--surface-mount-id live)) (revision (tp-surface-revision surface))) (setq value "LONG") (let ((tp--surface-publication-step-function (lambda (step _surface) (when (eq step 'client-state) (error "Injected mount-index failure"))))) (should-error (tp-surface-update surface producer))) (should (= (tp-surface-revision surface) revision)) (should (equal (buffer-string) "AA")) (should (eq logical (tp-object-resolve surface '(root logical)))) (should (equal (tp-object-mounts logical) mounts)) (should (eq live (tp--surface-mounts surface))) (should (eq index (tp--surface-mount-index surface))) (should (eq bucket (gethash logical index))) (should (equal mount-ids (mapcar #'tp--surface-mount-id live))) (tp-surface-update surface producer) (should (= (tp-surface-revision surface) (1+ revision))) (should (equal (buffer-string) "LONGLONG")) (should (eq logical (tp-object-resolve surface '(root logical)))) (should (equal mount-ids (mapcar #'tp--surface-mount-id (tp--surface-mounts surface)))) (should (equal (tp-object-mounts logical) '((:start 1 :end 5 :tags left) (:start 5 :end 9 :tags right)))))))) (ert-deftest tp-surface-test-properties-capability-rejects-text () "A properties-only mount cannot replace host text." (tp-surface-test--with-buffer (insert "host") (should-error (tp-surface-mount buffer (tp-surface-test--leaf 'root "replacement") '(:capability properties)) :type 'tp-capability-error) (should (equal (buffer-string) "host")))) (ert-deftest tp-surface-test-range-anchor-follows-edits-before-it () "A host insertion before an anchor moves its property contribution." (tp-surface-test--with-buffer (insert "012345") (let* ((anchor (tp-range-anchor-create buffer 3 5)) (value "A") (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props (list 'help-echo value) :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (should (equal (get-text-property 3 'help-echo) "A")) (goto-char 1) (insert "X") (setq value "B") (tp-surface-update surface producer) (should (equal (get-text-property 4 'help-echo) "B")) (should-not (get-text-property 3 'help-echo))))) (ert-deftest tp-surface-test-property-conflict-needs-explicit-rebase () "TP preserves an external property write until the anchor is rebased." (tp-surface-test--with-buffer (insert "host") (let* ((anchor (tp-range-anchor-create buffer 1 5)) (value "A") (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props (list 'help-echo value) :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties))) (revision (tp-surface-revision surface))) (put-text-property 1 5 'help-echo "external") (setq value "B") (should-error (tp-surface-update surface producer) :type 'tp-property-conflict) (should (= (tp-surface-revision surface) revision)) (should (equal (get-text-property 2 'help-echo) "external")) (tp-range-rebase anchor) (tp-surface-update surface producer) (should (equal (get-text-property 2 'help-echo) "B")) (tp-surface-unmount surface) (should (equal (get-text-property 2 'help-echo) "external"))))) (ert-deftest tp-surface-test-property-update-uses-policy-equality () "Retained property comparison uses the registered text-property policy." (tp-surface-test--with-buffer (insert "host") (let* ((old-policy (tp-property-policy 'text/help-echo)) (anchor (tp-range-anchor-create buffer 1 5)) (value "A") surface) (unwind-protect (progn (tp-define-property-policy 'text/help-echo :equality (lambda (left right) (string-equal (downcase left) (downcase right))) :merge (lambda (_old new) new) :projector (lambda (v) (list 'help-echo v))) (setq surface (tp-surface-mount buffer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props (list 'help-echo value) :capability 'properties)) '(:capability properties))) (let ((revision (tp-surface-revision surface))) (setq value "a") (tp-surface-update surface (tp--surface-producer surface)) (should (= (tp-surface-revision surface) revision)) (should (= (plist-get (tp-surface-report surface) :property-operations) 1)) (should (equal (get-text-property 2 'help-echo) "A")))) (when old-policy (puthash 'text/help-echo old-policy tp--property-policies)) (when (and surface (tp-surface-live-p surface)) (tp-surface-unmount surface)))))) (ert-deftest tp-surface-test-content-property-diff-uses-policy-equality () "Content publication skips policy-equal property writes." (tp-surface-test--with-buffer (let ((old-policy (tp-property-policy 'text/help-echo)) (value "A") (client-state 1) surface producer) (unwind-protect (progn (tp-define-property-policy 'text/help-echo :equality (lambda (left right) (string-equal (downcase left) (downcase right))) :merge (lambda (_old new) new) :projector (lambda (current) (list 'help-echo current))) (setq producer (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-result-create (tp-surface-test--leaf 'root "text" (list 'help-echo value)) (list :state client-state))) surface (tp-surface-mount buffer producer '(:capability content))) (let ((revision (tp-surface-revision surface))) (setq value "a" client-state 2) (let ((report (tp-surface-update surface producer))) (should (= (tp-surface-revision surface) (1+ revision))) (should (= (plist-get report :property-operations) 0)) (should (equal (get-text-property 1 'help-echo) "A"))))) (when old-policy (puthash 'text/help-echo old-policy tp--property-policies)) (when (and surface (tp-surface-live-p surface)) (tp-surface-unmount surface)))))) (ert-deftest tp-surface-test-content-property-diff-detects-interior-boundary () "Content property diffing notices a mismatch after a matching first char." (tp-surface-test--with-buffer (insert "abc") (put-text-property 1 2 'face 'bold) (let ((rendered (copy-sequence "abc"))) (put-text-property 0 3 'face 'bold rendered) (should (tp--string-property-run-diff-p buffer 1 rendered 0 3))))) (ert-deftest tp-surface-test-unmount-preserves-conflicting-host-value () "Unmount removes only TP's still-current property contribution." (tp-surface-test--with-buffer (insert "host") (let* ((anchor (tp-range-anchor-create buffer 1 5)) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo "tp") :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (put-text-property 1 5 'help-echo "external") (let ((report (tp-surface-unmount surface))) (should (plist-get report :property-conflicts))) (should (equal (get-text-property 2 'help-echo) "external")) (should-not (tp-surface-live-p surface)) (should-not (tp-range-anchor-live-p anchor))))) (ert-deftest tp-surface-test-complete-anchor-deletion-applies-boundary-policy () "Deleting an entire anchor span applies stale, shorten, and remove policy." (dolist (case '((stale . t) (shorten . nil) (remove . remove))) (tp-surface-test--with-buffer (insert "abcd") (let* ((policy (car case)) (expected (cdr case)) (anchor (tp-range-anchor-create buffer 2 4 :boundary-policy policy)) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo "tp") :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (delete-region 2 4) (should (eq (tp--anchor-stale anchor) expected)) (when expected (should-error (tp-surface-update surface producer) :type 'tp-stale-mount)))))) (ert-deftest tp-surface-test-complete-content-deletion-marks-surface-stale () "Deleting a content surface's full span makes the mount stale." (tp-surface-test--with-buffer (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "abc") '(:capability content)))) (delete-region 1 4) (should (tp--surface-stale surface)) (should-error (tp-surface-update surface (tp-surface-test--leaf 'root "next")) :type 'tp-stale-mount)))) (ert-deftest tp-surface-test-failed-prepare-rolls-back-direct-buffer-mutation () "Producer buffer edits during prepare roll back when preparation fails." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (revision (tp-surface-revision surface))) (should-error (tp-surface-update surface (lambda (context) (goto-char (point-min)) (insert "BAD") (tp-object-ensure context nil 'other 'text) (tp-surface-test--leaf 'root "new"))) :type 'tp-surface-error) (should (equal (buffer-string) "old")) (should-not (tp--surface-stale surface)) (should (= (tp-surface-revision surface) revision))))) (ert-deftest tp-surface-test-successful-prepare-rejects-direct-buffer-mutation () "Producers cannot commit live surface buffers outside TP publication." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (revision (tp-surface-revision surface))) (should-error (tp-surface-update surface (lambda (context) (tp-object-ensure context nil 'root 'text) (goto-char (point-min)) (insert "BAD") (tp-surface-test--leaf 'root "new"))) :type 'tp-surface-error) (should (equal (buffer-string) "old")) (should-not (tp--surface-stale surface)) (should (= (tp-surface-revision surface) revision))))) (ert-deftest tp-surface-test-prepare-rejects-direct-property-mutation () "Producers cannot write live surface properties during prepare." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old" '(help-echo "old")) '(:capability content))) (revision (tp-surface-revision surface))) (should-error (tp-surface-update surface (lambda (context) (tp-object-ensure context nil 'root 'text) (put-text-property (point-min) (1+ (point-min)) 'help-echo "BAD") (tp-surface-test--leaf 'root "new" '(help-echo "new")))) :type 'tp-producer-buffer-mutation) (should (equal (buffer-string) "old")) (should (equal (get-text-property 1 'help-echo) "old")) (should-not (tp--surface-stale surface)) (should (= (tp-surface-revision surface) revision))))) (ert-deftest tp-surface-test-boundary-crossing-uses-pre-edit-ranges () "A deletion crossing the old right boundary marks retained ranges stale." (tp-surface-test--with-buffer (insert "abcd") (let* ((anchor (tp-range-anchor-create buffer 2 4)) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo "tp") :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (delete-region 3 5) (should (tp--anchor-stale anchor)) (should-error (tp-surface-update surface producer) :type 'tp-stale-mount)))) (ert-deftest tp-surface-test-content-external-edit-marks-mount-stale () "An external edit inside content-owned text prevents silent overwrite." (tp-surface-test--with-buffer (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "abc") '(:capability content)))) (goto-char 2) (insert "X") (should-error (tp-surface-update surface (tp-surface-test--leaf 'root "next")) :type 'tp-stale-mount) (should (equal (buffer-string) "aXbc"))))) (ert-deftest tp-surface-test-publication-steps-roll-back-exactly () "Failure at each publication step keeps the prior surface revision." (dolist (step '(text property marker index client-state)) (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (revision (tp-surface-revision surface)) (object (tp-object-resolve surface '(root))) (reached nil) (producer (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-result-create (tp-surface-test--leaf 'root "new" '(face bold)) (list :candidate step)))) (tp--surface-publication-step-function (lambda (current _surface) (push current reached) (when (eq current step) (error "Injected %s failure" step))))) (should-error (tp-surface-update surface producer)) (should (memq step reached)) (should (equal-including-properties (buffer-string) "old")) (should (= (tp-surface-revision surface) revision)) (should (eq object (tp-object-resolve surface '(root)))))))) (ert-deftest tp-surface-test-full-and-scoped-precommit-stages-roll-back () "Every full and scoped precommit stage restores the complete old state." (dolist (mode '(full scoped)) (dolist (stage '(validate view anchors lifecycle observers cleanup)) (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 1)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (object (tp-object-resolve surface '(root))) (revision (tp-surface-revision surface)) (transaction-id tp--surface-transaction-id) (fail-stage stage) (tp--surface-precommit-step-function (lambda (current _state) (when (eq current fail-stage) (error "Injected precommit %S" current))))) (goto-char (point-min)) (should-error (tp-surface-test--update-mode mode surface object (tp-surface-test--leaf 'root "new") signal 2)) (should (equal (buffer-string) "old")) (should (= (tp-surface-revision surface) revision)) (should (= tp--surface-transaction-id transaction-id)) (should (= (tp-signal-peek signal) 1)) (should (= (tp-signal-revision signal) 0)) (should (eq object (tp-object-resolve surface '(root)))) (should (= (point) (point-min))) (setq fail-stage nil) (tp-surface-test--update-mode mode surface object (tp-surface-test--leaf 'root "new") signal 2) (should (equal (buffer-string) "new")) (should (= (tp-surface-revision surface) (1+ revision))) (should (= (tp-signal-peek signal) 2)) (should (= (tp-signal-revision signal) 1))))))) (ert-deftest tp-surface-test-full-and-scoped-final-accept-roll-back () "Final-accept error or quit rolls full and scoped state back exactly." (dolist (mode '(full scoped)) (dolist (injected '((error "Final accept error") (quit))) (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 1)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (object (tp-object-resolve surface '(root))) (revision (tp-surface-revision surface)) (transaction-id tp--surface-transaction-id) (original (symbol-function 'accept-change-group)) failure) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (signal (car injected) (cdr injected))))) (setq failure (tp-surface-test--capture-condition (lambda () (tp-surface-test--update-mode mode surface object (tp-surface-test--leaf 'root "new") signal 2))))) (should (eq (car failure) (car injected))) (should (equal (buffer-string) "old")) (should (= (tp-surface-revision surface) revision)) (should (= tp--surface-transaction-id transaction-id)) (should (= (tp-signal-peek signal) 1)) (should (= (tp-signal-revision signal) 0)) (should (eq object (tp-object-resolve surface '(root)))) (cl-letf (((symbol-function 'accept-change-group) original)) (tp-surface-test--update-mode mode surface object (tp-surface-test--leaf 'root "new") signal 2)) (should (equal (buffer-string) "new")) (should (= (tp-surface-revision surface) (1+ revision))) (should (= (tp-signal-revision signal) 1))))))) (ert-deftest tp-surface-test-final-accept-pending-quit-runs-postaccept () "A pending quit raised by accept cannot skip committed cleanup or observers." (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 1)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (old-mounts (copy-sequence (tp--surface-mounts surface))) (original (symbol-function 'accept-change-group)) cleanup-ran observer-ran) (setf (tp--surface-observers surface) (list (lambda (_surface _report) (setq observer-ran t)))) (let ((tp--surface-cleanup-step-function (lambda (_surface _owner) (setq cleanup-ran t)))) (cl-letf (((symbol-function 'accept-change-group) (lambda (group) (funcall original group) (setq quit-flag t)))) (tp-with-transaction (tp-signal-set signal 2) (tp-surface-update surface (tp-surface-test--leaf 'root "new"))))) (should (equal (buffer-string) "new")) (should (= (tp-signal-peek signal) 2)) (should cleanup-ran) (should observer-ran) (dolist (mount old-mounts) (when (eq (tp--surface-mount-capability mount) 'content) (should-not (marker-position (tp--surface-mount-start mount))) (should-not (marker-position (tp--surface-mount-end mount)))))))) (ert-deftest tp-surface-test-precommit-validates-final-object-state () "Precommit rejects corrupted published object state and rolls it back." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (object (tp-object-resolve surface '(root))) (corrupt t) (tp--surface-precommit-step-function (lambda (stage _state) (when (and corrupt (eq stage 'validate)) (setf (tp--surface-object-live object) nil))))) (should-error (tp-surface-update surface (tp-surface-test--leaf 'root "new"))) (should (equal (buffer-string) "old")) (should (tp--surface-object-live object)) (setq corrupt nil) (tp-surface-update surface (tp-surface-test--leaf 'root "new")) (should (equal (buffer-string) "new"))))) (ert-deftest tp-surface-test-postaccept-failures-are-contained-and-recorded () "Cleanup, observer, committed, and callback failures cannot undo accept." (dolist (cleanup-condition '((error "Cleanup error") (quit))) (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 1)) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (old-mounts (copy-sequence (tp--surface-mounts surface))) (cleanup-fired nil) (tp--surface-cleanup-step-function (lambda (_surface owner) (when (and (eq owner 'view-markers) (not cleanup-fired)) (setq cleanup-fired t) (signal (car cleanup-condition) (cdr cleanup-condition))))) (tp--transaction-committed-functions (append tp--transaction-committed-functions (list (lambda () (error "Committed failure")))))) (setf (tp--surface-observers surface) (list (lambda (_surface _report) (error "Observer error")) (lambda (_surface _report) (signal 'quit nil)))) (tp-with-transaction (tp--enqueue-after-commit (lambda () (error "Callback error"))) (tp--enqueue-after-commit (lambda () (signal 'quit nil))) (tp-signal-set signal 2) (tp-surface-update surface (tp-surface-test--leaf 'root "new"))) (should (equal (buffer-string) "new")) (should (= (tp-signal-peek signal) 2)) (should (= (tp-signal-revision signal) 1)) (should (= (length (plist-get (tp-surface-report surface) :cleanup-errors)) 1)) (should (= (length (plist-get (tp-surface-report surface) :observer-errors)) 2)) (should (equal (mapcar #'car tp--last-transaction-diagnostics) '(committed after-commit after-commit))) (dolist (mount old-mounts) (when (eq (tp--surface-mount-capability mount) 'content) (should-not (marker-position (tp--surface-mount-start mount))) (should-not (marker-position (tp--surface-mount-end mount))))))))) (ert-deftest tp-surface-test-view-capture-cleans-partial-markers () "A partial view snapshot failure disposes every marker already allocated." (tp-surface-test--with-buffer (let ((original (symbol-function 'copy-marker)) (calls 0) created) (cl-letf (((symbol-function 'get-buffer-window-list) (lambda (&rest _arguments) (list (selected-window)))) ((symbol-function 'copy-marker) (lambda (&rest arguments) (cl-incf calls) (when (= calls 2) (error "Second marker failure")) (let ((marker (apply original arguments))) (push marker created) marker)))) (should-error (tp--capture-view-state (list buffer)))) (should (= (length created) 1)) (should-not (marker-position (car created)))))) (ert-deftest tp-surface-test-anchor-apply-failure-uses-complete-undo-journal () "An anchor mutation failure restores old opaque ownership before retry." (tp-surface-test--with-buffer (insert "host") (let* ((old-anchor (tp-range-anchor-create buffer 1 5)) (new-anchor (tp-range-anchor-create buffer 1 5)) (current old-anchor) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object current)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo "owned") :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties))) (original (symbol-function 'tp--apply-anchor-ownership))) (setq current new-anchor) (cl-letf (((symbol-function 'tp--apply-anchor-ownership) (lambda (&rest arguments) (apply original arguments) (error "After anchor ownership mutation")))) (should-error (tp-surface-update surface producer))) (should (memq surface (tp--anchor-surfaces old-anchor))) (should-not (memq surface (tp--anchor-surfaces new-anchor))) (should (tp-range-anchor-live-p old-anchor)) (tp-surface-update surface producer) (should-not (tp-range-anchor-live-p old-anchor)) (should (memq surface (tp--anchor-surfaces new-anchor)))))) (ert-deftest tp-surface-test-phase2-failure-does-not-skip-later-undo () "A phase-2 restoration failure still runs surface, view, and anchor undo." (tp-surface-test--with-buffer (let* ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") '(:capability content))) (restore-properties (symbol-function 'tp--restore-property-journals)) (restore-snapshot (symbol-function 'tp--restore-surface-snapshot)) (restore-view (symbol-function 'tp--restore-view-state)) (undo-anchors (symbol-function 'tp--undo-anchor-ownership)) trace failure) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (setq trace nil) (error "Primary accept failure"))) ((symbol-function 'tp--restore-property-journals) (lambda (journals) (funcall restore-properties journals) (push 'properties trace) (error "Property rollback failure"))) ((symbol-function 'tp--restore-surface-snapshot) (lambda (&rest arguments) (push 'surface trace) (apply restore-snapshot arguments))) ((symbol-function 'tp--restore-view-state) (lambda (&rest arguments) (push 'view trace) (apply restore-view arguments))) ((symbol-function 'tp--undo-anchor-ownership) (lambda (undo) (push 'anchors trace) (funcall undo-anchors undo)))) (setq failure (tp-surface-test--capture-condition (lambda () (tp-surface-update surface (tp-surface-test--leaf 'root "new")))))) (should (eq (car failure) 'error)) (should (equal (cadr failure) "Primary accept failure")) (should (equal (nreverse trace) '(properties surface view anchors))) (should (equal (buffer-string) "old")) (should (equal (mapcar #'car (tp--transaction-condition-trailer failure :rollback-failures)) '(rollback-hooks)))))) (ert-deftest tp-surface-test-global-signal-update-is-multi-surface-atomic () "A second-surface failure rolls back buffers, bindings, and source value." (let* ((signal (tp-signal-create 1)) (first-buffer (generate-new-buffer " *tp-surface-first*")) (second-buffer (generate-new-buffer " *tp-surface-second*")) (producer (tp-surface-test--producer signal)) first second) (unwind-protect (progn (setq first (tp-surface-mount first-buffer producer '(:capability content)) second (tp-surface-mount second-buffer producer '(:capability content))) (let ((first-revision (tp-surface-revision first)) (second-revision (tp-surface-revision second)) (tp--surface-publication-step-function (lambda (step surface) (when (and (eq step 'client-state) (eq surface second)) (error "Injected second-surface failure"))))) (should-error (tp-signal-set signal 2)) (should (= (tp-signal-peek signal) 1)) (should (= (tp-surface-revision first) first-revision)) (should (= (tp-surface-revision second) second-revision)) (should (equal (tp-surface-client-state first) '(:value 1))) (should (equal (tp-surface-client-state second) '(:value 1))) (with-current-buffer first-buffer (should (equal (buffer-string) "1"))) (with-current-buffer second-buffer (should (equal (buffer-string) "1"))))) (when (buffer-live-p first-buffer) (kill-buffer first-buffer)) (when (buffer-live-p second-buffer) (kill-buffer second-buffer))))) (ert-deftest tp-surface-test-two-buffer-identities-are-isolated () "The same keyed producer creates separate object identity per surface." (let* ((first-buffer (generate-new-buffer " *tp-surface-first*")) (second-buffer (generate-new-buffer " *tp-surface-second*")) (producer (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-test--leaf 'root (buffer-name (current-buffer))))) first second) (unwind-protect (progn (setq first (tp-surface-mount first-buffer producer '(:capability content)) second (tp-surface-mount second-buffer producer '(:capability content))) (should-not (eq (tp-object-resolve first '(root)) (tp-object-resolve second '(root)))) (should-error (tp-surface-update-scoped first (list (tp-object-resolve second '(root))) producer) :type 'tp-cross-surface-object)) (when (buffer-live-p first-buffer) (kill-buffer first-buffer)) (when (buffer-live-p second-buffer) (kill-buffer second-buffer))))) (ert-deftest tp-surface-test-unmount-cleans-weak-registry-and-markers () "Unmount releases weak surface registration and marker-backed state." (tp-surface-test--with-buffer (insert "host") (let* ((anchor (tp-range-anchor-create buffer 1 5)) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo "tp") :capability 'properties))) (surface (tp-surface-mount buffer producer (list :capability 'properties :observers (list #'ignore)))) (id (tp--surface-id surface)) (ledger (tp--surface-ledger surface)) (mounts (tp--surface-mounts surface))) (should (eq (gethash id tp--surfaces) surface)) (tp-surface-unmount surface) (should-not (gethash id tp--surfaces)) (should-not (tp--surface-observers surface)) (should-not (tp-range-anchor-live-p anchor)) (dolist (entry ledger) (should-not (marker-position (tp--property-ledger-start entry))) (should-not (marker-position (tp--property-ledger-end entry)))) (dolist (mount mounts) (should-not (marker-position (tp--surface-mount-start mount))) (should-not (marker-position (tp--surface-mount-end mount))))))) (ert-deftest tp-surface-test-materialize-matches-first-content-mount () "Pure and live publication produce identical propertized text." (tp-surface-test--with-buffer (let* ((child (tp-surface-test--leaf 'child "text" '(face (:foreground "white")))) (plan (tp-surface-plan-create :key 'root :kind 'group :children (list child) :props '(help-echo "root") :capability 'content)) (materialized (tp-surface-materialize-string plan))) (tp-surface-mount buffer plan '(:capability content)) (should (equal-including-properties materialized (buffer-string)))))) (ert-deftest tp-surface-test-index-diagnostics-use-side-state () "Point queries and reports resolve through the retained side index." (tp-surface-test--with-buffer (let* ((child (tp-surface-test--leaf 'child "x")) (plan (tp-surface-plan-create :key 'root :kind 'group :children (list child) :tags '(:role root) :capability 'content)) (surface (tp-surface-mount buffer plan '(:capability content))) (objects (tp-surface-at-point 1 buffer)) (inspection (tp-surface-inspect surface))) (should (= (length objects) 2)) (should (eq (plist-get inspection :surface) surface)) (should (= (plist-get inspection :revision) 1)) (should (equal (plist-get (tp-surface-report surface) :surface-id) (plist-get inspection :id)))))) (ert-deftest tp-surface-test-kill-buffer-disposes-runtime () "Killing the lifecycle owner releases its surface and objects." (let* ((buffer (generate-new-buffer " *tp-surface-kill*")) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "x") (list :capability 'content :observers (list #'ignore)))) (object (tp-object-resolve surface '(root)))) (kill-buffer buffer) (should-not (tp-surface-live-p surface)) (should-not (tp--surface-observers surface)) (should-not (tp-object-live-p object)))) (ert-deftest tp-surface-test-omitted-binding-defaults-to-deletion () "A surviving object does not retain an omitted binding by accident." (tp-surface-test--with-buffer (let ((signal (tp-signal-create 1)) (include t) binding) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (when include (setq binding (tp-bind object '(test . optional) (lambda () (tp-signal-read signal)))) (tp-binding-read binding))) (tp-surface-test--leaf 'root "value"))) (surface (tp-surface-mount buffer producer '(:capability content)))) (should (= (tp-signal-subscriber-count signal) 1)) (setq include nil) (tp-surface-update surface producer) (should-not (tp-binding-live-p binding)) (should (= (tp-signal-subscriber-count signal) 0)))))) (ert-deftest tp-surface-test-explicit-retain-keeps-omitted-binding () "An explicit retain lifecycle keeps an omitted computation subscribed." (tp-surface-test--with-buffer (let ((signal (tp-signal-create 1)) (include t) binding) (let* ((producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (when include (setq binding (tp-bind object '(test . retained) (lambda () (tp-signal-read signal)) :lifecycle 'retain)) (tp-binding-read binding))) (tp-surface-test--leaf 'root "value"))) (surface (tp-surface-mount buffer producer '(:capability content)))) (setq include nil) (tp-surface-update surface producer) (should (tp-binding-live-p binding)) (should (= (tp-signal-subscriber-count signal) 1)))))) (ert-deftest tp-surface-test-read-only-and-narrowing-policy-is-explicit () "Content publication requires opt-in for read-only buffers and preserves narrowing." (tp-surface-test--with-buffer (insert "012345") (narrow-to-region 2 5) (setq buffer-read-only t) (should-error (tp-surface-mount buffer (tp-surface-test--leaf 'root "x") '(:capability content :start 2 :end 5)) :type 'buffer-read-only) (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "x") '(:capability content :start 2 :end 5 :inhibit-read-only t)))) (should buffer-read-only) (should (buffer-narrowed-p)) (should (equal (buffer-substring-no-properties (point-min) (point-max)) "x")) (tp-surface-unmount surface)))) (ert-deftest tp-surface-test-no-observer-skips-report-snapshot () "A surface without observers constructs no observation event or timing." (tp-surface-test--with-buffer (let ((copies 0) (clock-calls 0) surface (original (symbol-function 'tp-surface-report))) (cl-letf (((symbol-function 'tp-surface-report) (lambda (&rest arguments) (cl-incf copies) (apply original arguments))) ((symbol-function 'tp--surface-observation-clock) (lambda () (cl-incf clock-calls) 1.0))) (setq surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "committed") '(:capability content)))) (should (= copies 0)) (should (= clock-calls 0)) (let ((report (tp-surface-report surface))) (should-not (plist-get report :provider)) (should-not (plist-get report :stage)) (should-not (plist-get report :duration-ms)) (should-not (plist-get report :timing)))))) (ert-deftest tp-surface-test-observer-registration-is-public-and-idempotent () "Observers can be added and removed without duplicates." (tp-surface-test--with-buffer (let* ((calls 0) (observer (lambda (_surface _report) (cl-incf calls))) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "one") '(:capability content)))) (should-error (tp-surface-add-observer surface 'not-a-function) :type 'wrong-type-argument) (should-error (tp-surface-remove-observer surface 'not-a-function) :type 'wrong-type-argument) (should (eq (tp-surface-add-observer surface observer) surface)) (should (eq (tp-surface-add-observer surface observer) surface)) (should (= (length (tp--surface-observers surface)) 1)) (tp-surface-update surface (tp-surface-test--leaf 'root "two")) (should (= calls 1)) (should (eq (tp-surface-remove-observer surface observer) surface)) (tp-surface-update surface (tp-surface-test--leaf 'root "three")) (should (= calls 1))))) (ert-deftest tp-surface-test-observer-report-has-publication-metadata () "An enabled observer receives provider, stage, and duration metadata." (tp-surface-test--with-buffer (let ((clock-values '(10.0 10.025)) observed-report) (cl-letf (((symbol-function 'tp--surface-observation-clock) (lambda () (pop clock-values)))) (tp-surface-mount buffer (tp-surface-test--leaf 'root "committed") (list :capability 'content :observers (list (lambda (_surface report) (setq observed-report report)))))) (should (eq (plist-get observed-report :provider) 'tp)) (should (eq (plist-get observed-report :stage) 'publication)) (should (< (abs (- (plist-get observed-report :duration-ms) 25.0)) 0.001)) (should (plist-member observed-report :transaction-id)) (should (plist-member observed-report :scope-count)) (should (plist-member observed-report :text-operations))))) (ert-deftest tp-surface-test-observation-does-not-change-committed-state () "Observed and unobserved surfaces commit equivalent text and state." (let ((plain-buffer (generate-new-buffer " *tp-plain*")) (observed-buffer (generate-new-buffer " *tp-observed*"))) (unwind-protect (let* ((plan (tp-surface-test--leaf 'root "same" '(face bold help-echo "same"))) (plain (tp-surface-mount plain-buffer plan '(:capability content))) (observed (tp-surface-mount observed-buffer plan (list :capability 'content :observers (list #'ignore))))) (should (equal-including-properties (with-current-buffer plain-buffer (buffer-substring (point-min) (point-max))) (with-current-buffer observed-buffer (buffer-substring (point-min) (point-max))))) (should (= (tp-surface-revision plain) (tp-surface-revision observed))) (should (equal (tp-surface-client-state plain) (tp-surface-client-state observed))) (let ((next (tp-surface-test--leaf 'root "updated" '(face italic help-echo "updated")))) (tp-surface-update plain next) (tp-surface-update observed next)) (should (equal-including-properties (with-current-buffer plain-buffer (buffer-substring (point-min) (point-max))) (with-current-buffer observed-buffer (buffer-substring (point-min) (point-max))))) (should (= (tp-surface-revision plain) (tp-surface-revision observed))) (should (equal (tp-surface-client-state plain) (tp-surface-client-state observed)))) (when (buffer-live-p plain-buffer) (kill-buffer plain-buffer)) (when (buffer-live-p observed-buffer) (kill-buffer observed-buffer))))) (ert-deftest tp-surface-test-observer-failure-does-not-roll-back () "Observer errors are recorded after a successful publication." (tp-surface-test--with-buffer (let ((surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "committed") (list :capability 'content :observers (list (lambda (_surface _report) (error "Observer failure"))))))) (should (equal (buffer-string) "committed")) (should (= (tp-surface-revision surface) 1)) (should (= (length (plist-get (tp-surface-report surface) :observer-errors)) 1))))) (ert-deftest tp-surface-test-observer-runs-only-after-accepted-publication () "A rolled-back publication emits no observation; a retry emits one." (tp-surface-test--with-buffer (let* ((calls 0) (observer (lambda (_surface _report) (cl-incf calls))) (surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old" '(face bold help-echo "old")) (list :capability 'content :observers (list observer)))) (revision (tp-surface-revision surface)) (old (buffer-substring (point-min) (point-max))) (next (tp-surface-test--leaf 'root "new" '(face italic help-echo "new")))) (setq calls 0) (cl-letf (((symbol-function 'accept-change-group) (lambda (_group) (error "Injected final-accept failure")))) (should-error (tp-surface-update surface next))) (should (= calls 0)) (should (= (tp-surface-revision surface) revision)) (should (equal-including-properties old (buffer-substring (point-min) (point-max)))) (tp-surface-update surface next) (should (= calls 1)) (should (= (tp-surface-revision surface) (1+ revision))) (should (equal-including-properties (buffer-substring (point-min) (point-max)) (propertize "new" 'face 'italic 'help-echo "new")))))) (ert-deftest tp-surface-test-observer-write-starts-a-new-transaction () "An observer signal write runs after the publishing transaction exits." (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 1)) (write-once t) (producer (tp-surface-test--producer signal)) (surface (tp-surface-mount buffer producer (list :capability 'content :observers (list (lambda (_surface _report) (when write-once (setq write-once nil) (tp-signal-set signal 2)))))))) (should (= (tp-signal-peek signal) 2)) (should (equal (buffer-string) "2")) (should (= (tp-surface-revision surface) 2))))) (ert-deftest tp-surface-test-transaction-participant-sees-published-state () "A participant promotes side state before observers run." (tp-surface-test--with-buffer (let (surface events) (setq surface (tp-surface-mount buffer (tp-surface-test--leaf 'root "old") (list :capability 'content :observers (list (lambda (_surface _report) (push (list 'observer (buffer-string)) events)))))) (setq events nil) (tp-with-transaction (tp-transaction-participate-v2 :key '(test . promotion) :stage (lambda () (push (list 'participant (buffer-string) (tp-surface-client-state surface)) events)) :rollback (lambda () (push '(rollback) events))) (tp-surface-update surface (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-result-create (tp-surface-test--leaf 'root "new") '(:generation 2))))) (should (equal (nreverse events) '((participant "new" (:generation 2)) (observer "new"))))))) (ert-deftest tp-surface-test-failing-participant-rolls-back-every-owner () "A participant failure restores surface, source, and external state." (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 1)) (producer (tp-surface-test--producer signal)) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface)) (external 'old) rollback-ran) (should-error (tp-with-transaction (tp-transaction-participate-v2 :key '(test . failure) :stage (lambda () (setq external 'candidate) (error "Participant failure")) :rollback (lambda () (setq external 'old rollback-ran t))) (tp-signal-set signal 2))) (should rollback-ran) (should (eq external 'old)) (should (= (tp-signal-peek signal) 1)) (should (= (tp-surface-revision surface) revision)) (should (equal (buffer-string) "1"))))) (ert-deftest tp-surface-test-transaction-participant-key-is-unique () "Participant keys are unique within the outer transaction." (should-error (tp-with-transaction (tp-transaction-participate-v2 :key 'same :stage #'ignore :rollback #'ignore) (tp-transaction-participate-v2 :key 'same :stage #'ignore :rollback #'ignore)) :type 'tp-reactive-error)) (ert-deftest tp-surface-test-equal-reactive-plan-skips-surface-publication () "An equal producer result leaves the surface revision unchanged." (tp-surface-test--with-buffer (let* ((signal (tp-signal-create 10)) (producer (lambda (context) (tp-object-ensure context nil 'root 'text) (tp-surface-test--leaf 'root (number-to-string (/ (tp-signal-read signal) 10))))) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface))) (tp-signal-set signal 11) (should (= (tp-surface-revision surface) revision)) (should (equal (buffer-string) "1"))))) (ert-deftest tp-surface-test-output-equal-precommit-finalizes-and-rolls-back () "Output-equal candidates finalize lifecycle without visible publication." (tp-surface-test--with-buffer (let* ((source (tp-signal-create 10)) (auxiliary (tp-signal-create 1)) (include t) binding (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'text))) (when include (setq binding (tp-bind object '(test . optional) (lambda () (tp-signal-read auxiliary)))) (tp-binding-read binding))) (tp-surface-test--leaf 'root (number-to-string (/ (tp-signal-read source) 10))))) (surface (tp-surface-mount buffer producer '(:capability content))) (revision (tp-surface-revision surface)) (transaction-id tp--surface-transaction-id) (fail t) (tp--surface-precommit-step-function (lambda (step _state) (when (and fail (eq step 'lifecycle-finalized)) (error "After output-equal lifecycle"))))) (setq include nil) (should-error (tp-signal-set source 11)) (should (tp-binding-live-p binding)) (should (= (tp-signal-subscriber-count auxiliary) 1)) (should (= (tp-signal-peek source) 10)) (should (= (tp-signal-revision source) 0)) (should (= (tp-surface-revision surface) revision)) (should (= tp--surface-transaction-id transaction-id)) (should (equal (buffer-string) "1")) (setq fail nil) (tp-signal-set source 11) (should-not (tp-binding-live-p binding)) (should (= (tp-signal-subscriber-count auxiliary) 0)) (should (= (tp-signal-revision source) 1)) (should (= (tp-surface-revision surface) revision)) (should (= tp--surface-transaction-id transaction-id)) (should (equal (buffer-string) "1"))))) (ert-deftest tp-surface-test-keyed-reorder-preserves-object-handles () "Keyed children keep identity when their display order changes." (tp-surface-test--with-buffer (let* ((first (tp-surface-plan-create :key 'root :kind 'group :children (list (tp-surface-test--leaf 'a "A") (tp-surface-test--leaf 'b "B")) :capability 'content)) (surface (tp-surface-mount buffer first '(:capability content))) (a (tp-object-resolve surface '(root a))) (b (tp-object-resolve surface '(root b))) (second (tp-surface-plan-create :key 'root :kind 'group :children (list (tp-surface-test--leaf 'b "B") (tp-surface-test--leaf 'a "A")) :capability 'content))) (tp-surface-update surface second) (should (equal (buffer-string) "BA")) (should (eq a (tp-object-resolve surface '(root a)))) (should (eq b (tp-object-resolve surface '(root b)))) (should (= (plist-get (tp-surface-report surface) :moved-objects) 2))))) (ert-deftest tp-surface-test-overlapping-range-contributions-are-ordered () "Overlapping property mounts combine deterministically by plan order." (tp-surface-test--with-buffer (insert "abcd") (let* ((left (tp-range-anchor-create buffer 1 4)) (right (tp-range-anchor-create buffer 2 5)) (producer (lambda (context) (let* ((root (tp-object-ensure context nil 'root 'group)) (a (tp-object-ensure context root 'a 'range)) (b (tp-object-ensure context root 'b 'range))) (tp-object-attach-range context a left) (tp-object-attach-range context b right)) (tp-surface-plan-create :key 'root :kind 'group :capability 'properties :children (list (tp-surface-plan-create :key 'a :kind 'range :props '(help-echo "A") :capability 'properties) (tp-surface-plan-create :key 'b :kind 'range :props '(help-echo "B") :capability 'properties))))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (should (equal (get-text-property 1 'help-echo) "A")) (should (equal (get-text-property 2 'help-echo) "B")) (should (equal (get-text-property 4 'help-echo) "B")) (tp-surface-unmount surface) (should-not (get-text-property 2 'help-echo))))) (ert-deftest tp-surface-test-explicit-nil-is-a-property-contribution () "A present nil contribution hides and later restores its host baseline." (tp-surface-test--with-buffer (insert "host") (put-text-property 1 5 'help-echo "baseline") (let* ((anchor (tp-range-anchor-create buffer 1 5)) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo nil) :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (should (equal (tp--property-state-at buffer 2 'help-echo) '(t))) (tp-surface-unmount surface) (should (equal (get-text-property 2 'help-echo) "baseline"))))) (ert-deftest tp-surface-test-unmount-preserves-only-conflicting-subranges () "Unmount restores owned runs without clobbering a partial host override." (tp-surface-test--with-buffer (insert "abcd") (let* ((anchor (tp-range-anchor-create buffer 1 5)) (producer (lambda (context) (let ((object (tp-object-ensure context nil 'root 'range))) (tp-object-attach-range context object anchor)) (tp-surface-plan-create :key 'root :kind 'range :props '(help-echo "tp") :capability 'properties))) (surface (tp-surface-mount buffer producer '(:capability properties)))) (put-text-property 2 3 'help-echo "external") (tp-surface-unmount surface) (should-not (get-text-property 1 'help-echo)) (should (equal (get-text-property 2 'help-echo) "external")) (should-not (get-text-property 3 'help-echo))))) (ert-deftest tp-surface-test-cross-surface-overlap-is-rejected () "Independent surfaces cannot silently claim the same character range." (tp-surface-test--with-buffer (let ((first (tp-surface-mount buffer (tp-surface-test--leaf 'first "owned") '(:capability content)))) (should-error (tp-surface-mount buffer (tp-surface-test--leaf 'second "overlap") '(:capability content)) :type 'tp-capability-error) (should (tp-surface-live-p first)) (should (equal (buffer-string) "owned"))))) (ert-deftest tp-surface-test-kill-during-publication-is-authoritative () "A killed target stays dead while other surfaces and sources roll back." (let* ((signal (tp-signal-create 1)) (first-buffer (generate-new-buffer " *tp-surface-survivor*")) (victim-buffer (generate-new-buffer " *tp-surface-victim*")) (producer (tp-surface-test--producer signal)) first victim victim-object) (unwind-protect (progn (setq first (tp-surface-mount first-buffer producer '(:capability content)) victim (tp-surface-mount victim-buffer producer '(:capability content)) victim-object (tp-object-resolve victim '(root))) (let ((revision (tp-surface-revision first)) (tp--surface-publication-step-function (lambda (step surface) (when (and (eq step 'text) (eq surface victim)) (kill-buffer victim-buffer))))) (should-error (tp-signal-set signal 2)) (should (= (tp-signal-peek signal) 1)) (should (tp-surface-live-p first)) (should (= (tp-surface-revision first) revision)) (with-current-buffer first-buffer (should (equal (buffer-string) "1"))) (should-not (buffer-live-p victim-buffer)) (should-not (tp-surface-live-p victim)) (should-not (tp-object-live-p victim-object)))) (when (buffer-live-p first-buffer) (kill-buffer first-buffer)) (when (buffer-live-p victim-buffer) (kill-buffer victim-buffer))))) (provide 'tp-surface-tests) ;;; tp-surface-tests.el ends here