Add CEK reactive tests (9/9), fix test runners for CEK-default mode
test-cek-reactive.sx: 9 tests across 4 suites — deref pass-through, signal without reactive-reset, reactive-reset shift with continuation capture, scope disposal cleanup. run_cek_reactive_tests.py: new runner loading signals+frames+cek. Both test runners override sx_ref.eval_expr back to tree-walk so interpreted .sx uses tree-walk internally. Plan page added to sx-docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
154
shared/sx/ref/test-cek-reactive.sx
Normal file
154
shared/sx/ref/test-cek-reactive.sx
Normal file
@@ -0,0 +1,154 @@
|
||||
;; ==========================================================================
|
||||
;; 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) (invoke d)) disposers)
|
||||
;; Change signal — no update should fire
|
||||
(reset! s 999)
|
||||
(assert-equal 0 (len update-calls)))))
|
||||
Reference in New Issue
Block a user