Files
rose-ash/lib/content/tests/crdt-tree.sx
giles f1b0914797
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s
content: tree-CRDT orphan reparenting (no content loss on concurrent delete-section) + 4 tests (742/742)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:21:39 +00:00

290 lines
6.2 KiB
Plaintext

;; Extension — nested-tree CvRDT. Sections nest and merge collaboratively;
;; convergence is order/replica/duplicate-insensitive like the flat layer.
(st-bootstrap-classes!)
(content-bootstrap-blocks!)
(content-bootstrap-doc!)
(content-bootstrap-render!)
(content-bootstrap-section!)
(define same? (fn (a b) (= (get a :elements) (get b :elements))))
;; base: a section "s" at root, with one child heading.
(define
base
(crdt-tree-insert
(crdt-tree-insert
(crdt-empty)
"s"
"section"
(crdt-pos 1 0)
""
(list)
1
0)
"h"
"heading"
(crdt-pos 1 0)
"s"
(list (list "level" 2) (list "text" "Sub"))
1
0))
;; ── materialise rebuilds the tree ──
(content-test "tree order root" (crdt-tree-order base) (list "s"))
(content-test
"tree materialize ids"
(doc-tree-ids (crdt-tree-materialize "d" base))
(list "s" "h"))
(content-test
"tree render"
(asHTML (crdt-tree-materialize "d" base))
"<section><h2>Sub</h2></section>")
;; ── concurrent inserts into the SAME section converge + order by pos ──
(define
rA
(crdt-tree-insert
base
"a"
"text"
(crdt-pos 5 1)
"s"
(list (list "text" "A"))
2
1))
(define
rB
(crdt-tree-insert
base
"b"
"text"
(crdt-pos 5 2)
"s"
(list (list "text" "B"))
2
2))
(content-test
"same-parent merge commutes"
(same? (crdt-tree-merge rA rB) (crdt-tree-merge rB rA))
true)
(content-test
"same-parent order deterministic"
(doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-merge rA rB)))
(list "s" "h" "a" "b"))
;; ── concurrent inserts into DIFFERENT parents converge ──
(define
base2
(crdt-tree-insert
(crdt-tree-insert
(crdt-empty)
"s1"
"section"
(crdt-pos 1 0)
""
(list)
1
0)
"s2"
"section"
(crdt-pos 2 0)
""
(list)
1
0))
(define
x
(crdt-tree-insert
base2
"x"
"text"
(crdt-pos 1 0)
"s1"
(list (list "text" "X"))
2
1))
(define
y
(crdt-tree-insert
base2
"y"
"text"
(crdt-pos 1 0)
"s2"
(list (list "text" "Y"))
2
2))
(define m (crdt-tree-merge x y))
(content-test
"different-parent commutes"
(same? m (crdt-tree-merge y x))
true)
(content-test
"different-parent tree"
(doc-tree-ids (crdt-tree-materialize "d" m))
(list "s1" "x" "s2" "y"))
(content-test
"different-parent render"
(asHTML (crdt-tree-materialize "d" m))
"<section><p>X</p></section><section><p>Y</p></section>")
;; ── nested sections (section inside section) ──
(define
nested
(crdt-tree-apply-all
(crdt-empty)
(list
(crdt-tree-op-insert
"outer"
"section"
(crdt-pos 1 0)
""
(list)
1
0)
(crdt-tree-op-insert
"inner"
"section"
(crdt-pos 1 0)
"outer"
(list)
1
0)
(crdt-tree-op-insert
"leaf"
"text"
(crdt-pos 1 0)
"inner"
(list (list "text" "deep"))
1
0))))
(content-test
"nested tree ids"
(doc-tree-ids (crdt-tree-materialize "d" nested))
(list "outer" "inner" "leaf"))
(content-test
"nested render"
(asHTML (crdt-tree-materialize "d" nested))
"<section><section><p>deep</p></section></section>")
;; ── ops in any order converge (commutative) ──
(define
opA
(crdt-tree-op-insert
"p"
"text"
(crdt-pos 6 0)
"s"
(list (list "text" "P"))
3
1))
(define opB (crdt-tree-op-update "h" "text" "Edited" 5 1))
(define opC (crdt-tree-op-delete "h"))
(content-test
"ops commute"
(same?
(crdt-tree-apply-all base (list opA opB opC))
(crdt-tree-apply-all base (list opC opB opA)))
true)
(content-test
"ops idempotent"
(same?
(crdt-tree-apply-all base (list opA opB))
(crdt-tree-apply-all
(crdt-tree-apply-all base (list opA opB))
(list opA opB)))
true)
;; ── update into a section + LWW ──
(define u1 (crdt-tree-update base "h" "text" "v5" 5 1))
(define u2 (crdt-tree-update base "h" "text" "v7" 7 2))
(content-test
"tree LWW higher ts"
(str
(blk-send
(doc-deep-find (crdt-tree-materialize "d" (crdt-tree-merge u1 u2)) "h")
"text"))
"v7")
;; ── delete inside a section ──
(content-test
"delete in section"
(doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-delete base "h")))
(list "s"))
;; ── merge idempotence ──
(content-test "merge idempotent self" (same? (crdt-tree-merge m m) m) true)
;; ── full convergence: two replicas, divergent edits in different sections ──
(define
repl1
(crdt-tree-apply-all
base2
(list
(crdt-tree-op-insert
"p1"
"text"
(crdt-pos 1 0)
"s1"
(list (list "text" "from1"))
5
1))))
(define
repl2
(crdt-tree-apply-all
base2
(list
(crdt-tree-op-insert
"p2"
"text"
(crdt-pos 1 0)
"s2"
(list (list "text" "from2"))
6
2))))
(content-test
"two-replica tree converges"
(same? (crdt-tree-merge repl1 repl2) (crdt-tree-merge repl2 repl1))
true)
(content-test
"two-replica tree ids"
(doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-merge repl1 repl2)))
(list "s1" "p1" "s2" "p2"))
;; ── orphan reparenting: concurrent delete-section + insert-child ──
;; A deletes section s; B inserts a child into s. After merge, s is gone but the
;; child must survive (reparented to root), not silently vanish.
(define delA (crdt-tree-delete base "s"))
(define
insB
(crdt-tree-insert
base
"c"
"text"
(crdt-pos 9 0)
"s"
(list (list "text" "kept"))
5
2))
(define orphan-merge (crdt-tree-merge delA insB))
(content-test
"orphan survives delete-section"
(doc-tree-ids (crdt-tree-materialize "d" orphan-merge))
(list "h" "c"))
(content-test
"orphan reparent commutes"
(same? orphan-merge (crdt-tree-merge insB delA))
true)
(content-test
"orphan content preserved"
(str
(blk-send
(doc-deep-find (crdt-tree-materialize "d" orphan-merge) "c")
"text"))
"kept")
(content-test
"orphan render at root"
(asHTML (crdt-tree-materialize "d" orphan-merge))
"<h2>Sub</h2><p>kept</p>")