#!/usr/bin/env bash # next/flow/conformance.sh — flow-on-erlang engine conformance. # # Exercises the native Erlang-on-SX durable workflow engine # (next/flow/{flow,flow_spec,flow_store}.erl): the combinator algebra, # the deterministic-replay suspend/resume core, and the durable store. # This is the gate for the engine, replacing lib/flow/conformance.sh # (the Scheme engine) for the fed-sx substrate — the kernel's trigger # fan-out drives flows in its own runtime, with no cross-guest FFI. set -uo pipefail cd "$(git rev-parse --show-toplevel)" SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" if [ ! -x "$SX_SERVER" ]; then SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" fi if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found." >&2 exit 1 fi VERBOSE="${1:-}" PASS=0; FAIL=0; ERRORS="" TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT # Common combinator shorthands built per-epoch (Erlang locals don't # survive across erlang-eval-ast calls; the gen_server state does). N1='flow_spec:flow_node(fun(X) -> X + 1 end)' N2='flow_spec:flow_node(fun(X) -> X * 2 end)' SUSP_FLOW='flow_spec:sequence([flow_spec:flow_node(fun(X) -> X + 1 end), flow:suspend(wait1), flow_spec:flow_node(fun(V) -> {resumed, V} end)])' TWO_SUSP='flow_spec:sequence([flow:suspend(a), flow_spec:flow_node(fun(V) -> V * 10 end), flow:suspend(b), flow_spec:flow_node(fun(V) -> V + 1 end)])' cat > "$TMPFILE" < X < 10 end, ${N1}, 100), 0) =:= {flow_done, 10}\") :name)") (epoch 24) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:flow_until(fun(X) -> X >= 5 end, ${N1}, 100), 0) =:= {flow_done, 5}\") :name)") (epoch 25) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:flow_while(fun(_) -> true end, ${N1}, 3), 0) =:= {flow_done, 3}\") :name)") ;; ── branching ────────────────────────────────────────────── (epoch 30) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:branch(fun(X) -> X > 0 end, flow_spec:flow_const(pos), flow_spec:flow_const(neg)), 5) =:= {flow_done, pos}\") :name)") (epoch 31) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:branch(fun(X) -> X > 0 end, flow_spec:flow_const(pos), flow_spec:flow_const(neg)), -5) =:= {flow_done, neg}\") :name)") ;; ── railway failure ──────────────────────────────────────── (epoch 40) (eval "(get (erlang-eval-ast \"flow_spec:failed(flow_spec:fail(x)) andalso (flow_spec:failed(42) =:= false)\") :name)") (epoch 41) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:attempt([flow_spec:flow_node(fun(_) -> flow_spec:fail(boom) end), flow_spec:flow_node(fun(_) -> 999 end)]), 0) =:= {flow_done, {flow_fail, boom}}\") :name)") (epoch 42) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:attempt([${N1}, ${N2}]), 3) =:= {flow_done, 8}\") :name)") (epoch 43) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:recover(flow_spec:flow_node(fun(_) -> flow_spec:fail(bad) end), fun(R) -> {ok, R} end), 0) =:= {flow_done, {ok, bad}}\") :name)") ;; ── effects / exceptions ─────────────────────────────────── (epoch 50) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:tap(fun(_) -> ok end), 7) =:= {flow_done, 7}\") :name)") (epoch 51) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:try_catch(flow_spec:flow_node(fun(_) -> throw(oops) end), fun(E) -> {caught, E} end), 0) =:= {flow_done, {caught, oops}}\") :name)") (epoch 52) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:retry(5, flow_spec:flow_node(fun(X) -> X + 1 end)), 1) =:= {flow_done, 2}\") :name)") ;; ── suspend / replay (deterministic-replay core) ─────────── (epoch 60) (eval "(get (erlang-eval-ast \"flow:run(${SUSP_FLOW}, 0) =:= {flow_suspended, wait1}\") :name)") (epoch 61) (eval "(get (erlang-eval-ast \"flow:drive(${SUSP_FLOW}, 0, [{wait1, 99}]) =:= {flow_done, {resumed, 99}}\") :name)") (epoch 62) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:sequence([flow:suspend(a), flow:suspend(b)]), 0) =:= {flow_suspended, a}\") :name)") ;; wait/1 — timer-style suspend that PRESERVES the value on resume (epoch 63) (eval "(get (erlang-eval-ast \"flow:run(flow_spec:sequence([flow:wait(t), flow_spec:flow_node(fun(X) -> X + 1 end)]), 5) =:= {flow_suspended, t}\") :name)") (epoch 64) (eval "(get (erlang-eval-ast \"flow:drive(flow_spec:sequence([flow:wait(t), flow_spec:flow_node(fun(X) -> X + 1 end)]), 5, [{t, ignored}]) =:= {flow_done, 6}\") :name)") ;; ── durable store: registry ──────────────────────────────── (epoch 70) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(f1, ${N1}), flow_store:resolve_flow(f1) =/= not_found andalso flow_store:registered_flows() =:= [f1]\") :name)") (epoch 71) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:resolve_flow(ghost) =:= not_found\") :name)") ;; ── durable store: start / resume ────────────────────────── ;; one-shot flow runs to completion on start (epoch 80) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(done1, ${N1}), flow_store:start(done1, 41) =:= {ok, 1, {flow_done, 42}}\") :name)") ;; suspending flow: start suspends, resume completes (epoch 81) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(s1, ${SUSP_FLOW}), {ok, Id, R} = flow_store:start(s1, 10), R =:= {flow_suspended, wait1} andalso flow_store:status(Id) =:= {ok, {suspended, wait1}}\") :name)") (epoch 82) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(s1, ${SUSP_FLOW}), {ok, Id, _} = flow_store:start(s1, 10), flow_store:resume(Id, 99) =:= {ok, {flow_done, {resumed, 99}}}\") :name)") (epoch 83) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(s1, ${SUSP_FLOW}), {ok, Id, _} = flow_store:start(s1, 10), flow_store:resume(Id, 99), flow_store:status(Id) =:= {ok, {done, {resumed, 99}}}\") :name)") ;; two-suspend flow: resume chain accumulates the replay log (epoch 84) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(s2, ${TWO_SUSP}), {ok, Id, _} = flow_store:start(s2, 0), {ok, R1} = flow_store:resume(Id, 5), R2 = flow_store:resume(Id, 7), R1 =:= {flow_suspended, b} andalso R2 =:= {ok, {flow_done, 8}}\") :name)") ;; error paths (epoch 85) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:start(ghost, 0) =:= {error, no_such_flow}\") :name)") (epoch 86) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:resume(999, x) =:= {error, no_such_instance}\") :name)") (epoch 87) (eval "(get (erlang-eval-ast \"flow_store:start_link(), flow_store:register_flow(done1, ${N1}), {ok, Id, _} = flow_store:start(done1, 0), flow_store:resume(Id, x) =:= {error, already_done}\") :name)") EPOCHS OUTPUT=$(timeout 360 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) check() { local epoch="$1" desc="$2" expected="$3" local actual actual=$(echo "$OUTPUT" | awk -v e="$epoch" ' $0 ~ "^\\(ok-len " e " " { getline; print; exit } $0 ~ "^\\(ok " e " " { print; exit } $0 ~ "^\\(error " e " " { print; exit } ') [ -z "$actual" ] && actual="" if echo "$actual" | grep -qF -- "$expected"; then PASS=$((PASS+1)) [ "$VERBOSE" = "-v" ] && echo " ok $desc" else FAIL=$((FAIL+1)) ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual " fi } check 3 "flow module loaded" "flow" check 4 "flow_spec module loaded" "flow_spec" check 5 "flow_store module loaded" "flow_store" check 10 "flow_id" "true" check 11 "flow_const" "true" check 12 "flow_node" "true" check 20 "sequence threads left-to-right" "true" check 21 "parallel fans out" "true" check 22 "map_flow over a list" "true" check 23 "flow_while bounded by pred" "true" check 24 "flow_until bounded by pred" "true" check 25 "flow_while bounded by max" "true" check 30 "branch then-arm" "true" check 31 "branch else-arm" "true" check 40 "failed? predicate" "true" check 41 "attempt stops at first fail" "true" check 42 "attempt threads on success" "true" check 43 "recover handles fail value" "true" check 50 "tap pass-through" "true" check 51 "try_catch catches a raise" "true" check 52 "retry runs node" "true" check 60 "suspend miss short-circuits" "true" check 61 "suspend replay completes" "true" check 62 "first of two suspends wins" "true" check 63 "wait short-circuits on miss" "true" check 64 "wait preserves value on resume" "true" check 70 "register + resolve + list" "true" check 71 "resolve unknown -> not_found" "true" check 80 "start one-shot -> done" "true" check 81 "start suspends + status" "true" check 82 "resume completes" "true" check 83 "status after resume = done" "true" check 84 "two-suspend resume chain" "true" check 85 "start unknown -> no_such_flow" "true" check 86 "resume unknown -> no_such_instance" "true" check 87 "resume a done flow -> already_done" "true" TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL flow-on-erlang engine tests passed" else echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" echo "$ERRORS" fi [ $FAIL -eq 0 ]