lib/host/behavior.sx — the substrate-independent seam every runner/transport/registry/driver plugs into. An engine bundles four dict-of-functions adapters (trigger-registry, runner, transport, driver); behavior/process folds an ACTIVITY through the pipeline: emit → match triggers → run each behavior DAG → dispatch each effect-as-data → recurse on new activities (loop closure, depth-guarded at 8). Every stage injected, so the same DAG + engine run over the synchronous op-table runner / Erlang durable / celery-sx / fed-sx transport unchanged. Reference tests (mock adapters) prove the contract: publish→trigger→runner→effect flows; a non-matching activity fires nothing (log complete, execution precise); an effect that emits a new activity re-triggers (loop closes); an unbounded loop is depth-guarded (terminates). Wired into conformance.sh + serve.sh MODULES. behavior 4/4; full host conformance 575/575. Next: P0 supplies the REAL adapters (publish activity ← host/blog--publish-activity, local-SX trigger, sync op-table runner over a publish-DAG, host driver) — same engine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.9 KiB
Plaintext
71 lines
3.9 KiB
Plaintext
;; 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}))
|