Files
rose-ash/shared/sx/ref/test-cek-reactive.sx
giles d0a5ce1070 Remove invoke from platform interfaces, add cek-call integration tests
- platform_js.py: remove invoke function definition and PRIMITIVES
  registration, switch domListen handler wrapping to cek-call
- platform_py.py: remove invoke function definition
- run_signal_tests.py: remove invoke patch, use cek_call in batch wrapper
- run_cek_reactive_tests.py: remove invoke, fix primitive lookup to use
  two-level is_primitive/get_primitive, increase recursion limit for
  interpreted CEK evaluation
- test-cek-reactive.sx: add 7 new tests covering cek-call dispatch with
  computed, effect, cleanup, batch coalescing

All 79 tests pass (20 signal + 43 CEK + 16 CEK reactive).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 10:29:32 +00:00

212 lines
7.7 KiB
Plaintext

;; ==========================================================================
;; test-cek-reactive.sx — Tests for deref-as-shift reactive rendering
;;
;; Tests that (deref signal) inside a reactive-reset boundary performs
;; continuation capture: the rest of the expression becomes the subscriber.
;;
;; Requires: test-framework.sx, frames.sx, cek.sx, signals.sx loaded first.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Basic deref behavior through CEK
;; --------------------------------------------------------------------------
(defsuite "deref pass-through"
(deftest "deref non-signal passes through"
(let ((result (eval-expr-cek
(sx-parse-one "(deref 42)")
(test-env))))
(assert-equal 42 result)))
(deftest "deref nil passes through"
(let ((result (eval-expr-cek
(sx-parse-one "(deref nil)")
(test-env))))
(assert-nil result)))
(deftest "deref string passes through"
(let ((result (eval-expr-cek
(sx-parse-one "(deref \"hello\")")
(test-env))))
(assert-equal "hello" result))))
;; --------------------------------------------------------------------------
;; Deref signal without reactive-reset (no shift)
;; --------------------------------------------------------------------------
(defsuite "deref signal without reactive-reset"
(deftest "deref signal returns current value"
(let ((s (signal 99)))
(env-set! (test-env) "test-sig" s)
(let ((result (eval-expr-cek
(sx-parse-one "(deref test-sig)")
(test-env))))
(assert-equal 99 result))))
(deftest "deref signal in expression returns computed value"
(let ((s (signal 10)))
(env-set! (test-env) "test-sig" s)
(let ((result (eval-expr-cek
(sx-parse-one "(+ 5 (deref test-sig))")
(test-env))))
(assert-equal 15 result)))))
;; --------------------------------------------------------------------------
;; Reactive reset + deref: continuation capture
;; --------------------------------------------------------------------------
(defsuite "reactive-reset shift"
(deftest "deref signal with reactive-reset captures continuation"
(let ((s (signal 42))
(captured-val nil))
;; Run CEK with a ReactiveResetFrame
(let ((result (cek-run
(make-cek-state
(sx-parse-one "(deref test-sig)")
(let ((e (env-extend (test-env))))
(env-set! e "test-sig" s)
e)
(list (make-reactive-reset-frame
(test-env)
(fn (v) (set! captured-val v))
true))))))
;; Initial render: returns current value, update-fn NOT called (first-render)
(assert-equal 42 result)
(assert-nil captured-val))))
(deftest "signal change invokes subscriber with update-fn"
(let ((s (signal 10))
(update-calls (list)))
;; Set up reactive-reset with tracking update-fn
(scope-push! "sx-island-scope" nil)
(let ((e (env-extend (test-env))))
(env-set! e "test-sig" s)
(cek-run
(make-cek-state
(sx-parse-one "(deref test-sig)")
e
(list (make-reactive-reset-frame
e
(fn (v) (append! update-calls v))
true)))))
;; Change signal — subscriber should fire
(reset! s 20)
(assert-equal 1 (len update-calls))
(assert-equal 20 (first update-calls))
;; Change again
(reset! s 30)
(assert-equal 2 (len update-calls))
(assert-equal 30 (nth update-calls 1))
(scope-pop! "sx-island-scope")))
(deftest "expression with deref captures rest as continuation"
(let ((s (signal 5))
(update-calls (list)))
(scope-push! "sx-island-scope" nil)
(let ((e (env-extend (test-env))))
(env-set! e "test-sig" s)
;; (str "val=" (deref test-sig)) — continuation captures (str "val=" [HOLE])
(let ((result (cek-run
(make-cek-state
(sx-parse-one "(str \"val=\" (deref test-sig))")
e
(list (make-reactive-reset-frame
e
(fn (v) (append! update-calls v))
true))))))
(assert-equal "val=5" result)))
;; Change signal — should get updated string
(reset! s 42)
(assert-equal 1 (len update-calls))
(assert-equal "val=42" (first update-calls))
(scope-pop! "sx-island-scope"))))
;; --------------------------------------------------------------------------
;; Disposal and cleanup
;; --------------------------------------------------------------------------
(defsuite "disposal"
(deftest "scope cleanup unsubscribes continuation"
(let ((s (signal 1))
(update-calls (list))
(disposers (list)))
;; Create island scope with collector that accumulates disposers
(scope-push! "sx-island-scope" (fn (d) (append! disposers d)))
(let ((e (env-extend (test-env))))
(env-set! e "test-sig" s)
(cek-run
(make-cek-state
(sx-parse-one "(deref test-sig)")
e
(list (make-reactive-reset-frame
e
(fn (v) (append! update-calls v))
true)))))
;; Pop scope — call all disposers
(scope-pop! "sx-island-scope")
(for-each (fn (d) (cek-call d nil)) disposers)
;; Change signal — no update should fire
(reset! s 999)
(assert-equal 0 (len update-calls)))))
;; --------------------------------------------------------------------------
;; cek-call integration — computed/effect use cek-call dispatch
;; --------------------------------------------------------------------------
(defsuite "cek-call dispatch"
(deftest "cek-call invokes native function"
(let ((log (list)))
(cek-call (fn (x) (append! log x)) (list 42))
(assert-equal (list 42) log)))
(deftest "cek-call invokes zero-arg lambda"
(let ((result (cek-call (fn () (+ 1 2)) nil)))
(assert-equal 3 result)))
(deftest "cek-call with nil function returns nil"
(assert-nil (cek-call nil nil)))
(deftest "computed tracks deps via cek-call"
(let ((s (signal 10)))
(let ((c (computed (fn () (* 2 (deref s))))))
(assert-equal 20 (deref c))
(reset! s 5)
(assert-equal 10 (deref c)))))
(deftest "effect runs and re-runs via cek-call"
(let ((s (signal "a"))
(log (list)))
(effect (fn () (append! log (deref s))))
(assert-equal (list "a") log)
(reset! s "b")
(assert-equal (list "a" "b") log)))
(deftest "effect cleanup runs on re-trigger"
(let ((s (signal 0))
(log (list)))
(effect (fn ()
(let ((val (deref s)))
(append! log (str "run:" val))
;; Return cleanup function
(fn () (append! log (str "clean:" val))))))
(assert-equal (list "run:0") log)
(reset! s 1)
(assert-equal (list "run:0" "clean:0" "run:1") log)))
(deftest "batch coalesces via cek-call"
(let ((s (signal 0))
(count (signal 0)))
(effect (fn () (do (deref s) (swap! count inc))))
(assert-equal 1 (deref count))
(batch (fn ()
(reset! s 1)
(reset! s 2)
(reset! s 3)))
;; batch should coalesce — effect runs once, not three times
(assert-equal 2 (deref count)))))