;; lib/host/tests/behavior.sx — the adapter seam (lib/host/behavior.sx). MOCK adapters prove the ;; pipeline contract (emit → match → run → dispatch → loop-closure) independent of any substrate. (define host-be-pass 0) (define host-be-fail 0) (define host-be-fails (list)) (define host-be-test (fn (name actual expected) (if (= actual expected) (set! host-be-pass (+ host-be-pass 1)) (begin (set! host-be-fail (+ host-be-fail 1)) (append! host-be-fails {:name name :actual actual :expected expected}))))) ;; ── mock adapters: a publish activity fires a runner that emits one notify effect ── (define be-transport {:emit (fn (a) nil) :deliver (fn () (list))}) (define be-triggers {:register! (fn (spec dag hint) nil) :match (fn (a) (if (= (get a :verb) "publish") (list {:dag "publish-dag"}) (list)))}) (define be-runner {:run (fn (dag env) (if (= dag "publish-dag") {:status "done" :effects (list {:kind "notify" :to (get (get env :activity) :actor)})} {:status "done" :effects (list)}))}) (define be-driver {:dispatch (fn (eff) (list))}) ;; no follow-up activities (define be-engine (behavior/make-engine {:triggers be-triggers :runner be-runner :transport be-transport :driver be-driver})) (host-be-test "publish activity → trigger → runner → effect flows through the seam" (let ((tr (behavior/process be-engine {:verb "publish" :actor "alice" :object "cid1"}))) (list (len (get tr :emitted)) (len (get tr :ran)) (len (get tr :effects)) (get (first (get tr :effects)) :kind) (get (first (get tr :effects)) :to))) (list 1 1 1 "notify" "alice")) (host-be-test "a non-matching activity fires nothing (log complete, execution precise)" (let ((tr (behavior/process be-engine {:verb "draft" :actor "bob" :object "cid2"}))) (list (len (get tr :emitted)) (len (get tr :ran)) (len (get tr :effects)))) (list 1 0 0)) ;; ── loop closure: an effect's driver emits a NEW activity that re-triggers a second DAG ── (define bl-triggers {:register! (fn (spec dag hint) nil) :match (fn (a) (cond ((= (get a :verb) "publish") (list {:dag "pub"})) ((= (get a :verb) "followup") (list {:dag "fu"})) (else (list))))}) (define bl-runner {:run (fn (dag env) (cond ((= dag "pub") {:effects (list {:kind "chain"})}) ((= dag "fu") {:effects (list {:kind "done"})}) (else {:effects (list)})))}) (define bl-driver {:dispatch (fn (eff) (if (= (get eff :kind) "chain") (list {:verb "followup" :actor "a" :object "c2"}) (list)))}) (define bl-engine (behavior/make-engine {:triggers bl-triggers :runner bl-runner :transport be-transport :driver bl-driver})) (host-be-test "loop closure — an effect emits a new activity that re-triggers, bounded" (let ((tr (behavior/process bl-engine {:verb "publish" :actor "a" :object "c1"}))) (list (len (get tr :emitted)) (len (get tr :ran)) (map (fn (e) (get e :kind)) (get tr :effects)))) (list 2 2 (list "chain" "done"))) ;; ── an unbounded loop is depth-guarded (terminates instead of running forever) ── (define bi-triggers {:register! (fn (s d h) nil) :match (fn (a) (if (= (get a :verb) "loop") (list {:dag "l"}) (list)))}) (define bi-runner {:run (fn (dag env) {:effects (list {:kind "again"})})}) (define bi-driver {:dispatch (fn (eff) (list {:verb "loop" :actor "a" :object "c"}))}) (define bi-engine (behavior/make-engine {:triggers bi-triggers :runner bi-runner :transport be-transport :driver bi-driver})) (host-be-test "an unbounded loop is depth-guarded (terminates)" (let ((tr (behavior/process bi-engine {:verb "loop" :actor "a" :object "c"}))) (and (> (len (get tr :emitted)) 1) (<= (len (get tr :emitted)) 10))) true) (define host-be-tests-run! (fn () {:total (+ host-be-pass host-be-fail) :passed host-be-pass :failed host-be-fail :fails host-be-fails}))