host RA: the Erlang durable runner adapter — built + tested (module + integration)
lib/host/ra.sx — a PURE-SX seam runner (advertises {effect,branch,each,suspend}) with an INJECTED
erl-eval (real = er-to-sx-deep ∘ erlang-eval-ast; mock in unit tests), so it loads in the plain host
(Erlang refs resolve lazily inside lambdas) and is unit-testable without the Erlang runtime.
host/ra--{atom,bin,erl-src,start-expr,resume-expr,parse,make-runner,resume,real-eval}: marshals our
canonical activity → Erlang source (CID as <<"…">> binary, atoms single-quoted), starts a named
next/ flow via flow_store, parses (ok Id (flow_done V))→{:status done :effects V :flow-id} /
(ok Id (flow_suspended T))→{:status suspended :resume {:id :tag}}.
DUAL-RUNNER ROUTING (flows.sx): host/flow--required-caps now handles a {:erl-flow :needs} DAG
(declared caps, since a foreign flow can't be introspected); host/flow--select-runner picks the
cheapest runner whose capabilities cover the DAG's needs. The capability model is now REAL with two
runners — an {effect,branch} composition lands on exec-runner; a {suspend} DAG routes to RA.
Verified: ra 9/9 (mock erl-eval) + plans/ra-integration.sh 4/4 (the REAL module driving live
flow_store: urgent→done, newsletter→suspended with a resume handle, digest_sent effect-as-data).
Full host conformance 607/607; next/tests/triggers_e2e.sh 10/10 baseline intact.
FINDING → RA-LIVE deferred: gen_servers don't persist across separate erlang-eval-ast calls (flow
README), so true cross-call suspend/resume needs a PERSISTENT next/ kernel process. The runner +
marshalling + suspend/resume mechanics are proven; RA-live is process lifecycle + wiring, documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
62
lib/host/tests/ra.sx
Normal file
62
lib/host/tests/ra.sx
Normal file
@@ -0,0 +1,62 @@
|
||||
;; lib/host/tests/ra.sx — the RA (Erlang durable) runner adapter (lib/host/ra.sx), unit-tested with
|
||||
;; a MOCK erl-eval (no Erlang runtime needed). The REAL dispatch is proven in plans/ra-spike.sh.
|
||||
|
||||
(define host-ra-pass 0)
|
||||
(define host-ra-fail 0)
|
||||
(define host-ra-fails (list))
|
||||
(define host-ra-test
|
||||
(fn (name actual expected)
|
||||
(if (= actual expected)
|
||||
(set! host-ra-pass (+ host-ra-pass 1))
|
||||
(begin (set! host-ra-fail (+ host-ra-fail 1))
|
||||
(append! host-ra-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define ra-urgent {:verb "create" :actor "site" :id "u1" :object-type "article" :category "urgent"})
|
||||
(define ra-news {:verb "create" :actor "site" :id "n1" :object-type "article" :category "newsletter"})
|
||||
|
||||
;; ── marshalling: our canonical activity → Erlang source ──
|
||||
(host-ra-test "erl-src marshals the activity → Erlang activity-proplist source"
|
||||
(host/ra--erl-src ra-urgent)
|
||||
"[{type, 'create'}, {actor, 'site'}, {id, <<\"u1\">>}, {object, [{type, 'article'}, {category, 'urgent'}]}]")
|
||||
(host-ra-test "start-expr wraps it in flow_store:start with the env"
|
||||
(host/ra--start-expr "bd" ra-urgent)
|
||||
"flow_store:start(bd, [{activity, [{type, 'create'}, {actor, 'site'}, {id, <<\"u1\">>}, {object, [{type, 'article'}, {category, 'urgent'}]}]}, {actor, 'site'}])")
|
||||
|
||||
;; ── result parsing: flow_store's er-to-sx-deep'd result → the seam runner contract ──
|
||||
(host-ra-test "parse DONE → {:status done :effects … :flow-id}"
|
||||
(let ((p (host/ra--parse (list "ok" 1 (list "flow_done" (list "digest_sent"))))))
|
||||
(list (get p :status) (get p :flow-id)))
|
||||
(list "done" 1))
|
||||
(host-ra-test "parse SUSPENDED → {:status suspended :resume {:id :tag}}"
|
||||
(let ((p (host/ra--parse (list "ok" 1 (list "flow_suspended" "morning")))))
|
||||
(list (get p :status) (get (get p :resume) :id) (get (get p :resume) :tag)))
|
||||
(list "suspended" 1 "morning"))
|
||||
(host-ra-test "parse garbage → failed"
|
||||
(get (host/ra--parse (list "error" "boom")) :status) "failed")
|
||||
|
||||
;; ── the runner (MOCK erl-eval keyed on the marshalled src) ──
|
||||
(define ra-mock
|
||||
(fn (src) (if (>= (index-of src "newsletter") 0)
|
||||
(list "ok" 7 (list "flow_suspended" "morning"))
|
||||
(list "ok" 3 (list "flow_done" (list "digest_sent"))))))
|
||||
(define ra-runner (host/ra--make-runner ra-mock))
|
||||
(host-ra-test "RA runner advertises {effect, branch, each, suspend}"
|
||||
(get ra-runner :capabilities) (list "effect" "branch" "each" "suspend"))
|
||||
(host-ra-test "RA runner: urgent → done; newsletter → suspended (via the injected erl-eval)"
|
||||
(list (get ((get ra-runner :run) {:erl-flow "bd"} {:activity ra-urgent}) :status)
|
||||
(get ((get ra-runner :run) {:erl-flow "bd"} {:activity ra-news}) :status))
|
||||
(list "done" "suspended"))
|
||||
(host-ra-test "RA resume drives a suspended instance (async re-entry)"
|
||||
(get (host/ra--resume ra-mock 7 "morning_ts") :status) "done")
|
||||
|
||||
;; ── dual-runner routing: the capability model, now REAL (2 runners) ──
|
||||
(host-ra-test "select-runner: {effect,branch} composition → exec-runner; {suspend} dag → RA"
|
||||
(let ((fleet (list host/flow--exec-runner ra-runner)))
|
||||
(list (get (host/flow--select-runner fleet (quote (alt (when (eq "k" "v") (effect a)) (else (effect b))))) :capabilities)
|
||||
(get (host/flow--select-runner fleet {:erl-flow "bd" :needs (list "effect" "branch" "suspend")}) :capabilities)))
|
||||
(list (list "effect" "branch" "each") (list "effect" "branch" "each" "suspend")))
|
||||
|
||||
(define host-ra-tests-run!
|
||||
(fn ()
|
||||
{:total (+ host-ra-pass host-ra-fail)
|
||||
:passed host-ra-pass :failed host-ra-fail :fails host-ra-fails}))
|
||||
Reference in New Issue
Block a user