fed-sx-m1: Step 6b-env — pipeline:stage_envelope wired against envelope:validate_shape + 12 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 26s

This commit is contained in:
2026-05-28 03:03:55 +00:00
parent 9cb002c856
commit 460257f2bb
4 changed files with 149 additions and 14 deletions

View File

@@ -1,7 +1,8 @@
-module(pipeline). -module(pipeline).
-export([run_stages/2, -export([run_stages/2,
validate_inbound/1, validate_outbound/1, validate_inbound/1, validate_outbound/1,
inbound_stages/0, outbound_stages/0]). inbound_stages/0, outbound_stages/0,
stage_envelope/1]).
%% Validation pipeline per design §14. %% Validation pipeline per design §14.
%% %%
@@ -33,5 +34,16 @@ validate_inbound(Activity) ->
validate_outbound(Activity) -> validate_outbound(Activity) ->
run_stages(Activity, outbound_stages()). run_stages(Activity, outbound_stages()).
inbound_stages() -> []. inbound_stages() ->
outbound_stages() -> []. [fun (A) -> stage_envelope(A) end].
outbound_stages() ->
[fun (A) -> stage_envelope(A) end].
%% ── Concrete stages ─────────────────────────────────────────────
%% stage_envelope/1 — wrap envelope:validate_shape/1. The pipeline
%% driver expects ok | {error, R}; validate_shape returns exactly
%% that, so delegation is direct.
stage_envelope(Activity) ->
envelope:validate_shape(Activity).

View File

@@ -55,17 +55,19 @@ cat > "$TMPFILE" <<'EPOCHS'
(epoch 14) (epoch 14)
(eval "(get (erlang-eval-ast \"pipeline:run_stages(my_act, [fun (A) -> case A of my_act -> ok; _ -> {error, wrong_arg} end end]) =:= ok\") :name)") (eval "(get (erlang-eval-ast \"pipeline:run_stages(my_act, [fun (A) -> case A of my_act -> ok; _ -> {error, wrong_arg} end end]) =:= ok\") :name)")
;; Empty inbound_stages / outbound_stages lists ;; inbound_stages / outbound_stages are lists (concrete stages
;; tested in pipeline_envelope.sh; we just confirm they're lists).
(epoch 15) (epoch 15)
(eval "(get (erlang-eval-ast \"pipeline:inbound_stages() =:= []\") :name)") (eval "(get (erlang-eval-ast \"is_list(pipeline:inbound_stages())\") :name)")
(epoch 16) (epoch 16)
(eval "(get (erlang-eval-ast \"pipeline:outbound_stages() =:= []\") :name)") (eval "(get (erlang-eval-ast \"is_list(pipeline:outbound_stages())\") :name)")
;; Wrappers delegate to run_stages with the right list (empty => ok) ;; Driver-only invariants: explicit empty list with the wrappers
;; semantics is exercised via run_stages directly.
(epoch 17) (epoch 17)
(eval "(get (erlang-eval-ast \"pipeline:validate_inbound(anything) =:= ok\") :name)") (eval "(get (erlang-eval-ast \"pipeline:run_stages(anything, []) =:= ok\") :name)")
(epoch 18) (epoch 18)
(eval "(get (erlang-eval-ast \"pipeline:validate_outbound(anything) =:= ok\") :name)") (eval "(get (erlang-eval-ast \"pipeline:run_stages(my_act, [fun (_) -> ok end]) =:= ok\") :name)")
EPOCHS EPOCHS
OUTPUT=$(timeout 120 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) OUTPUT=$(timeout 120 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
@@ -95,10 +97,10 @@ check 11 "all-ok stages -> ok" "true"
check 12 "first failure halts pipeline" "true" check 12 "first failure halts pipeline" "true"
check 13 "single failing stage" "true" check 13 "single failing stage" "true"
check 14 "stage receives activity verbatim" "true" check 14 "stage receives activity verbatim" "true"
check 15 "inbound_stages = []" "true" check 15 "inbound_stages is a list" "true"
check 16 "outbound_stages = []" "true" check 16 "outbound_stages is a list" "true"
check 17 "validate_inbound = ok (empty)" "true" check 17 "run_stages empty -> ok" "true"
check 18 "validate_outbound = ok (empty)" "true" check 18 "run_stages single ok stage" "true"
TOTAL=$((PASS+FAIL)) TOTAL=$((PASS+FAIL))
if [ $FAIL -eq 0 ]; then if [ $FAIL -eq 0 ]; then

119
next/tests/pipeline_envelope.sh Executable file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# next/tests/pipeline_envelope.sh — Step 6b acceptance test.
#
# Exercises stage_envelope/1 directly and via validate_inbound /
# validate_outbound. The envelope module must be loaded first
# because stage_envelope delegates to envelope:validate_shape/1.
# 10 cases.
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
cat > "$TMPFILE" <<'EPOCHS'
(epoch 1)
(load "lib/erlang/tokenizer.sx")
(load "lib/erlang/parser.sx")
(load "lib/erlang/parser-core.sx")
(load "lib/erlang/parser-expr.sx")
(load "lib/erlang/parser-module.sx")
(load "lib/erlang/transpile.sx")
(load "lib/erlang/runtime.sx")
(load "lib/erlang/vm/dispatcher.sx")
(epoch 2)
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
(epoch 3)
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
;; Stage list now has exactly one stage
(epoch 10)
(eval "(erlang-eval-ast \"length(pipeline:inbound_stages())\")")
(epoch 11)
(eval "(erlang-eval-ast \"length(pipeline:outbound_stages())\")")
;; stage_envelope on a valid envelope returns ok
(epoch 12)
(eval "(get (erlang-eval-ast \"pipeline:stage_envelope([{id,1},{type,create},{actor,a},{published,1},{signature,[{key_id,k},{algorithm,e},{value,v}]}]) =:= ok\") :name)")
;; stage_envelope on a non-list returns {error, not_a_proplist}
(epoch 13)
(eval "(get (erlang-eval-ast \"pipeline:stage_envelope(not_a_list) =:= {error, not_a_proplist}\") :name)")
;; stage_envelope on missing id surfaces the missing-field error
(epoch 14)
(eval "(get (erlang-eval-ast \"case pipeline:stage_envelope([{type,create}]) of {error, {missing_field, id}} -> ok; _ -> bad end\") :name)")
;; validate_inbound runs stage_envelope and returns ok for valid input
(epoch 15)
(eval "(get (erlang-eval-ast \"pipeline:validate_inbound([{id,1},{type,create},{actor,a},{published,1},{signature,[{key_id,k},{algorithm,e},{value,v}]}]) =:= ok\") :name)")
;; validate_inbound short-circuits with the envelope error
(epoch 16)
(eval "(get (erlang-eval-ast \"case pipeline:validate_inbound([{type,create}]) of {error, {missing_field, id}} -> ok; _ -> bad end\") :name)")
;; validate_outbound likewise
(epoch 17)
(eval "(get (erlang-eval-ast \"pipeline:validate_outbound([{id,1},{type,create},{actor,a},{published,1},{signature,[{key_id,k},{algorithm,e},{value,v}]}]) =:= ok\") :name)")
(epoch 18)
(eval "(get (erlang-eval-ast \"case pipeline:validate_outbound([{id,1},{actor,a}]) of {error, _} -> ok; _ -> bad end\") :name)")
;; Signature-subfield missing surfaces nested error tag
(epoch 19)
(eval "(get (erlang-eval-ast \"case pipeline:validate_inbound([{id,1},{type,create},{actor,a},{published,1},{signature,[{key_id,k}]}]) of {error, {bad_signature, _}} -> ok; _ -> bad end\") :name)")
EPOCHS
OUTPUT=$(timeout 120 "$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="<no output for epoch $epoch>"
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 2 "envelope module loaded" "envelope"
check 3 "pipeline module loaded" "pipeline"
check 10 "inbound_stages length = 1" "1"
check 11 "outbound_stages length = 1" "1"
check 12 "stage_envelope ok on valid" "true"
check 13 "stage_envelope errs on non-list" "true"
check 14 "stage_envelope missing id error" "ok"
check 15 "validate_inbound ok on valid" "true"
check 16 "validate_inbound surfaces error" "ok"
check 17 "validate_outbound ok on valid" "true"
check 18 "validate_outbound errs on bad" "ok"
check 19 "nested bad_signature surfaces" "ok"
TOTAL=$((PASS+FAIL))
if [ $FAIL -eq 0 ]; then
echo "ok $PASS/$TOTAL next/tests/pipeline_envelope.sh passed"
else
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
echo "$ERRORS"
fi
[ $FAIL -eq 0 ]

View File

@@ -387,7 +387,8 @@ projection fold maintains it.)
**Sub-deliverables:** **Sub-deliverables:**
- [x] **6a**`pipeline:run_stages/2` driver — pure fold over a stage list of `(Activity) -> ok | {error, R}` funs, halts on first failure. `validate_inbound/1` + `validate_outbound/1` + `inbound_stages/0` + `outbound_stages/0` (empty lists for now). `next/tests/pipeline_driver.sh` (10 cases). - [x] **6a**`pipeline:run_stages/2` driver — pure fold over a stage list of `(Activity) -> ok | {error, R}` funs, halts on first failure. `validate_inbound/1` + `validate_outbound/1` + `inbound_stages/0` + `outbound_stages/0` (empty lists for now). `next/tests/pipeline_driver.sh` (10 cases).
- [ ] **6b**Stage modules calling existing envelope module: `stage_envelope/1` (validate_shape), `stage_signature/1` (needs actor-state lookup — accept any signed proxy for v1) - [x] **6b-env**`pipeline:stage_envelope/1` delegating to `envelope:validate_shape/1`; wired into both `inbound_stages` and `outbound_stages`. `next/tests/pipeline_envelope.sh` (12 cases); pipeline_driver.sh updated to test the driver in isolation.
- [ ] **6b-sig**`pipeline:stage_signature/2` taking actor-state context, delegating to `envelope:verify_signature/2`. Needs a runtime-context shape since the driver only passes the activity.
- [ ] **6c**`stage_replay/1` (checks the log for existing activity id), `stage_activity_schema/1` (registry lookup + schema body eval is deferred — placeholder) - [ ] **6c**`stage_replay/1` (checks the log for existing activity id), `stage_activity_schema/1` (registry lookup + schema body eval is deferred — placeholder)
- [ ] **6d**`outbox:publish/2`: envelope construction, sign, validate_outbound, log:append, returns `{ok, #{cid, ap_id}}` - [ ] **6d**`outbox:publish/2`: envelope construction, sign, validate_outbound, log:append, returns `{ok, #{cid, ap_id}}`
- [ ] **6e** — HTTP handler for POST /activity glue (depends on Step 8 http server) - [ ] **6e** — HTTP handler for POST /activity glue (depends on Step 8 http server)
@@ -968,6 +969,7 @@ A few things still under-specified; resolve as work begins.
Newest first. One line per sub-deliverable commit. Erlang conformance gate Newest first. One line per sub-deliverable commit. Erlang conformance gate
(`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry. (`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry.
- **2026-05-28** — Step 6b-env: `pipeline:stage_envelope/1` wraps `envelope:validate_shape/1`; wired into both `inbound_stages` and `outbound_stages` lists. `validate_inbound`/`validate_outbound` now exercises the full envelope shape contract end-to-end (missing fields, signature sub-shape, non-list input). `next/tests/pipeline_envelope.sh` 12/12; `pipeline_driver.sh` refactored to test the driver against explicit stage lists rather than depending on the now-non-empty defaults. Split 6b in the plan into 6b-env (done) + 6b-sig (needs runtime context for actor-state). Erlang conformance 729/729.
- **2026-05-28** — Step 6a: `next/kernel/pipeline.erl` — validation pipeline driver per design §14. `run_stages/2` is a pure fold over `(Activity) -> ok | {error, R}` funs, halting on first failure. Halt verified by inserting a post-error stage that would set a contradictory tag if it ran. `validate_inbound/1` + `validate_outbound/1` wrappers; concrete stage lists are empty (6b wires `stage_envelope`/`stage_signature`). Port quirk: `Pattern = Var` match-alias syntax unsupported — split into separate `Result = X, case Result of ...`. `next/tests/pipeline_driver.sh` 10/10. Step 6 broken into 6a6e on the plan. Erlang conformance 729/729. - **2026-05-28** — Step 6a: `next/kernel/pipeline.erl` — validation pipeline driver per design §14. `run_stages/2` is a pure fold over `(Activity) -> ok | {error, R}` funs, halting on first failure. Halt verified by inserting a post-error stage that would set a contradictory tag if it ran. `validate_inbound/1` + `validate_outbound/1` wrappers; concrete stage lists are empty (6b wires `stage_envelope`/`stage_signature`). Port quirk: `Pattern = Var` match-alias syntax unsupported — split into separate `Result = X, case Result of ...`. `next/tests/pipeline_driver.sh` 10/10. Step 6 broken into 6a6e on the plan. Erlang conformance 729/729.
- **2026-05-28** — Step 5b: `registry.erl` extended with gen_server callbacks + named-process API. `start_link/0` spawns the worker, registers it under the literal `registry` atom, returns the Pid (port returns raw Pid not `{ok, Pid}` — diverges from OTP). 3-arity `register`, 2-arity `lookup`, 1-arity `list` delegate to the pure /4 and /3 functions inside handle_call. Port note documented: `?MODULE` macro unsupported; tests must inline start_link with operations since spawned processes don't persist across separate `erlang-eval-ast` calls. `next/tests/registry_server.sh` 12/12. Erlang conformance 729/729. - **2026-05-28** — Step 5b: `registry.erl` extended with gen_server callbacks + named-process API. `start_link/0` spawns the worker, registers it under the literal `registry` atom, returns the Pid (port returns raw Pid not `{ok, Pid}` — diverges from OTP). 3-arity `register`, 2-arity `lookup`, 1-arity `list` delegate to the pure /4 and /3 functions inside handle_call. Port note documented: `?MODULE` macro unsupported; tests must inline start_link with operations since spawned processes don't persist across separate `erlang-eval-ast` calls. `next/tests/registry_server.sh` 12/12. Erlang conformance 729/729.
- **2026-05-28** — Step 4e: `bootstrap:load_genesis/1` + `strip_sx_suffix/1` in `next/kernel/bootstrap.erl`. Walks `read_genesis` output and threads each entry through `registry:register/4`, using the section atom as the kind and the filename-minus-`.sx` as the entry name. Per-kind counts match the seven bootstrap sections exactly (3/10/7/3/3/2/3 = 31 entries total). `next/tests/bootstrap_load.sh` 15/15. Determinism verified by comparing `cid:to_string` of the loaded state across calls (faster than deep-equality on the nested-binary state). Step 4 is now complete end-to-end except for SX-source parsing of the loaded entries. Erlang conformance 729/729. - **2026-05-28** — Step 4e: `bootstrap:load_genesis/1` + `strip_sx_suffix/1` in `next/kernel/bootstrap.erl`. Walks `read_genesis` output and threads each entry through `registry:register/4`, using the section atom as the kind and the filename-minus-`.sx` as the entry name. Per-kind counts match the seven bootstrap sections exactly (3/10/7/3/3/2/3 = 31 entries total). `next/tests/bootstrap_load.sh` 15/15. Determinism verified by comparing `cid:to_string` of the loaded state across calls (faster than deep-equality on the nested-binary state). Step 4 is now complete end-to-end except for SX-source parsing of the loaded entries. Erlang conformance 729/729.