Implement sx-swap pure tree rewriting and fix handler test infrastructure

Write lib/sx-swap.sx — string-level SX scanner that finds elements by :id
and applies swap operations (innerHTML, outerHTML, beforeend, afterbegin,
beforebegin, afterend, delete, none). Includes OOB extraction via
find-oob-elements/strip-oob/apply-response for out-of-band targeted swaps.

Fix &rest varargs bug in test-handlers.sx helper mock — fn doesn't support
&rest, so change to positional (name a1 a2) with nil defaults. Fix into
branch, add run-handler sx-expr unwrapping.

Add missing primitives to run_tests.ml: scope-peek, callable?, make-sx-expr,
sx-expr-source, sx-expr?, spread?, call-lambda. These unblock aser-based
handler evaluation in tests.

Add algebraic integration tests (test-swap-integration.sx) demonstrating the
sx1 ⊕(mode,target) sx2 = sx3 pattern with real handler execution.

1219 → 1330 passing tests (+111).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 18:00:51 +00:00
parent f5f58ea47e
commit aa508bad77
5 changed files with 1445 additions and 2 deletions

131
spec/tests/test-sx-swap.sx Normal file
View File

@@ -0,0 +1,131 @@
(defsuite
"sx-swap:innerHTML"
(deftest
"replaces children of target"
(let
((result (sx-swap "(div :id \"t\" (p \"old\"))" "innerHTML" "t" "(p \"new\")")))
(assert-equal result "(div :id \"t\" (p \"new\"))")))
(deftest
"replaces multiple children"
(let
((result (sx-swap "(div :id \"t\" (p \"a\") (p \"b\"))" "innerHTML" "t" "(span \"x\")")))
(assert-equal result "(div :id \"t\" (span \"x\"))")))
(deftest
"handles nested target"
(let
((result (sx-swap "(main (div :id \"t\" (p \"old\")))" "innerHTML" "t" "(p \"new\")")))
(assert-equal result "(main (div :id \"t\" (p \"new\")))")))
(deftest
"preserves attrs"
(let
((result (sx-swap "(div :id \"t\" :class \"box\" (p \"old\"))" "innerHTML" "t" "(p \"new\")")))
(assert-equal result "(div :id \"t\" :class \"box\" (p \"new\"))"))))
(defsuite
"sx-swap:outerHTML"
(deftest
"replaces entire element"
(let
((result (sx-swap "(main (div :id \"t\" (p \"old\")) (footer \"f\"))" "outerHTML" "t" "(section \"new\")")))
(assert-equal result "(main (section \"new\") (footer \"f\"))")))
(deftest
"replaces at root"
(let
((result (sx-swap "(div :id \"t\" (p \"old\"))" "outerHTML" "t" "(span \"new\")")))
(assert-equal result "(span \"new\")"))))
(defsuite
"sx-swap:beforeend"
(deftest
"appends to children"
(let
((result (sx-swap "(ul :id \"t\" (li \"a\"))" "beforeend" "t" "(li \"b\")")))
(assert-equal result "(ul :id \"t\" (li \"a\") (li \"b\"))")))
(deftest
"appends to empty element"
(let
((result (sx-swap "(div :id \"t\")" "beforeend" "t" "(p \"new\")")))
(assert-equal result "(div :id \"t\" (p \"new\"))"))))
(defsuite
"sx-swap:afterbegin"
(deftest
"prepends to children"
(let
((result (sx-swap "(ul :id \"t\" (li \"b\"))" "afterbegin" "t" "(li \"a\")")))
(assert-equal result "(ul :id \"t\" (li \"a\") (li \"b\"))"))))
(defsuite
"sx-swap:beforebegin"
(deftest
"inserts before element"
(let
((result (sx-swap "(div (p :id \"t\" \"x\"))" "beforebegin" "t" "(hr)")))
(assert-equal result "(div (hr)(p :id \"t\" \"x\"))"))))
(defsuite
"sx-swap:afterend"
(deftest
"inserts after element"
(let
((result (sx-swap "(div (p :id \"t\" \"x\") (span \"y\"))" "afterend" "t" "(hr)")))
(assert-equal result "(div (p :id \"t\" \"x\")(hr) (span \"y\"))"))))
(defsuite
"sx-swap:delete"
(deftest
"removes element"
(let
((result (sx-swap "(div (p :id \"t\" \"bye\") (p \"stay\"))" "delete" "t" "")))
(assert-true (contains? result "stay"))
(assert-false (contains? result "bye")))))
(defsuite
"sx-swap:none"
(deftest
"returns unchanged"
(let
((page "(div :id \"t\" (p \"x\"))"))
(assert-equal (sx-swap page "none" "t" "(p \"y\")") page))))
(defsuite
"sx-swap:missing-target"
(deftest
"returns unchanged when id not found"
(let
((page "(div :id \"other\" (p \"x\"))"))
(assert-equal (sx-swap page "innerHTML" "missing" "(p \"y\")") page))))
(defsuite
"sx-swap:oob"
(deftest
"finds oob elements"
(let
((src "(<> (p \"main\") (div :id \"oob-t\" :sx-swap-oob \"innerHTML\" (p \"oob\")))"))
(let
((oobs (find-oob-elements src)))
(assert-equal (len oobs) 1)
(assert-equal (get (first oobs) "id") "oob-t")
(assert-equal (get (first oobs) "mode") "innerHTML"))))
(deftest
"strips oob from response"
(let
((src "(<> (p \"main\") (div :id \"oob-t\" :sx-swap-oob \"innerHTML\" (p \"oob\")))"))
(let
((oobs (find-oob-elements src)))
(let
((main (strip-oob src oobs)))
(assert-true (contains? main "main"))
(assert-false (contains? main "oob-t"))))))
(deftest
"full pipeline applies primary + oob"
(let
((page "(div (div :id \"a\" (p \"A old\")) (div :id \"b\" (p \"B old\")))")
(response
"(<> (p \"A new\") (div :id \"b\" :sx-swap-oob \"innerHTML\" (p \"B new\")))"))
(let
((result (apply-response page response "innerHTML" "a")))
(assert-true (contains? result "A new"))
(assert-true (contains? result "B new"))
(assert-false (contains? result "A old"))
(assert-false (contains? result "B old"))))))