Compare commits
2 Commits
loops/fed-
...
4fc73a97f4
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc73a97f4 | |||
| 0f7444e0d5 |
@@ -1468,26 +1468,9 @@
|
|||||||
;; entry is keyed by "Module/Name/Arity"; multi-arity BIFs register
|
;; entry is keyed by "Module/Name/Arity"; multi-arity BIFs register
|
||||||
;; once per arity. Called eagerly at the end of runtime.sx so the
|
;; once per arity. Called eagerly at the end of runtime.sx so the
|
||||||
;; registry is ready before any erlang-eval-ast call.
|
;; registry is ready before any erlang-eval-ast call.
|
||||||
(define
|
(define er-register-builtin-bifs!
|
||||||
er-bif-http-listen
|
(fn ()
|
||||||
(fn
|
;; erlang module — type predicates (all pure)
|
||||||
(vs)
|
|
||||||
(let
|
|
||||||
((port (nth vs 0)) (handler (nth vs 1)))
|
|
||||||
(cond
|
|
||||||
(not (= (type-of port) "number"))
|
|
||||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
|
||||||
(not (er-fun? handler))
|
|
||||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
|
||||||
:else (let
|
|
||||||
((sx-handler (fn (req-dict) (let ((er-req (er-of-sx req-dict))) (er-to-sx (er-apply-fun handler (list er-req)))))))
|
|
||||||
(http-listen port sx-handler))))))
|
|
||||||
|
|
||||||
;; Register everything at load time.
|
|
||||||
(define
|
|
||||||
er-register-builtin-bifs!
|
|
||||||
(fn
|
|
||||||
()
|
|
||||||
(er-register-pure-bif! "erlang" "is_integer" 1 er-bif-is-integer)
|
(er-register-pure-bif! "erlang" "is_integer" 1 er-bif-is-integer)
|
||||||
(er-register-pure-bif! "erlang" "is_atom" 1 er-bif-is-atom)
|
(er-register-pure-bif! "erlang" "is_atom" 1 er-bif-is-atom)
|
||||||
(er-register-pure-bif! "erlang" "is_list" 1 er-bif-is-list)
|
(er-register-pure-bif! "erlang" "is_list" 1 er-bif-is-list)
|
||||||
@@ -1496,61 +1479,27 @@
|
|||||||
(er-register-pure-bif! "erlang" "is_float" 1 er-bif-is-float)
|
(er-register-pure-bif! "erlang" "is_float" 1 er-bif-is-float)
|
||||||
(er-register-pure-bif! "erlang" "is_boolean" 1 er-bif-is-boolean)
|
(er-register-pure-bif! "erlang" "is_boolean" 1 er-bif-is-boolean)
|
||||||
(er-register-pure-bif! "erlang" "is_pid" 1 er-bif-is-pid)
|
(er-register-pure-bif! "erlang" "is_pid" 1 er-bif-is-pid)
|
||||||
(er-register-pure-bif!
|
(er-register-pure-bif! "erlang" "is_reference" 1 er-bif-is-reference)
|
||||||
"erlang"
|
|
||||||
"is_reference"
|
|
||||||
1
|
|
||||||
er-bif-is-reference)
|
|
||||||
(er-register-pure-bif! "erlang" "is_binary" 1 er-bif-is-binary)
|
(er-register-pure-bif! "erlang" "is_binary" 1 er-bif-is-binary)
|
||||||
(er-register-pure-bif!
|
(er-register-pure-bif! "erlang" "is_function" 1 er-bif-is-function)
|
||||||
"erlang"
|
(er-register-pure-bif! "erlang" "is_function" 2 er-bif-is-function)
|
||||||
"is_function"
|
;; erlang module — pure data ops
|
||||||
1
|
|
||||||
er-bif-is-function)
|
|
||||||
(er-register-pure-bif!
|
|
||||||
"erlang"
|
|
||||||
"is_function"
|
|
||||||
2
|
|
||||||
er-bif-is-function)
|
|
||||||
(er-register-pure-bif! "erlang" "length" 1 er-bif-length)
|
(er-register-pure-bif! "erlang" "length" 1 er-bif-length)
|
||||||
(er-register-pure-bif! "erlang" "hd" 1 er-bif-hd)
|
(er-register-pure-bif! "erlang" "hd" 1 er-bif-hd)
|
||||||
(er-register-pure-bif! "erlang" "tl" 1 er-bif-tl)
|
(er-register-pure-bif! "erlang" "tl" 1 er-bif-tl)
|
||||||
(er-register-pure-bif! "erlang" "element" 2 er-bif-element)
|
(er-register-pure-bif! "erlang" "element" 2 er-bif-element)
|
||||||
(er-register-pure-bif! "erlang" "tuple_size" 1 er-bif-tuple-size)
|
(er-register-pure-bif! "erlang" "tuple_size" 1 er-bif-tuple-size)
|
||||||
(er-register-pure-bif! "erlang" "byte_size" 1 er-bif-byte-size)
|
(er-register-pure-bif! "erlang" "byte_size" 1 er-bif-byte-size)
|
||||||
(er-register-pure-bif!
|
(er-register-pure-bif! "erlang" "atom_to_list" 1 er-bif-atom-to-list)
|
||||||
"erlang"
|
(er-register-pure-bif! "erlang" "list_to_atom" 1 er-bif-list-to-atom)
|
||||||
"atom_to_list"
|
|
||||||
1
|
|
||||||
er-bif-atom-to-list)
|
|
||||||
(er-register-pure-bif!
|
|
||||||
"erlang"
|
|
||||||
"list_to_atom"
|
|
||||||
1
|
|
||||||
er-bif-list-to-atom)
|
|
||||||
(er-register-pure-bif! "erlang" "abs" 1 er-bif-abs)
|
(er-register-pure-bif! "erlang" "abs" 1 er-bif-abs)
|
||||||
(er-register-pure-bif! "erlang" "min" 2 er-bif-min)
|
(er-register-pure-bif! "erlang" "min" 2 er-bif-min)
|
||||||
(er-register-pure-bif! "erlang" "max" 2 er-bif-max)
|
(er-register-pure-bif! "erlang" "max" 2 er-bif-max)
|
||||||
(er-register-pure-bif!
|
(er-register-pure-bif! "erlang" "tuple_to_list" 1 er-bif-tuple-to-list)
|
||||||
"erlang"
|
(er-register-pure-bif! "erlang" "list_to_tuple" 1 er-bif-list-to-tuple)
|
||||||
"tuple_to_list"
|
(er-register-pure-bif! "erlang" "integer_to_list" 1 er-bif-integer-to-list)
|
||||||
1
|
(er-register-pure-bif! "erlang" "list_to_integer" 1 er-bif-list-to-integer)
|
||||||
er-bif-tuple-to-list)
|
;; erlang module — process / runtime (side-effecting)
|
||||||
(er-register-pure-bif!
|
|
||||||
"erlang"
|
|
||||||
"list_to_tuple"
|
|
||||||
1
|
|
||||||
er-bif-list-to-tuple)
|
|
||||||
(er-register-pure-bif!
|
|
||||||
"erlang"
|
|
||||||
"integer_to_list"
|
|
||||||
1
|
|
||||||
er-bif-integer-to-list)
|
|
||||||
(er-register-pure-bif!
|
|
||||||
"erlang"
|
|
||||||
"list_to_integer"
|
|
||||||
1
|
|
||||||
er-bif-list-to-integer)
|
|
||||||
(er-register-bif! "erlang" "self" 0 er-bif-self)
|
(er-register-bif! "erlang" "self" 0 er-bif-self)
|
||||||
(er-register-bif! "erlang" "spawn" 1 er-bif-spawn)
|
(er-register-bif! "erlang" "spawn" 1 er-bif-spawn)
|
||||||
(er-register-bif! "erlang" "spawn" 3 er-bif-spawn)
|
(er-register-bif! "erlang" "spawn" 3 er-bif-spawn)
|
||||||
@@ -1566,16 +1515,12 @@
|
|||||||
(er-register-bif! "erlang" "unregister" 1 er-bif-unregister)
|
(er-register-bif! "erlang" "unregister" 1 er-bif-unregister)
|
||||||
(er-register-bif! "erlang" "whereis" 1 er-bif-whereis)
|
(er-register-bif! "erlang" "whereis" 1 er-bif-whereis)
|
||||||
(er-register-bif! "erlang" "registered" 0 er-bif-registered)
|
(er-register-bif! "erlang" "registered" 0 er-bif-registered)
|
||||||
(er-register-bif!
|
;; erlang module — exception raising (modelled as side-effecting)
|
||||||
"erlang"
|
(er-register-bif! "erlang" "throw" 1
|
||||||
"throw"
|
|
||||||
1
|
|
||||||
(fn (vs) (raise (er-mk-throw-marker (er-bif-arg1 vs "throw")))))
|
(fn (vs) (raise (er-mk-throw-marker (er-bif-arg1 vs "throw")))))
|
||||||
(er-register-bif!
|
(er-register-bif! "erlang" "error" 1
|
||||||
"erlang"
|
|
||||||
"error"
|
|
||||||
1
|
|
||||||
(fn (vs) (raise (er-mk-error-marker (er-bif-arg1 vs "error")))))
|
(fn (vs) (raise (er-mk-error-marker (er-bif-arg1 vs "error")))))
|
||||||
|
;; lists module — all pure
|
||||||
(er-register-pure-bif! "lists" "reverse" 1 er-bif-lists-reverse)
|
(er-register-pure-bif! "lists" "reverse" 1 er-bif-lists-reverse)
|
||||||
(er-register-pure-bif! "lists" "map" 2 er-bif-lists-map)
|
(er-register-pure-bif! "lists" "map" 2 er-bif-lists-map)
|
||||||
(er-register-pure-bif! "lists" "foldl" 3 er-bif-lists-foldl)
|
(er-register-pure-bif! "lists" "foldl" 3 er-bif-lists-foldl)
|
||||||
@@ -1589,13 +1534,11 @@
|
|||||||
(er-register-pure-bif! "lists" "filter" 2 er-bif-lists-filter)
|
(er-register-pure-bif! "lists" "filter" 2 er-bif-lists-filter)
|
||||||
(er-register-pure-bif! "lists" "any" 2 er-bif-lists-any)
|
(er-register-pure-bif! "lists" "any" 2 er-bif-lists-any)
|
||||||
(er-register-pure-bif! "lists" "all" 2 er-bif-lists-all)
|
(er-register-pure-bif! "lists" "all" 2 er-bif-lists-all)
|
||||||
(er-register-pure-bif!
|
(er-register-pure-bif! "lists" "duplicate" 2 er-bif-lists-duplicate)
|
||||||
"lists"
|
;; io module — side-effecting (writes to io buffer)
|
||||||
"duplicate"
|
|
||||||
2
|
|
||||||
er-bif-lists-duplicate)
|
|
||||||
(er-register-bif! "io" "format" 1 er-bif-io-format)
|
(er-register-bif! "io" "format" 1 er-bif-io-format)
|
||||||
(er-register-bif! "io" "format" 2 er-bif-io-format)
|
(er-register-bif! "io" "format" 2 er-bif-io-format)
|
||||||
|
;; ets module — side-effecting (mutates table state)
|
||||||
(er-register-bif! "ets" "new" 2 er-bif-ets-new)
|
(er-register-bif! "ets" "new" 2 er-bif-ets-new)
|
||||||
(er-register-bif! "ets" "insert" 2 er-bif-ets-insert)
|
(er-register-bif! "ets" "insert" 2 er-bif-ets-insert)
|
||||||
(er-register-bif! "ets" "lookup" 2 er-bif-ets-lookup)
|
(er-register-bif! "ets" "lookup" 2 er-bif-ets-lookup)
|
||||||
@@ -1603,21 +1546,23 @@
|
|||||||
(er-register-bif! "ets" "delete" 2 er-bif-ets-delete)
|
(er-register-bif! "ets" "delete" 2 er-bif-ets-delete)
|
||||||
(er-register-bif! "ets" "tab2list" 1 er-bif-ets-tab2list)
|
(er-register-bif! "ets" "tab2list" 1 er-bif-ets-tab2list)
|
||||||
(er-register-bif! "ets" "info" 2 er-bif-ets-info)
|
(er-register-bif! "ets" "info" 2 er-bif-ets-info)
|
||||||
|
;; code module — side-effecting (mutates module registry, kills procs)
|
||||||
(er-register-bif! "code" "load_binary" 3 er-bif-code-load-binary)
|
(er-register-bif! "code" "load_binary" 3 er-bif-code-load-binary)
|
||||||
(er-register-bif! "code" "purge" 1 er-bif-code-purge)
|
(er-register-bif! "code" "purge" 1 er-bif-code-purge)
|
||||||
(er-register-bif! "code" "soft_purge" 1 er-bif-code-soft-purge)
|
(er-register-bif! "code" "soft_purge" 1 er-bif-code-soft-purge)
|
||||||
(er-register-bif! "code" "which" 1 er-bif-code-which)
|
(er-register-bif! "code" "which" 1 er-bif-code-which)
|
||||||
(er-register-bif! "code" "is_loaded" 1 er-bif-code-is-loaded)
|
(er-register-bif! "code" "is_loaded" 1 er-bif-code-is-loaded)
|
||||||
(er-register-bif! "code" "all_loaded" 0 er-bif-code-all-loaded)
|
(er-register-bif! "code" "all_loaded" 0 er-bif-code-all-loaded)
|
||||||
|
;; file module
|
||||||
(er-register-bif! "file" "read_file" 1 er-bif-file-read-file)
|
(er-register-bif! "file" "read_file" 1 er-bif-file-read-file)
|
||||||
(er-register-bif! "file" "write_file" 2 er-bif-file-write-file)
|
(er-register-bif! "file" "write_file" 2 er-bif-file-write-file)
|
||||||
(er-register-bif! "file" "delete" 1 er-bif-file-delete)
|
(er-register-bif! "file" "delete" 1 er-bif-file-delete)
|
||||||
|
;; Phase 8 FFI — host-primitive BIFs (loops/fed-prims)
|
||||||
(er-register-pure-bif! "crypto" "hash" 2 er-bif-crypto-hash)
|
(er-register-pure-bif! "crypto" "hash" 2 er-bif-crypto-hash)
|
||||||
(er-register-pure-bif! "cid" "from_bytes" 1 er-bif-cid-from-bytes)
|
(er-register-pure-bif! "cid" "from_bytes" 1 er-bif-cid-from-bytes)
|
||||||
(er-register-pure-bif! "cid" "to_string" 1 er-bif-cid-to-string)
|
(er-register-pure-bif! "cid" "to_string" 1 er-bif-cid-to-string)
|
||||||
(er-register-bif! "file" "list_dir" 1 er-bif-file-list-dir)
|
(er-register-bif! "file" "list_dir" 1 er-bif-file-list-dir)
|
||||||
(er-mk-atom "ok")))
|
(er-mk-atom "ok")))
|
||||||
|
|
||||||
(er-register-bif! "http" "listen" 2 er-bif-http-listen)
|
;; Register everything at load time.
|
||||||
|
|
||||||
(er-register-builtin-bifs!)
|
(er-register-builtin-bifs!)
|
||||||
|
|||||||
133
lib/go/conformance.sh
Executable file
133
lib/go/conformance.sh
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Go-on-SX conformance runner.
|
||||||
|
#
|
||||||
|
# Loads every Go-on-SX test suite via the epoch protocol, collects
|
||||||
|
# pass/fail counts, and writes lib/go/scoreboard.json + .md.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash lib/go/conformance.sh # run all suites
|
||||||
|
# bash lib/go/conformance.sh -v # verbose per-suite
|
||||||
|
|
||||||
|
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:-}"
|
||||||
|
TMPFILE=$(mktemp)
|
||||||
|
OUTFILE=$(mktemp)
|
||||||
|
trap "rm -f $TMPFILE $OUTFILE" EXIT
|
||||||
|
|
||||||
|
# Each suite: name | pass-counter | total-counter
|
||||||
|
SUITES=(
|
||||||
|
"lex|go-test-pass|go-test-count"
|
||||||
|
)
|
||||||
|
|
||||||
|
cat > "$TMPFILE" <<'EPOCHS'
|
||||||
|
(epoch 1)
|
||||||
|
(load "lib/guest/lex.sx")
|
||||||
|
(load "lib/go/lex.sx")
|
||||||
|
(load "lib/go/tests/lex.sx")
|
||||||
|
EPOCHS
|
||||||
|
|
||||||
|
idx=0
|
||||||
|
for entry in "${SUITES[@]}"; do
|
||||||
|
name="${entry%%|*}"
|
||||||
|
pass_var=$(echo "$entry" | awk -F'|' '{print $2}')
|
||||||
|
total_var=$(echo "$entry" | awk -F'|' '{print $3}')
|
||||||
|
epoch=$((100 + idx))
|
||||||
|
echo "(epoch $epoch)" >> "$TMPFILE"
|
||||||
|
echo "(eval \"(list $pass_var $total_var)\")" >> "$TMPFILE"
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
"$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||||
|
|
||||||
|
parse_pair() {
|
||||||
|
local epoch="$1"
|
||||||
|
local line
|
||||||
|
line=$(grep -A1 "^(ok-len $epoch " "$OUTFILE" | tail -1)
|
||||||
|
echo "$line" | sed -E 's/[()]//g'
|
||||||
|
}
|
||||||
|
|
||||||
|
TOTAL_PASS=0
|
||||||
|
TOTAL_COUNT=0
|
||||||
|
JSON_SUITES=""
|
||||||
|
MD_ROWS=""
|
||||||
|
|
||||||
|
idx=0
|
||||||
|
for entry in "${SUITES[@]}"; do
|
||||||
|
name="${entry%%|*}"
|
||||||
|
epoch=$((100 + idx))
|
||||||
|
pair=$(parse_pair "$epoch")
|
||||||
|
pass=$(echo "$pair" | awk '{print $1}')
|
||||||
|
count=$(echo "$pair" | awk '{print $2}')
|
||||||
|
if [ -z "$pass" ] || [ -z "$count" ]; then
|
||||||
|
pass=0
|
||||||
|
count=0
|
||||||
|
fi
|
||||||
|
TOTAL_PASS=$((TOTAL_PASS + pass))
|
||||||
|
TOTAL_COUNT=$((TOTAL_COUNT + count))
|
||||||
|
status="ok"
|
||||||
|
marker="✅"
|
||||||
|
if [ "$pass" != "$count" ]; then
|
||||||
|
status="fail"
|
||||||
|
marker="❌"
|
||||||
|
fi
|
||||||
|
if [ "$VERBOSE" = "-v" ]; then
|
||||||
|
printf " %-12s %s/%s\n" "$name" "$pass" "$count"
|
||||||
|
fi
|
||||||
|
if [ -n "$JSON_SUITES" ]; then JSON_SUITES+=","; fi
|
||||||
|
JSON_SUITES+=$'\n '
|
||||||
|
JSON_SUITES+="{\"name\":\"$name\",\"pass\":$pass,\"total\":$count,\"status\":\"$status\"}"
|
||||||
|
MD_ROWS+="| $marker | $name | $pass | $count |"$'\n'
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\nGo-on-SX conformance: %d / %d\n' "$TOTAL_PASS" "$TOTAL_COUNT"
|
||||||
|
|
||||||
|
cat > lib/go/scoreboard.json <<JSON
|
||||||
|
{
|
||||||
|
"language": "go",
|
||||||
|
"total_pass": $TOTAL_PASS,
|
||||||
|
"total": $TOTAL_COUNT,
|
||||||
|
"suites": [$JSON_SUITES,
|
||||||
|
{"name":"parse","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"types","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"eval","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"runtime","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"stdlib","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"e2e","pass":0,"total":0,"status":"pending"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
cat > lib/go/scoreboard.md <<MD
|
||||||
|
# Go-on-SX Scoreboard
|
||||||
|
|
||||||
|
**Total: ${TOTAL_PASS} / ${TOTAL_COUNT} tests passing**
|
||||||
|
|
||||||
|
| | Suite | Pass | Total |
|
||||||
|
|---|---|---|---|
|
||||||
|
$MD_ROWS| ⬜ | parse | 0 | 0 |
|
||||||
|
| ⬜ | types | 0 | 0 |
|
||||||
|
| ⬜ | eval | 0 | 0 |
|
||||||
|
| ⬜ | runtime | 0 | 0 |
|
||||||
|
| ⬜ | stdlib | 0 | 0 |
|
||||||
|
| ⬜ | e2e | 0 | 0 |
|
||||||
|
|
||||||
|
Generated by \`lib/go/conformance.sh\`.
|
||||||
|
MD
|
||||||
|
|
||||||
|
if [ "$TOTAL_PASS" -eq "$TOTAL_COUNT" ]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
371
lib/go/lex.sx
Normal file
371
lib/go/lex.sx
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
;; lib/go/lex.sx — Go tokenizer with automatic semicolon insertion.
|
||||||
|
;;
|
||||||
|
;; Consumes lib/guest/lex.sx character-class predicates.
|
||||||
|
;;
|
||||||
|
;; Tokens: {:type T :value V :pos P}
|
||||||
|
;; Types:
|
||||||
|
;; "ident" — identifiers (foo, _bar, mixedCase)
|
||||||
|
;; "keyword" — one of the 25 Go keywords
|
||||||
|
;; "int" — integer literals (decimal only this iteration)
|
||||||
|
;; "string" — interpreted string literals "..."
|
||||||
|
;; "rune" — rune literals 'x' (single char + simple escapes)
|
||||||
|
;; "op" — operators & punctuation; :value is the literal text
|
||||||
|
;; "semi" — explicit ';' or auto-inserted (Go spec § Semicolons)
|
||||||
|
;; "eof" — end-of-input sentinel
|
||||||
|
;;
|
||||||
|
;; ASI (Go spec § Semicolons): a newline (or EOF, or a block comment
|
||||||
|
;; containing a newline) emits a ";semi" if the previous emitted token's
|
||||||
|
;; type is ident/int/string/rune, or its value is one of
|
||||||
|
;; {break, continue, fallthrough, return, ++, --, ), ], }}.
|
||||||
|
;;
|
||||||
|
;; All scanner locals are gl- prefixed: SX host primitives (peek/emit/etc.)
|
||||||
|
;; silently shadow guest-language defines. See feedback_sx_bind_clash.
|
||||||
|
|
||||||
|
(define
|
||||||
|
go-keywords
|
||||||
|
(list
|
||||||
|
"break"
|
||||||
|
"case"
|
||||||
|
"chan"
|
||||||
|
"const"
|
||||||
|
"continue"
|
||||||
|
"default"
|
||||||
|
"defer"
|
||||||
|
"else"
|
||||||
|
"fallthrough"
|
||||||
|
"for"
|
||||||
|
"func"
|
||||||
|
"go"
|
||||||
|
"goto"
|
||||||
|
"if"
|
||||||
|
"import"
|
||||||
|
"interface"
|
||||||
|
"map"
|
||||||
|
"package"
|
||||||
|
"range"
|
||||||
|
"return"
|
||||||
|
"select"
|
||||||
|
"struct"
|
||||||
|
"switch"
|
||||||
|
"type"
|
||||||
|
"var"))
|
||||||
|
|
||||||
|
(define go-keyword? (fn (s) (some (fn (k) (= k s)) go-keywords)))
|
||||||
|
|
||||||
|
(define go-asi-keywords (list "break" "continue" "fallthrough" "return"))
|
||||||
|
|
||||||
|
(define go-asi-ops (list "++" "--" ")" "]" "}"))
|
||||||
|
|
||||||
|
(define
|
||||||
|
go-asi-trigger?
|
||||||
|
(fn
|
||||||
|
(tok)
|
||||||
|
(if
|
||||||
|
(= tok nil)
|
||||||
|
false
|
||||||
|
(let
|
||||||
|
((ty (get tok :type)) (v (get tok :value)))
|
||||||
|
(or
|
||||||
|
(= ty "ident")
|
||||||
|
(= ty "int")
|
||||||
|
(= ty "string")
|
||||||
|
(= ty "rune")
|
||||||
|
(and (= ty "keyword") (some (fn (k) (= k v)) go-asi-keywords))
|
||||||
|
(and (= ty "op") (some (fn (o) (= o v)) go-asi-ops)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
go-tokenize
|
||||||
|
(fn
|
||||||
|
(src)
|
||||||
|
(let
|
||||||
|
((tokens (list)) (pos 0) (src-len (len src)))
|
||||||
|
(define
|
||||||
|
gl-peek
|
||||||
|
(fn
|
||||||
|
(offset)
|
||||||
|
(if (< (+ pos offset) src-len) (nth src (+ pos offset)) nil)))
|
||||||
|
(define gl-cur (fn () (gl-peek 0)))
|
||||||
|
(define gl-advance! (fn (n) (set! pos (+ pos n))))
|
||||||
|
(define
|
||||||
|
gl-last
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(if
|
||||||
|
(= (len tokens) 0)
|
||||||
|
nil
|
||||||
|
(nth tokens (- (len tokens) 1)))))
|
||||||
|
(define gl-emit! (fn (type value start) (append! tokens {:type type :value value :pos start})))
|
||||||
|
(define
|
||||||
|
gl-maybe-asi!
|
||||||
|
(fn
|
||||||
|
(at)
|
||||||
|
(when (go-asi-trigger? (gl-last)) (gl-emit! "semi" "\n" at))))
|
||||||
|
(define
|
||||||
|
gl-skip-line!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (not (= (gl-cur) "\n")))
|
||||||
|
(gl-advance! 1)
|
||||||
|
(gl-skip-line!))))
|
||||||
|
(define
|
||||||
|
gl-skip-block!
|
||||||
|
(fn
|
||||||
|
(saw-nl)
|
||||||
|
(cond
|
||||||
|
(>= pos src-len)
|
||||||
|
saw-nl
|
||||||
|
(and (= (gl-cur) "*") (= (gl-peek 1) "/"))
|
||||||
|
(do (gl-advance! 2) saw-nl)
|
||||||
|
:else (let
|
||||||
|
((is-nl (= (gl-cur) "\n")))
|
||||||
|
(gl-advance! 1)
|
||||||
|
(gl-skip-block! (or saw-nl is-nl))))))
|
||||||
|
(define
|
||||||
|
gl-read-ident!
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (lex-ident-char? (gl-cur)))
|
||||||
|
(gl-advance! 1)
|
||||||
|
(gl-read-ident! start))
|
||||||
|
(slice src start pos)))
|
||||||
|
(define
|
||||||
|
gl-read-digits!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (lex-digit? (gl-cur)))
|
||||||
|
(gl-advance! 1)
|
||||||
|
(gl-read-digits!))))
|
||||||
|
(define
|
||||||
|
gl-read-string!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(gl-advance! 1)
|
||||||
|
(let
|
||||||
|
((chars (list)))
|
||||||
|
(define
|
||||||
|
gl-string-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
(>= pos src-len)
|
||||||
|
nil
|
||||||
|
(= (gl-cur) "\"")
|
||||||
|
(gl-advance! 1)
|
||||||
|
(= (gl-cur) "\\")
|
||||||
|
(do
|
||||||
|
(gl-advance! 1)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (gl-cur)))
|
||||||
|
(cond
|
||||||
|
(= ch "n")
|
||||||
|
(append! chars "\n")
|
||||||
|
(= ch "t")
|
||||||
|
(append! chars "\t")
|
||||||
|
(= ch "r")
|
||||||
|
(append! chars "\r")
|
||||||
|
(= ch "\\")
|
||||||
|
(append! chars "\\")
|
||||||
|
(= ch "\"")
|
||||||
|
(append! chars "\"")
|
||||||
|
(= ch "'")
|
||||||
|
(append! chars "'")
|
||||||
|
:else (append! chars ch))
|
||||||
|
(gl-advance! 1)))
|
||||||
|
(gl-string-loop))
|
||||||
|
:else (do
|
||||||
|
(append! chars (gl-cur))
|
||||||
|
(gl-advance! 1)
|
||||||
|
(gl-string-loop)))))
|
||||||
|
(gl-string-loop)
|
||||||
|
(join "" chars))))
|
||||||
|
(define
|
||||||
|
gl-read-rune!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(gl-advance! 1)
|
||||||
|
(let
|
||||||
|
((chars (list)))
|
||||||
|
(cond
|
||||||
|
(and (< pos src-len) (= (gl-cur) "\\"))
|
||||||
|
(do
|
||||||
|
(gl-advance! 1)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (gl-cur)))
|
||||||
|
(cond
|
||||||
|
(= ch "n")
|
||||||
|
(append! chars "\n")
|
||||||
|
(= ch "t")
|
||||||
|
(append! chars "\t")
|
||||||
|
(= ch "r")
|
||||||
|
(append! chars "\r")
|
||||||
|
(= ch "\\")
|
||||||
|
(append! chars "\\")
|
||||||
|
(= ch "'")
|
||||||
|
(append! chars "'")
|
||||||
|
(= ch "\"")
|
||||||
|
(append! chars "\"")
|
||||||
|
:else (append! chars ch))
|
||||||
|
(gl-advance! 1))))
|
||||||
|
(< pos src-len)
|
||||||
|
(do (append! chars (gl-cur)) (gl-advance! 1)))
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (= (gl-cur) "'"))
|
||||||
|
(gl-advance! 1))
|
||||||
|
(join "" chars))))
|
||||||
|
(define
|
||||||
|
gl-match-op
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((c0 (gl-cur))
|
||||||
|
(c1 (gl-peek 1))
|
||||||
|
(c2 (gl-peek 2)))
|
||||||
|
(cond
|
||||||
|
(and (= c0 "<") (= c1 "<") (= c2 "="))
|
||||||
|
"<<="
|
||||||
|
(and (= c0 ">") (= c1 ">") (= c2 "="))
|
||||||
|
">>="
|
||||||
|
(and (= c0 "&") (= c1 "^") (= c2 "="))
|
||||||
|
"&^="
|
||||||
|
(and (= c0 ".") (= c1 ".") (= c2 "."))
|
||||||
|
"..."
|
||||||
|
(and (= c0 "=") (= c1 "="))
|
||||||
|
"=="
|
||||||
|
(and (= c0 "!") (= c1 "="))
|
||||||
|
"!="
|
||||||
|
(and (= c0 "<") (= c1 "="))
|
||||||
|
"<="
|
||||||
|
(and (= c0 ">") (= c1 "="))
|
||||||
|
">="
|
||||||
|
(and (= c0 "&") (= c1 "&"))
|
||||||
|
"&&"
|
||||||
|
(and (= c0 "|") (= c1 "|"))
|
||||||
|
"||"
|
||||||
|
(and (= c0 "+") (= c1 "+"))
|
||||||
|
"++"
|
||||||
|
(and (= c0 "-") (= c1 "-"))
|
||||||
|
"--"
|
||||||
|
(and (= c0 "<") (= c1 "<"))
|
||||||
|
"<<"
|
||||||
|
(and (= c0 ">") (= c1 ">"))
|
||||||
|
">>"
|
||||||
|
(and (= c0 "+") (= c1 "="))
|
||||||
|
"+="
|
||||||
|
(and (= c0 "-") (= c1 "="))
|
||||||
|
"-="
|
||||||
|
(and (= c0 "*") (= c1 "="))
|
||||||
|
"*="
|
||||||
|
(and (= c0 "/") (= c1 "="))
|
||||||
|
"/="
|
||||||
|
(and (= c0 "%") (= c1 "="))
|
||||||
|
"%="
|
||||||
|
(and (= c0 "&") (= c1 "="))
|
||||||
|
"&="
|
||||||
|
(and (= c0 "|") (= c1 "="))
|
||||||
|
"|="
|
||||||
|
(and (= c0 "^") (= c1 "="))
|
||||||
|
"^="
|
||||||
|
(and (= c0 ":") (= c1 "="))
|
||||||
|
":="
|
||||||
|
(and (= c0 "<") (= c1 "-"))
|
||||||
|
"<-"
|
||||||
|
(and (= c0 "&") (= c1 "^"))
|
||||||
|
"&^"
|
||||||
|
(or
|
||||||
|
(= c0 "+")
|
||||||
|
(= c0 "-")
|
||||||
|
(= c0 "*")
|
||||||
|
(= c0 "/")
|
||||||
|
(= c0 "%")
|
||||||
|
(= c0 "&")
|
||||||
|
(= c0 "|")
|
||||||
|
(= c0 "^")
|
||||||
|
(= c0 "<")
|
||||||
|
(= c0 ">")
|
||||||
|
(= c0 "=")
|
||||||
|
(= c0 "!")
|
||||||
|
(= c0 "(")
|
||||||
|
(= c0 ")")
|
||||||
|
(= c0 "{")
|
||||||
|
(= c0 "}")
|
||||||
|
(= c0 "[")
|
||||||
|
(= c0 "]")
|
||||||
|
(= c0 ",")
|
||||||
|
(= c0 ".")
|
||||||
|
(= c0 ":"))
|
||||||
|
c0
|
||||||
|
:else nil))))
|
||||||
|
(define
|
||||||
|
gl-scan!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
(>= pos src-len)
|
||||||
|
nil
|
||||||
|
(= (gl-cur) "\n")
|
||||||
|
(do (gl-maybe-asi! pos) (gl-advance! 1) (gl-scan!))
|
||||||
|
(lex-space? (gl-cur))
|
||||||
|
(do (gl-advance! 1) (gl-scan!))
|
||||||
|
(and (= (gl-cur) "/") (= (gl-peek 1) "/"))
|
||||||
|
(do (gl-advance! 2) (gl-skip-line!) (gl-scan!))
|
||||||
|
(and (= (gl-cur) "/") (= (gl-peek 1) "*"))
|
||||||
|
(do
|
||||||
|
(gl-advance! 2)
|
||||||
|
(let
|
||||||
|
((saw-nl (gl-skip-block! false)))
|
||||||
|
(when saw-nl (gl-maybe-asi! pos)))
|
||||||
|
(gl-scan!))
|
||||||
|
(= (gl-cur) ";")
|
||||||
|
(do
|
||||||
|
(gl-emit! "semi" ";" pos)
|
||||||
|
(gl-advance! 1)
|
||||||
|
(gl-scan!))
|
||||||
|
(lex-ident-start? (gl-cur))
|
||||||
|
(do
|
||||||
|
(let
|
||||||
|
((start pos))
|
||||||
|
(gl-read-ident! start)
|
||||||
|
(let
|
||||||
|
((word (slice src start pos)))
|
||||||
|
(gl-emit!
|
||||||
|
(if (go-keyword? word) "keyword" "ident")
|
||||||
|
word
|
||||||
|
start)))
|
||||||
|
(gl-scan!))
|
||||||
|
(lex-digit? (gl-cur))
|
||||||
|
(do
|
||||||
|
(let
|
||||||
|
((start pos))
|
||||||
|
(gl-read-digits!)
|
||||||
|
(gl-emit! "int" (slice src start pos) start))
|
||||||
|
(gl-scan!))
|
||||||
|
(= (gl-cur) "\"")
|
||||||
|
(let
|
||||||
|
((start pos) (v (gl-read-string!)))
|
||||||
|
(gl-emit! "string" v start)
|
||||||
|
(gl-scan!))
|
||||||
|
(= (gl-cur) "'")
|
||||||
|
(let
|
||||||
|
((start pos) (v (gl-read-rune!)))
|
||||||
|
(gl-emit! "rune" v start)
|
||||||
|
(gl-scan!))
|
||||||
|
:else (let
|
||||||
|
((op (gl-match-op)))
|
||||||
|
(cond
|
||||||
|
op
|
||||||
|
(do
|
||||||
|
(gl-emit! "op" op pos)
|
||||||
|
(gl-advance! (len op))
|
||||||
|
(gl-scan!))
|
||||||
|
:else (do (gl-advance! 1) (gl-scan!)))))))
|
||||||
|
(gl-scan!)
|
||||||
|
(gl-maybe-asi! pos)
|
||||||
|
(gl-emit! "eof" nil pos)
|
||||||
|
tokens)))
|
||||||
14
lib/go/scoreboard.json
Normal file
14
lib/go/scoreboard.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"language": "go",
|
||||||
|
"total_pass": 78,
|
||||||
|
"total": 78,
|
||||||
|
"suites": [
|
||||||
|
{"name":"lex","pass":78,"total":78,"status":"ok"},
|
||||||
|
{"name":"parse","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"types","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"eval","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"runtime","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"stdlib","pass":0,"total":0,"status":"pending"},
|
||||||
|
{"name":"e2e","pass":0,"total":0,"status":"pending"}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
lib/go/scoreboard.md
Normal file
15
lib/go/scoreboard.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Go-on-SX Scoreboard
|
||||||
|
|
||||||
|
**Total: 78 / 78 tests passing**
|
||||||
|
|
||||||
|
| | Suite | Pass | Total |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ✅ | lex | 78 | 78 |
|
||||||
|
| ⬜ | parse | 0 | 0 |
|
||||||
|
| ⬜ | types | 0 | 0 |
|
||||||
|
| ⬜ | eval | 0 | 0 |
|
||||||
|
| ⬜ | runtime | 0 | 0 |
|
||||||
|
| ⬜ | stdlib | 0 | 0 |
|
||||||
|
| ⬜ | e2e | 0 | 0 |
|
||||||
|
|
||||||
|
Generated by `lib/go/conformance.sh`.
|
||||||
204
lib/go/tests/lex.sx
Normal file
204
lib/go/tests/lex.sx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
;; Go tokenizer tests.
|
||||||
|
|
||||||
|
(define go-test-count 0)
|
||||||
|
(define go-test-pass 0)
|
||||||
|
(define go-test-fails (list))
|
||||||
|
|
||||||
|
(define gtok-type (fn (t) (get t :type)))
|
||||||
|
(define gtok-value (fn (t) (get t :value)))
|
||||||
|
(define tok-types (fn (src) (map gtok-type (go-tokenize src))))
|
||||||
|
(define tok-values (fn (src) (map gtok-value (go-tokenize src))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
go-test
|
||||||
|
(fn
|
||||||
|
(name actual expected)
|
||||||
|
(set! go-test-count (+ go-test-count 1))
|
||||||
|
(if
|
||||||
|
(= actual expected)
|
||||||
|
(set! go-test-pass (+ go-test-pass 1))
|
||||||
|
(append! go-test-fails {:name name :expected expected :actual actual}))))
|
||||||
|
|
||||||
|
;; ── empty / whitespace ────────────────────────────────────────────
|
||||||
|
(go-test "empty source" (tok-types "") (list "eof"))
|
||||||
|
(go-test "spaces only" (tok-types " ") (list "eof"))
|
||||||
|
(go-test "tabs only" (tok-types "\t\t") (list "eof"))
|
||||||
|
(go-test
|
||||||
|
"newline only — no prior token, no ASI"
|
||||||
|
(tok-types "\n")
|
||||||
|
(list "eof"))
|
||||||
|
|
||||||
|
;; ── identifiers ───────────────────────────────────────────────────
|
||||||
|
(go-test "ident: simple" (tok-values "foo") (list "foo" "\n" nil))
|
||||||
|
(go-test
|
||||||
|
"ident: underscore prefix"
|
||||||
|
(tok-values "_bar")
|
||||||
|
(list "_bar" "\n" nil))
|
||||||
|
(go-test "ident: mixed case" (tok-values "fooBar") (list "fooBar" "\n" nil))
|
||||||
|
(go-test "ident: with digits" (tok-values "x123") (list "x123" "\n" nil))
|
||||||
|
(go-test "ident: type tag" (tok-types "foo") (list "ident" "semi" "eof"))
|
||||||
|
|
||||||
|
;; ── keywords (all 25) ─────────────────────────────────────────────
|
||||||
|
(go-test "kw: break" (tok-types "break") (list "keyword" "semi" "eof"))
|
||||||
|
(go-test "kw: case" (tok-types "case") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: chan" (tok-types "chan") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: const" (tok-types "const") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: continue" (tok-types "continue") (list "keyword" "semi" "eof"))
|
||||||
|
(go-test "kw: default" (tok-types "default") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: defer" (tok-types "defer") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: else" (tok-types "else") (list "keyword" "eof"))
|
||||||
|
(go-test
|
||||||
|
"kw: fallthrough"
|
||||||
|
(tok-types "fallthrough")
|
||||||
|
(list "keyword" "semi" "eof"))
|
||||||
|
(go-test "kw: for" (tok-types "for") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: func" (tok-types "func") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: go" (tok-types "go") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: goto" (tok-types "goto") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: if" (tok-types "if") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: import" (tok-types "import") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: interface" (tok-types "interface") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: map" (tok-types "map") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: package" (tok-types "package") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: range" (tok-types "range") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: return" (tok-types "return") (list "keyword" "semi" "eof"))
|
||||||
|
(go-test "kw: select" (tok-types "select") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: struct" (tok-types "struct") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: switch" (tok-types "switch") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: type" (tok-types "type") (list "keyword" "eof"))
|
||||||
|
(go-test "kw: var" (tok-types "var") (list "keyword" "eof"))
|
||||||
|
|
||||||
|
;; ── integer literals ──────────────────────────────────────────────
|
||||||
|
(go-test "int: zero" (tok-values "0") (list "0" "\n" nil))
|
||||||
|
(go-test "int: small" (tok-values "42") (list "42" "\n" nil))
|
||||||
|
(go-test "int: bigger" (tok-values "123456") (list "123456" "\n" nil))
|
||||||
|
(go-test "int: type" (tok-types "42") (list "int" "semi" "eof"))
|
||||||
|
|
||||||
|
;; ── string literals ───────────────────────────────────────────────
|
||||||
|
(go-test "string: empty" (tok-values "\"\"") (list "" "\n" nil))
|
||||||
|
(go-test "string: hello" (tok-values "\"hello\"") (list "hello" "\n" nil))
|
||||||
|
(go-test
|
||||||
|
"string: with space"
|
||||||
|
(tok-values "\"hi there\"")
|
||||||
|
(list "hi there" "\n" nil))
|
||||||
|
(go-test "string: escape n" (tok-values "\"a\\nb\"") (list "a\nb" "\n" nil))
|
||||||
|
(go-test "string: escape quote" (tok-values "\"a\\\"b\"") (list "a\"b" "\n" nil))
|
||||||
|
(go-test
|
||||||
|
"string: escape backslash"
|
||||||
|
(tok-values "\"a\\\\b\"")
|
||||||
|
(list "a\\b" "\n" nil))
|
||||||
|
(go-test "string: type" (tok-types "\"x\"") (list "string" "semi" "eof"))
|
||||||
|
|
||||||
|
;; ── rune literals ─────────────────────────────────────────────────
|
||||||
|
(go-test "rune: simple" (tok-values "'a'") (list "a" "\n" nil))
|
||||||
|
(go-test "rune: escape" (tok-values "'\\n'") (list "\n" "\n" nil))
|
||||||
|
(go-test "rune: type" (tok-types "'a'") (list "rune" "semi" "eof"))
|
||||||
|
|
||||||
|
;; ── comments ──────────────────────────────────────────────────────
|
||||||
|
(go-test "line comment" (tok-types "// ignored") (list "eof"))
|
||||||
|
(go-test "line comment then code" (tok-values "// hi\nx") (list "x" "\n" nil))
|
||||||
|
(go-test "block comment" (tok-types "/* a b c */") (list "eof"))
|
||||||
|
(go-test
|
||||||
|
"block comment inline"
|
||||||
|
(tok-values "x /* mid */ y")
|
||||||
|
(list "x" "y" "\n" nil))
|
||||||
|
(go-test
|
||||||
|
"block comment with newline — ASI"
|
||||||
|
(tok-types "x /* multi\nline */ y")
|
||||||
|
(list "ident" "semi" "ident" "semi" "eof"))
|
||||||
|
|
||||||
|
;; ── operators & punctuation ───────────────────────────────────────
|
||||||
|
(go-test
|
||||||
|
"ops: arithmetic"
|
||||||
|
(tok-values "+ - * / %")
|
||||||
|
(list "+" "-" "*" "/" "%" nil))
|
||||||
|
(go-test
|
||||||
|
"ops: comparison"
|
||||||
|
(tok-values "== != < > <= >=")
|
||||||
|
(list "==" "!=" "<" ">" "<=" ">=" nil))
|
||||||
|
(go-test "ops: logical" (tok-values "&& || !") (list "&&" "||" "!" nil))
|
||||||
|
(go-test
|
||||||
|
"ops: assign forms"
|
||||||
|
(tok-values "= := += -=")
|
||||||
|
(list "=" ":=" "+=" "-=" nil))
|
||||||
|
(go-test "ops: channel arrow" (tok-values "<- chan") (list "<-" "chan" nil))
|
||||||
|
(go-test "ops: incdec ASI" (tok-types "++ --") (list "op" "op" "semi" "eof"))
|
||||||
|
(go-test "ops: ellipsis" (tok-values "...") (list "..." nil))
|
||||||
|
(go-test
|
||||||
|
"punct: all brackets"
|
||||||
|
(tok-values "( ) { } [ ]")
|
||||||
|
(list "(" ")" "{" "}" "[" "]" "\n" nil))
|
||||||
|
(go-test
|
||||||
|
"punct: comma colon dot"
|
||||||
|
(tok-values ", : .")
|
||||||
|
(list "," ":" "." nil))
|
||||||
|
|
||||||
|
;; ── automatic semicolon insertion (Go spec § Semicolons) ──────────
|
||||||
|
(go-test
|
||||||
|
"ASI: after ident at newline"
|
||||||
|
(tok-types "x\ny")
|
||||||
|
(list "ident" "semi" "ident" "semi" "eof"))
|
||||||
|
(go-test "ASI: after int" (tok-types "42\n") (list "int" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: after string"
|
||||||
|
(tok-types "\"hi\"\n")
|
||||||
|
(list "string" "semi" "eof"))
|
||||||
|
(go-test "ASI: after rune" (tok-types "'a'\n") (list "rune" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: after )"
|
||||||
|
(tok-types "f()\n")
|
||||||
|
(list "ident" "op" "op" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: after ]"
|
||||||
|
(tok-types "x[0]\n")
|
||||||
|
(list "ident" "op" "int" "op" "semi" "eof"))
|
||||||
|
(go-test "ASI: after }" (tok-types "{}\n") (list "op" "op" "semi" "eof"))
|
||||||
|
(go-test "ASI: after ++" (tok-types "i++\n") (list "ident" "op" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: NOT after +"
|
||||||
|
(tok-types "x +\ny")
|
||||||
|
(list "ident" "op" "ident" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: NOT after ("
|
||||||
|
(tok-types "f(\nx)")
|
||||||
|
(list "ident" "op" "ident" "op" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: blank lines collapse — single semi only"
|
||||||
|
(tok-types "x\n\n\ny")
|
||||||
|
(list "ident" "semi" "ident" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: at EOF after ident"
|
||||||
|
(tok-types "x")
|
||||||
|
(list "ident" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"ASI: explicit semi"
|
||||||
|
(tok-types "x;y")
|
||||||
|
(list "ident" "semi" "ident" "semi" "eof"))
|
||||||
|
|
||||||
|
;; ── short program ─────────────────────────────────────────────────
|
||||||
|
(go-test
|
||||||
|
"short-decl: x := 42 (types)"
|
||||||
|
(tok-types "x := 42")
|
||||||
|
(list "ident" "op" "int" "semi" "eof"))
|
||||||
|
(go-test
|
||||||
|
"short-decl: x := 42 (values)"
|
||||||
|
(tok-values "x := 42")
|
||||||
|
(list "x" ":=" "42" "\n" nil))
|
||||||
|
(go-test
|
||||||
|
"func decl shape"
|
||||||
|
(tok-types "func foo() int { return 0 }")
|
||||||
|
(list
|
||||||
|
"keyword"
|
||||||
|
"ident"
|
||||||
|
"op"
|
||||||
|
"op"
|
||||||
|
"ident"
|
||||||
|
"op"
|
||||||
|
"keyword"
|
||||||
|
"int"
|
||||||
|
"op"
|
||||||
|
"semi"
|
||||||
|
"eof"))
|
||||||
|
|
||||||
|
;; ── report ────────────────────────────────────────────────────────
|
||||||
|
(define go-lex-test-summary (str "lex " go-test-pass "/" go-test-count))
|
||||||
1
next/.gitignore
vendored
1
next/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
data/
|
|
||||||
155
next/README.md
155
next/README.md
@@ -1,155 +0,0 @@
|
|||||||
# next — fed-sx Milestone 1 kernel
|
|
||||||
|
|
||||||
Single-instance, single-actor fed-sx server built as Erlang-on-SX modules.
|
|
||||||
See `plans/fed-sx-design.md` for the architecture and
|
|
||||||
`plans/fed-sx-milestone-1.md` for the build plan + per-step progress log.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Both Step 9 smoke proof points are functional **in-process**:
|
|
||||||
|
|
||||||
- **9a-pure (verb extensibility)** — `Create{DefineActivity{Pin}}` registers Pin
|
|
||||||
at runtime; subsequent `Pin{path, cid}` activities fold into a pin-state
|
|
||||||
projection. Zero kernel code between definition and use.
|
|
||||||
See `next/tests/smoke_pin_pure.sh`.
|
|
||||||
- **9b-pure (reactive application)** — A trigger projection matches Notes
|
|
||||||
tagged `smoketest` and derives a `TestEcho` carrying the source CID.
|
|
||||||
See `next/tests/smoke_app_pure.sh`.
|
|
||||||
|
|
||||||
The remaining `9a-tcp` / `9b-tcp` deliverables layer TCP transport on top — see
|
|
||||||
*Substrate gaps* below.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
next/
|
|
||||||
├── kernel/ Erlang-on-SX kernel modules (.erl)
|
|
||||||
├── genesis/ SX source files for the bootstrap bundle
|
|
||||||
├── tests/ Bash test scripts driving sx_server.exe via the epoch protocol
|
|
||||||
└── data/ Runtime state — gitignored
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module map
|
|
||||||
|
|
||||||
| Module | Role |
|
|
||||||
|-----------------------|------------------------------------------------------------------------|
|
|
||||||
| `nx_cid.erl` | Canonical CID wrapper around the host `cid:to_string` BIF |
|
|
||||||
| `envelope.erl` | Activity envelope shape, canonical bytes, time-aware sig verify |
|
|
||||||
| `log.erl` | Per-actor in-memory append log (open / append / tip / replay / entries) |
|
|
||||||
| `registry.erl` | Pure-functional + gen_server-wrapped registry keyed by Kind |
|
|
||||||
| `pipeline.erl` | Validation driver + stage_envelope/signature/replay/schema |
|
|
||||||
| `projection.erl` | Pure projection driver + gen_server-per-projection wrapper |
|
|
||||||
| `outbox.erl` | Envelope construct + sign + publish orchestrator + broadcast |
|
|
||||||
| `bootstrap.erl` | Genesis read/build/verify/load + one-call `start/3` kernel bring-up |
|
|
||||||
| `define_registry.erl` | Meta-projection fold for `Create{Define*}` → registry |
|
|
||||||
| `sandbox.erl` | `eval_pure/2,3` try/catch envelope for projection folds |
|
|
||||||
| `nx_kernel.erl` | Long-lived runtime orchestrator (state + gen_server) |
|
|
||||||
| `http_server.erl` | route/1,2 + format-aware GET + POST + Accept header content negotiation |
|
|
||||||
|
|
||||||
## Genesis bundle
|
|
||||||
|
|
||||||
`next/genesis/` contains 31 SX files across 7 sections, all consumed as data
|
|
||||||
(read + serialised by `bootstrap:populate_registry`, not eval'd):
|
|
||||||
|
|
||||||
- 3 activity-types — Create, Update, Delete
|
|
||||||
- 10 object-types — SXArtifact, Note, Tombstone, 6 Define* meta-types, Snapshot
|
|
||||||
- 7 projections — activity-log, by-type, by-actor, by-object, actor-state,
|
|
||||||
define-registry, audience-graph
|
|
||||||
- 3 validators — envelope-shape, signature, type-schema
|
|
||||||
- 3 codecs — dag-cbor, raw, dag-json
|
|
||||||
- 2 sig-suites — rsa-sha256-2018, ed25519-2020
|
|
||||||
- 3 audience predicates — Public, Followers, Direct
|
|
||||||
|
|
||||||
`manifest.sx` is the bundle root, listed in dependency-friendly order.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
43 test suites, ~560+ assertions. Each script drives `sx_server.exe` via the
|
|
||||||
epoch protocol — loads the Erlang substrate, loads relevant kernel modules
|
|
||||||
via `code:load_binary` / `erlang-load-module`, then exercises behaviour
|
|
||||||
through `erlang-eval-ast`.
|
|
||||||
|
|
||||||
Conventions:
|
|
||||||
|
|
||||||
- Scripts marked `_pure.sh` exercise pure-functional state.
|
|
||||||
- Scripts marked `_server.sh` (or no suffix) exercise gen_server APIs and
|
|
||||||
must inline `start_link` with operations — the Erlang-on-SX scheduler
|
|
||||||
doesn't preserve spawned processes across separate `erlang-eval-ast`
|
|
||||||
invocations.
|
|
||||||
- `smoke_*_pure.sh` are end-to-end smoke tests demonstrating the §Step 9
|
|
||||||
proof points without TCP / curl / JSON.
|
|
||||||
|
|
||||||
The Erlang-on-SX conformance gate (`bash lib/erlang/conformance.sh`, **729 /
|
|
||||||
729**) is the no-regression contract — every commit on `loops/fed-sx-m1`
|
|
||||||
preserves it.
|
|
||||||
|
|
||||||
## Substrate
|
|
||||||
|
|
||||||
Each `.erl` source file is hot-loaded at boot via
|
|
||||||
`code:load_binary(Mod, Filename, SourceString)` (Phase 7 BIF). Tests drive
|
|
||||||
the runtime via the epoch protocol:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
printf '(epoch 1)\n(load "lib/erlang/runtime.sx")\n(epoch 2)\n<test-expr>\n' \
|
|
||||||
| hosts/ocaml/_build/default/bin/sx_server.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
The kernel calls into these host primitives: `crypto:hash/2`,
|
|
||||||
`cid:from_bytes/1`, `cid:to_string/1`, `file:read_file/1`, `file:write_file/2`,
|
|
||||||
`file:delete/1`, `file:list_dir/1`, `code:load_binary/3`, plus `http:listen/2`
|
|
||||||
(the briefing's allowed scope exception, added to `lib/erlang/runtime.sx`).
|
|
||||||
|
|
||||||
### Substrate gaps (parked work)
|
|
||||||
|
|
||||||
These three gaps block the remaining unchecked deliverables:
|
|
||||||
|
|
||||||
1. **Term codec** (`3b`/`3c`) — `atom_to_list`/`integer_to_list` return
|
|
||||||
SX-strings (an opaque OCaml-string type), not Erlang charlists;
|
|
||||||
`binary_to_list`/`list_to_binary` are unregistered; `$X` char literals
|
|
||||||
decode to `nil` in `parse-number`. Net effect: no in-Erlang term ↔ binary
|
|
||||||
round-trip path. Blocks on-disk log persistence.
|
|
||||||
|
|
||||||
2. **SX-source eval bridge** — There's no BIF that lets Erlang call into the
|
|
||||||
SX evaluator on a parsed source string. Blocks evaluating the `:schema` /
|
|
||||||
`:fold` / `:predicate` / `:verify` bodies from the genesis bundle. Erlang-fun
|
|
||||||
stand-ins (`pipeline:stage_schema`, `define_registry:fold`, etc.) prove the
|
|
||||||
API shapes; the bridge would let bundle bodies dispatch through them
|
|
||||||
unchanged.
|
|
||||||
|
|
||||||
3. **Dict ↔ proplist marshalling for `http:listen/2`** — The native
|
|
||||||
`http-listen` primitive calls the handler with an SX dict; the BIF
|
|
||||||
wrapper's bridge would need to marshal that to / from an Erlang proplist.
|
|
||||||
Blocks `Step 8b-start` (actual TCP listening with working route dispatch).
|
|
||||||
The briefing allowed the BIF *wrapper* as a single scope exception; further
|
|
||||||
in-place modifications need agent approval.
|
|
||||||
|
|
||||||
### Bringing up the kernel
|
|
||||||
|
|
||||||
For tests, `bootstrap:start/3(ActorId, KeySpec, ActorState)` is the
|
|
||||||
one-call boot:
|
|
||||||
|
|
||||||
```erlang
|
|
||||||
KM = <<1,2,3,4>>,
|
|
||||||
KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}],
|
|
||||||
AS = [{public_keys, [[{id, k1}, {created, 0}, {value, KM}]]}],
|
|
||||||
Pid = bootstrap:start(alice, KS, AS),
|
|
||||||
%% nx_kernel + registry populated; you now have a kernel.
|
|
||||||
```
|
|
||||||
|
|
||||||
The HTTP layer (`http_server`) and `nx_kernel:publish/1` flow through the
|
|
||||||
same in-process gen_servers; `http_publish_fold.sh` is the end-to-end proof
|
|
||||||
the chain works.
|
|
||||||
|
|
||||||
## What's next (when work resumes)
|
|
||||||
|
|
||||||
In priority order:
|
|
||||||
|
|
||||||
1. **8b-bridge** — extend `er-bif-http-listen` with dict ↔ proplist marshalling
|
|
||||||
so requests reach `route/1` shaped correctly.
|
|
||||||
2. **8b-start** — `http_server:start/1` spawns a process hosting `http:listen/2`.
|
|
||||||
3. **9a-tcp / 9b-tcp** — replace the in-process smoke scripts with curl-driven
|
|
||||||
versions hitting the running server.
|
|
||||||
4. **Term codec / on-disk log** — needs either a new BIF or a temp-file
|
|
||||||
workaround; current in-memory log keeps everything functional otherwise.
|
|
||||||
5. **SX-source eval bridge** — unlocks real `:schema` / `:fold` body
|
|
||||||
evaluation from the genesis bundle.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
;; next/genesis/activity-types/create.sx
|
|
||||||
;;
|
|
||||||
;; Bootstrap definition of the Create verb per design §3 and §12.2.
|
|
||||||
;; Read as data by the bundler (bootstrap.erl) — never evaluated as
|
|
||||||
;; code. The :schema and :semantics bodies are SX source; the
|
|
||||||
;; validation pipeline (Step 6) and projection scheduler (Step 7)
|
|
||||||
;; evaluate them at the appropriate times.
|
|
||||||
|
|
||||||
(DefineActivity
|
|
||||||
:name "Create"
|
|
||||||
:doc "Publish a new object. Required for actor onboarding and for\n every Define* meta-activity. The activity's :object holds\n the canonical content of the published object."
|
|
||||||
:schema (fn
|
|
||||||
(act)
|
|
||||||
(and (not (nil? (-> act :object))) (string? (-> act :object :type))))
|
|
||||||
:semantics (fn (state act) state))
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
;; next/genesis/activity-types/delete.sx
|
|
||||||
;;
|
|
||||||
;; Bootstrap definition of the Delete verb per design §3 and §12.2.
|
|
||||||
;; Read as data by the bundler — never evaluated as code here. The
|
|
||||||
;; :schema and :semantics bodies are SX source; the validator
|
|
||||||
;; pipeline (Step 6) and projection scheduler (Step 7) evaluate them
|
|
||||||
;; at the appropriate times.
|
|
||||||
|
|
||||||
(DefineActivity
|
|
||||||
:name "Delete"
|
|
||||||
:doc "Tombstone an existing object. :object is the CID of the\n target. Projections fold Delete by removing the object from\n their working indexes; the underlying log line is never\n erased — durability of the historical record is independent\n of projection state."
|
|
||||||
:schema (fn (act) (string? (-> act :object)))
|
|
||||||
:semantics (fn (state act) state))
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
;; next/genesis/activity-types/update.sx
|
|
||||||
;;
|
|
||||||
;; Bootstrap definition of the Update verb per design §3 and §12.2.
|
|
||||||
;; Read as data by the bundler — never evaluated as code here. The
|
|
||||||
;; :schema and :semantics bodies are SX source; the validator
|
|
||||||
;; pipeline (Step 6) and projection scheduler (Step 7) evaluate them
|
|
||||||
;; at the appropriate times.
|
|
||||||
|
|
||||||
(DefineActivity
|
|
||||||
:name "Update"
|
|
||||||
:doc "Patch or replace an existing object. :object is the CID of\n the target; :patch is the field-level edit. Behaviour is\n delegated to per-object-type semantics — e.g. an Update of a\n DefineActivity supersedes the prior registry entry; an\n Update of a Person actor rotates keys via :patch :add-publicKey\n + :patch :supersede."
|
|
||||||
:schema (fn
|
|
||||||
(act)
|
|
||||||
(and (string? (-> act :object)) (not (nil? (-> act :patch)))))
|
|
||||||
:semantics (fn (state act) state))
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
;; next/genesis/audience/direct.sx
|
|
||||||
;;
|
|
||||||
;; Direct audience: an actor is a member iff they are
|
|
||||||
;; explicitly named in the activity's :to or :cc lists. No
|
|
||||||
;; group expansion — true direct addressing only.
|
|
||||||
|
|
||||||
(DefineAudience
|
|
||||||
:name "Direct"
|
|
||||||
:doc "Direct-addressing predicate. Tests literal membership\n in the activity's :to or :cc."
|
|
||||||
:member-of (fn
|
|
||||||
(actor audience)
|
|
||||||
(or
|
|
||||||
(member? actor (-> audience :to))
|
|
||||||
(member? actor (-> audience :cc)))))
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
;; next/genesis/audience/followers.sx
|
|
||||||
;;
|
|
||||||
;; Followers audience: an actor is a member iff they appear in
|
|
||||||
;; the audience-owner's :followers set in the audience-graph
|
|
||||||
;; projection. Federation (m2) wires this to peer delivery.
|
|
||||||
|
|
||||||
(DefineAudience
|
|
||||||
:name "Followers"
|
|
||||||
:doc "Followers-of-owner predicate. Looks up the\n audience-graph projection's :followers list for the\n audience owner and tests membership."
|
|
||||||
:member-of (fn
|
|
||||||
(actor audience)
|
|
||||||
(member?
|
|
||||||
actor
|
|
||||||
(-> (get-projection :audience-graph) (-> audience :owner) :followers))))
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
;; next/genesis/audience/public.sx
|
|
||||||
;;
|
|
||||||
;; Public audience: every actor is a member. Maps to the AP
|
|
||||||
;; magic id `https://www.w3.org/ns/activitystreams#Public`.
|
|
||||||
|
|
||||||
(DefineAudience
|
|
||||||
:name "Public"
|
|
||||||
:doc "Public audience predicate. Always returns true — every\n actor on the network is considered a member."
|
|
||||||
:member-of (fn (actor audience) true))
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
;; next/genesis/codecs/dag-cbor.sx
|
|
||||||
;;
|
|
||||||
;; Canonical CBOR encoding per IPLD dag-cbor. Used to compute
|
|
||||||
;; envelope canonical bytes for signature coverage and to serialise
|
|
||||||
;; the genesis bundle itself. In Erlang-on-SX mode the kernel
|
|
||||||
;; dispatches to the host cid:to_string substrate (Step 1b) when
|
|
||||||
;; this codec is requested.
|
|
||||||
|
|
||||||
(DefineCodec
|
|
||||||
:name "dag-cbor"
|
|
||||||
:doc "Deterministic CBOR with dag-cbor restrictions: sorted\n map keys, no floats unless required, no indefinite-length\n items. The canonical wire format for fed-sx artifacts."
|
|
||||||
:encode (fn (term) (host-codec :dag-cbor :encode term))
|
|
||||||
:decode (fn (bytes) (host-codec :dag-cbor :decode bytes)))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
;; next/genesis/codecs/dag-json.sx
|
|
||||||
;;
|
|
||||||
;; JSON encoding with dag-json restrictions per IPLD: sorted map
|
|
||||||
;; keys, no NaN / Infinity, no comments, CIDs as `{"/": "..."}`.
|
|
||||||
;; Used as the human-readable wire format for ActivityPub interop
|
|
||||||
;; (JSON-LD over dag-json).
|
|
||||||
|
|
||||||
(DefineCodec
|
|
||||||
:name "dag-json"
|
|
||||||
:doc "Deterministic JSON with dag-json restrictions. Sorted\n keys, CIDs as the {\"/\": \"...\"} object. Used by the\n HTTP server (Step 8) for application/json responses."
|
|
||||||
:encode (fn (term) (host-codec :dag-json :encode term))
|
|
||||||
:decode (fn (bytes) (host-codec :dag-json :decode bytes)))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
;; next/genesis/codecs/raw.sx
|
|
||||||
;;
|
|
||||||
;; Identity codec — input bytes pass through unchanged in both
|
|
||||||
;; directions. Used for already-encoded payloads and for binary
|
|
||||||
;; artifacts (images, archives) whose CID is computed over the
|
|
||||||
;; raw bytes directly.
|
|
||||||
|
|
||||||
(DefineCodec
|
|
||||||
:name "raw"
|
|
||||||
:doc "Identity codec. The CID's multicodec byte is 0x55.\n :encode and :decode return their input unchanged."
|
|
||||||
:encode (fn (bytes) bytes)
|
|
||||||
:decode (fn (bytes) bytes))
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
;; next/genesis/manifest.sx
|
|
||||||
;;
|
|
||||||
;; Genesis bundle root per design §12.2. Lists every definition file
|
|
||||||
;; that gets packed into the bundle. The bundler (bootstrap.erl)
|
|
||||||
;; walks this manifest, reads each referenced file, parses its
|
|
||||||
;; top-level form, and inserts it into the bundle dict at the
|
|
||||||
;; appropriate section path.
|
|
||||||
;;
|
|
||||||
;; The bundle CID is the content-address of the resulting dag-cbor
|
|
||||||
;; (or v1 stand-in) blob over the assembled dict. That CID is
|
|
||||||
;; baked into the kernel at build time and re-verified on startup
|
|
||||||
;; per design §12.3.
|
|
||||||
;;
|
|
||||||
;; Section values are bare parenthesised paths (data lists, not
|
|
||||||
;; function calls) — the manifest is consumed by `parse`, not
|
|
||||||
;; `eval`. Empty sections are written as `()`.
|
|
||||||
|
|
||||||
(GenesisManifest
|
|
||||||
:version "0.0.1"
|
|
||||||
:kernel-version "1.0.0-m1"
|
|
||||||
:activity-types ("activity-types/create.sx"
|
|
||||||
"activity-types/update.sx"
|
|
||||||
"activity-types/delete.sx")
|
|
||||||
:object-types ("object-types/sx-artifact.sx"
|
|
||||||
"object-types/note.sx"
|
|
||||||
"object-types/tombstone.sx"
|
|
||||||
"object-types/define-activity.sx"
|
|
||||||
"object-types/define-object.sx"
|
|
||||||
"object-types/define-projection.sx"
|
|
||||||
"object-types/define-validator.sx"
|
|
||||||
"object-types/define-codec.sx"
|
|
||||||
"object-types/define-sig-suite.sx"
|
|
||||||
"object-types/snapshot.sx")
|
|
||||||
:projections ("projections/activity-log.sx"
|
|
||||||
"projections/by-type.sx"
|
|
||||||
"projections/by-actor.sx"
|
|
||||||
"projections/by-object.sx"
|
|
||||||
"projections/actor-state.sx"
|
|
||||||
"projections/define-registry.sx"
|
|
||||||
"projections/audience-graph.sx")
|
|
||||||
:validators ("validators/envelope-shape.sx"
|
|
||||||
"validators/signature.sx"
|
|
||||||
"validators/type-schema.sx")
|
|
||||||
:codecs ("codecs/dag-cbor.sx" "codecs/raw.sx" "codecs/dag-json.sx")
|
|
||||||
:sig-suites ("sig-suites/rsa-sha256-2018.sx" "sig-suites/ed25519-2020.sx")
|
|
||||||
:audience ("audience/public.sx" "audience/followers.sx" "audience/direct.sx"))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
;; next/genesis/object-types/define-activity.sx
|
|
||||||
;;
|
|
||||||
;; Meta-object that registers a new activity verb. Published as
|
|
||||||
;; Create{DefineActivity{...}}; the define-registry projection
|
|
||||||
;; folds it into the activity-types registry. Per design §5.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "DefineActivity"
|
|
||||||
:doc "Activity-type registration. :name is the verb (e.g.\n \"Pin\"); :schema is an SX predicate over activity\n envelopes; :semantics is an optional state-fold body."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and (string? (-> obj :name)) (not (nil? (-> obj :schema))))))
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
;; next/genesis/object-types/define-codec.sx
|
|
||||||
;;
|
|
||||||
;; Meta-object that registers a content codec — an encode/decode
|
|
||||||
;; pair. The bootstrap bundle ships dag-cbor, raw, and dag-json
|
|
||||||
;; codecs; new codecs can be added via Create{DefineCodec{...}}.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "DefineCodec"
|
|
||||||
:doc "Codec registration. :name identifies the codec ('dag-cbor',\n 'raw', 'dag-json', ...); :encode and :decode are the\n SX bodies the kernel calls when serialising / parsing\n artifacts under this codec."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and
|
|
||||||
(string? (-> obj :name))
|
|
||||||
(not (nil? (-> obj :encode)))
|
|
||||||
(not (nil? (-> obj :decode))))))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
;; next/genesis/object-types/define-object.sx
|
|
||||||
;;
|
|
||||||
;; Meta-object that registers a new object-type. Bootstrap-level —
|
|
||||||
;; runtime registration of new object types (e.g. DefineSubscription
|
|
||||||
;; in the Step 9b smoke test) flows through this.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "DefineObject"
|
|
||||||
:doc "Object-type registration. :name is the type tag (e.g.\n \"PinSpec\"); :schema is an SX predicate over object\n forms of that type."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and (string? (-> obj :name)) (not (nil? (-> obj :schema))))))
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
;; next/genesis/object-types/define-projection.sx
|
|
||||||
;;
|
|
||||||
;; Meta-object that registers a new projection. The projection
|
|
||||||
;; scheduler (Step 7) spawns one gen_server per registered
|
|
||||||
;; projection and feeds activities through its :fold body in
|
|
||||||
;; sandbox mode.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "DefineProjection"
|
|
||||||
:doc "Projection registration. :name is the projection key;\n :initial-state is the empty state value; :fold is the\n pure (state activity) -> state function evaluated in\n sandbox mode per activity."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and
|
|
||||||
(string? (-> obj :name))
|
|
||||||
(not (nil? (-> obj :initial-state)))
|
|
||||||
(not (nil? (-> obj :fold))))))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
;; next/genesis/object-types/define-sig-suite.sx
|
|
||||||
;;
|
|
||||||
;; Meta-object that registers a signature suite. Bootstrap ships
|
|
||||||
;; rsa-sha256-2018 and ed25519-2020; the suite name maps an
|
|
||||||
;; algorithm to a :verify body and a :key-format predicate.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "DefineSigSuite"
|
|
||||||
:doc "Signature suite registration. :name identifies the suite\n ('rsa-sha256-2018', 'ed25519-2020', ...); :verify is the\n SX (canonical-bytes signature key) -> bool body; the\n envelope-signature validator dispatches by suite name."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and (string? (-> obj :name)) (not (nil? (-> obj :verify))))))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
;; next/genesis/object-types/define-validator.sx
|
|
||||||
;;
|
|
||||||
;; Meta-object that registers a validator predicate. The validation
|
|
||||||
;; pipeline (Step 6) consults registered validators by name when
|
|
||||||
;; running its stages.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "DefineValidator"
|
|
||||||
:doc "Validator registration. :name is the validator key (e.g.\n \"envelope-shape\"); :predicate is the SX (activity) ->\n ok|{error, R} body."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and (string? (-> obj :name)) (not (nil? (-> obj :predicate))))))
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
;; next/genesis/object-types/note.sx
|
|
||||||
;;
|
|
||||||
;; Short message intended for an audience, ActivityPub-Note-compatible.
|
|
||||||
;; Used by the Step 9b reactive smoke test (Note tagged "smoketest"
|
|
||||||
;; matches the Topic subscription).
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "Note"
|
|
||||||
:doc "Short authored message. :content is the body text;\n :tags is a list of subscription-routable tags."
|
|
||||||
:schema (fn (obj) (string? (-> obj :content))))
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
;; next/genesis/object-types/snapshot.sx
|
|
||||||
;;
|
|
||||||
;; Projection state checkpoint. The projection scheduler emits
|
|
||||||
;; Snapshot{projection-name, state-cid, log-seq} periodically;
|
|
||||||
;; cold starts read the most recent Snapshot and replay only
|
|
||||||
;; activities after :log-seq. Per design §10.5.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "Snapshot"
|
|
||||||
:doc "Projection-state checkpoint. :projection-name identifies\n the projection; :state-cid is the content-address of\n the snapshotted state value; :log-seq is the activity\n sequence number the snapshot was taken at."
|
|
||||||
:schema (fn
|
|
||||||
(obj)
|
|
||||||
(and (string? (-> obj :projection-name)) (string? (-> obj :state-cid)))))
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
;; next/genesis/object-types/sx-artifact.sx
|
|
||||||
;;
|
|
||||||
;; Content-addressed SX source — a library, component, or
|
|
||||||
;; executable form published via Create{SXArtifact{...}}.
|
|
||||||
;; Consumers reference an artifact by its CID. Per design §3.4.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "SXArtifact"
|
|
||||||
:doc "Published SX source. :source carries the form text;\n :language is optional ('sx' by default); :imports lists\n CIDs the artifact depends on."
|
|
||||||
:schema (fn (obj) (string? (-> obj :source))))
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
;; next/genesis/object-types/tombstone.sx
|
|
||||||
;;
|
|
||||||
;; Replacement for an object that has been Delete'd. Lets projection
|
|
||||||
;; folds keep a marker without retaining the deleted content.
|
|
||||||
|
|
||||||
(DefineObject
|
|
||||||
:name "Tombstone"
|
|
||||||
:doc "Marker for a deleted object. :former-cid carries the CID\n of the object that was removed. Projections fold Tombstone\n by replacing the cached entry (not by omitting it)."
|
|
||||||
:schema (fn (obj) (string? (-> obj :former-cid))))
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
;; next/genesis/projections/activity-log.sx
|
|
||||||
;;
|
|
||||||
;; Identity projection: stores every activity by its CID. The
|
|
||||||
;; base ledger every other projection could be re-derived from
|
|
||||||
;; if needed. Per design §10.2.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "activity-log"
|
|
||||||
:doc "Maps activity CID to the full envelope. Every activity\n flows through; no filter. State is the CID-keyed dict."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn (state act) (assoc state (-> act :cid) act)))
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
;; next/genesis/projections/actor-state.sx
|
|
||||||
;;
|
|
||||||
;; Per-actor live state: publicKeys (with history per design §9.6),
|
|
||||||
;; profile fields (preferredUsername, summary, ...), follower/
|
|
||||||
;; following counts. Powers the actor doc endpoint and the
|
|
||||||
;; time-aware signature verification in envelope:verify_signature/2.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "actor-state"
|
|
||||||
:doc "Actor-id -> {publicKeys, profile, followers, following}.\n Updated by Create{Person|Service|Group}, Update (key\n rotation, profile edits), Move (federation migration)."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn
|
|
||||||
(state act)
|
|
||||||
(let
|
|
||||||
((aid (-> act :actor)) (t (-> act :type)))
|
|
||||||
(cond
|
|
||||||
(= t "Create")
|
|
||||||
(assoc state aid (or (-> act :object) {}))
|
|
||||||
(= t "Update")
|
|
||||||
(assoc
|
|
||||||
state
|
|
||||||
aid
|
|
||||||
(merge
|
|
||||||
(or (get state aid) {})
|
|
||||||
(or (-> act :patch) {})))
|
|
||||||
:else state))))
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
;; next/genesis/projections/audience-graph.sx
|
|
||||||
;;
|
|
||||||
;; Per-actor follow / follower graph and audience caches. Folded
|
|
||||||
;; from Follow / Accept / Reject / Undo{Follow}. Used by the
|
|
||||||
;; activity router to expand :to / :cc audiences (Public,
|
|
||||||
;; Followers, Direct) into concrete recipient sets. Per design §16.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "audience-graph"
|
|
||||||
:doc "Actor-id -> {following, followers, pending} sets.\n Updated by Follow / Accept / Reject / Undo. Federation\n (m2) wires this projection to the delivery queue."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn
|
|
||||||
(state act)
|
|
||||||
(let
|
|
||||||
((t (-> act :type)))
|
|
||||||
(cond
|
|
||||||
(= t "Follow")
|
|
||||||
state
|
|
||||||
(= t "Accept")
|
|
||||||
state
|
|
||||||
(= t "Reject")
|
|
||||||
state
|
|
||||||
(= t "Undo")
|
|
||||||
state
|
|
||||||
:else state))))
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
;; next/genesis/projections/by-actor.sx
|
|
||||||
;;
|
|
||||||
;; Index of activity CIDs grouped by :actor. Maps actor-id to a
|
|
||||||
;; list of CIDs in append order. Powers the per-actor outbox
|
|
||||||
;; listing (Step 8) without re-scanning the full log.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "by-actor"
|
|
||||||
:doc "Actor-id -> list of activity CIDs (append order)."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn
|
|
||||||
(state act)
|
|
||||||
(let
|
|
||||||
((a (-> act :actor)) (cid (-> act :cid)))
|
|
||||||
(assoc state a (append (or (get state a) (list)) (list cid))))))
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
;; next/genesis/projections/by-object.sx
|
|
||||||
;;
|
|
||||||
;; Index of activities that reference each :object CID. Maps
|
|
||||||
;; object-CID to the list of activity CIDs that target it
|
|
||||||
;; (Update / Delete / Announce / etc.). Used for "show me
|
|
||||||
;; everything that happened to X" queries.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "by-object"
|
|
||||||
:doc "Object CID -> list of activity CIDs that target it."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn
|
|
||||||
(state act)
|
|
||||||
(let
|
|
||||||
((obj-cid (-> act :object)) (cid (-> act :cid)))
|
|
||||||
(if
|
|
||||||
(string? obj-cid)
|
|
||||||
(assoc
|
|
||||||
state
|
|
||||||
obj-cid
|
|
||||||
(append (or (get state obj-cid) (list)) (list cid)))
|
|
||||||
state))))
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
;; next/genesis/projections/by-type.sx
|
|
||||||
;;
|
|
||||||
;; Index of activity CIDs grouped by :type. Maps type-name to a
|
|
||||||
;; list of CIDs in append order. Used by the outbox listing
|
|
||||||
;; endpoints (Step 8) for type-filtered pagination.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "by-type"
|
|
||||||
:doc "Type-name -> list of activity CIDs (append order)."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn
|
|
||||||
(state act)
|
|
||||||
(let
|
|
||||||
((t (-> act :type)) (cid (-> act :cid)))
|
|
||||||
(assoc state t (append (or (get state t) (list)) (list cid))))))
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
;; next/genesis/projections/define-registry.sx
|
|
||||||
;;
|
|
||||||
;; The meta-projection: folds Create{Define*{...}} activities into
|
|
||||||
;; the kernel registry. Resolves the chicken-and-egg circle —
|
|
||||||
;; bootstrap.erl populates the registry directly at startup from
|
|
||||||
;; the genesis bundle, and from then on define-registry's fold
|
|
||||||
;; keeps it current as new Define* activities arrive. Per design §5.
|
|
||||||
|
|
||||||
(DefineProjection
|
|
||||||
:name "define-registry"
|
|
||||||
:doc "Maps {kind, name} -> definition entry. Folded from\n Create{DefineActivity|DefineObject|DefineProjection|\n DefineValidator|DefineCodec|DefineSigSuite|...}. Kind is\n derived from the inner :object :type tag."
|
|
||||||
:initial-state {}
|
|
||||||
:fold (fn
|
|
||||||
(state act)
|
|
||||||
(let
|
|
||||||
((obj (-> act :object)) (otype (-> act :object :type)))
|
|
||||||
(cond
|
|
||||||
(= (-> act :type) "Create")
|
|
||||||
(cond
|
|
||||||
(= otype "DefineActivity")
|
|
||||||
(assoc-in state (list :activity-types (-> obj :name)) obj)
|
|
||||||
(= otype "DefineObject")
|
|
||||||
(assoc-in state (list :object-types (-> obj :name)) obj)
|
|
||||||
(= otype "DefineProjection")
|
|
||||||
(assoc-in state (list :projections (-> obj :name)) obj)
|
|
||||||
(= otype "DefineValidator")
|
|
||||||
(assoc-in state (list :validators (-> obj :name)) obj)
|
|
||||||
(= otype "DefineCodec")
|
|
||||||
(assoc-in state (list :codecs (-> obj :name)) obj)
|
|
||||||
(= otype "DefineSigSuite")
|
|
||||||
(assoc-in state (list :sig-suites (-> obj :name)) obj)
|
|
||||||
:else state)
|
|
||||||
:else state))))
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
;; next/genesis/sig-suites/ed25519-2020.sx
|
|
||||||
;;
|
|
||||||
;; W3C Verifiable Credential signature suite — Ed25519 over
|
|
||||||
;; canonical bytes, key material in multibase. Default suite
|
|
||||||
;; for fed-sx actors per design §9.
|
|
||||||
|
|
||||||
(DefineSigSuite
|
|
||||||
:name "ed25519-2020"
|
|
||||||
:doc "Ed25519 verification. Key carries publicKeyMultibase.\n :verify takes canonical-bytes + signature + key and\n returns bool. Real verification deferred to m2 once\n crypto:verify_ed25519/3 BIF lands; v1 stand-in returns\n false to defer all Ed25519-signed activities."
|
|
||||||
:verify (fn (canonical-bytes signature key) false)
|
|
||||||
:key-format (fn (key-doc) (string? (-> key-doc :publicKeyMultibase))))
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
;; next/genesis/sig-suites/rsa-sha256-2018.sx
|
|
||||||
;;
|
|
||||||
;; W3C Verifiable Credential signature suite — RSA-SHA256 over
|
|
||||||
;; canonical bytes, key material in PEM. Compatible with
|
|
||||||
;; Mastodon's HTTP-Signatures / Linked-Data-Signatures-2017.
|
|
||||||
|
|
||||||
(DefineSigSuite
|
|
||||||
:name "rsa-sha256-2018"
|
|
||||||
:doc "RSA-SHA256 verification. Key carries publicKeyPem.\n :verify takes canonical-bytes + signature + key and\n returns bool. Real verification deferred to m2 once\n crypto:verify_rsa/3 BIF lands; v1 stand-in returns\n false to defer all RSA-signed activities."
|
|
||||||
:verify (fn (canonical-bytes signature key) false)
|
|
||||||
:key-format (fn (key-doc) (string? (-> key-doc :publicKeyPem))))
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
;; next/genesis/validators/envelope-shape.sx
|
|
||||||
;;
|
|
||||||
;; Validates required envelope fields per design §3.1. Stage 1 of
|
|
||||||
;; the validation pipeline (Step 6). Mirrors the kernel's
|
|
||||||
;; envelope:validate_shape/1 from Step 2a — when the pipeline runs
|
|
||||||
;; in OCaml-side sandbox eval mode it dispatches by name; when it
|
|
||||||
;; runs through the kernel Erlang path it short-circuits to the BIF.
|
|
||||||
|
|
||||||
(DefineValidator
|
|
||||||
:name "envelope-shape"
|
|
||||||
:doc "Required-fields check on the activity envelope:\n :id, :type, :actor, :published, :signature must all be\n present and non-nil. The :signature sub-field needs\n :key_id, :algorithm, :value."
|
|
||||||
:predicate (fn
|
|
||||||
(act)
|
|
||||||
(and
|
|
||||||
(not (nil? (-> act :id)))
|
|
||||||
(not (nil? (-> act :type)))
|
|
||||||
(not (nil? (-> act :actor)))
|
|
||||||
(not (nil? (-> act :published)))
|
|
||||||
(not (nil? (-> act :signature)))
|
|
||||||
(not (nil? (-> act :signature :key_id)))
|
|
||||||
(not (nil? (-> act :signature :algorithm)))
|
|
||||||
(not (nil? (-> act :signature :value))))))
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
;; next/genesis/validators/signature.sx
|
|
||||||
;;
|
|
||||||
;; Stage 2 of the validation pipeline per design §14. Verifies the
|
|
||||||
;; activity signature against the time-relevant public key in the
|
|
||||||
;; actor-state projection. Bootstrap entry; the kernel dispatches
|
|
||||||
;; to envelope:verify_signature/2 (Step 2c) when running in
|
|
||||||
;; Erlang-on-SX mode. Per design §9.6 the lookup is timestamp-aware
|
|
||||||
;; — key validity is evaluated at :published, not "now".
|
|
||||||
|
|
||||||
(DefineValidator
|
|
||||||
:name "signature"
|
|
||||||
:doc "Signature verification. Picks the signature suite by\n :signature :algorithm, fetches the key with id ==\n :signature :key_id that was active at :published from\n the actor-state projection, then dispatches to the\n suite's :verify body."
|
|
||||||
:predicate (fn (act) true))
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
;; next/genesis/validators/type-schema.sx
|
|
||||||
;;
|
|
||||||
;; Stage 5 of the validation pipeline per design §14. Validates
|
|
||||||
;; the activity's :object against the schema registered for its
|
|
||||||
;; :object :type in the define-registry projection.
|
|
||||||
|
|
||||||
(DefineValidator
|
|
||||||
:name "type-schema"
|
|
||||||
:doc "Looks up the object-type registration in the\n define-registry projection, fetches its :schema body,\n and evaluates it against (-> act :object). Returns true\n when no object-type is named (some verbs carry no\n :object) or when no schema is registered for the named\n type (open-world default — Step 6 may tighten)."
|
|
||||||
:predicate (fn
|
|
||||||
(act)
|
|
||||||
(let
|
|
||||||
((obj (-> act :object)))
|
|
||||||
(cond
|
|
||||||
(nil? obj)
|
|
||||||
true
|
|
||||||
(nil? (-> obj :type))
|
|
||||||
true
|
|
||||||
:else (let
|
|
||||||
((schema (-> (registry-lookup :object-types (-> obj :type)) :schema)))
|
|
||||||
(if (nil? schema) true (apply-schema schema obj)))))))
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
-module(bootstrap).
|
|
||||||
-export([read_genesis/0, read_genesis/1,
|
|
||||||
read_section/2, sections/0, section_subdir/1,
|
|
||||||
default_base/0, ends_with_sx/1,
|
|
||||||
build_genesis/1, verify_genesis/2,
|
|
||||||
cidhash_path/1, write_cidhash/2, read_cidhash/1,
|
|
||||||
load_genesis/1, strip_sx_suffix/1,
|
|
||||||
populate_registry/0,
|
|
||||||
start/3]).
|
|
||||||
|
|
||||||
%% Genesis bundle reader per design §12.2.
|
|
||||||
%%
|
|
||||||
%% read_genesis/0,1 walks the seven canonical section subdirectories
|
|
||||||
%% under `next/genesis/`, filters .sx files, reads each file into a
|
|
||||||
%% binary, and returns a structured snapshot:
|
|
||||||
%%
|
|
||||||
%% {ok, [{Section :: atom,
|
|
||||||
%% [{FileName :: binary, FileBytes :: binary}, ...]},
|
|
||||||
%% ...]}
|
|
||||||
%%
|
|
||||||
%% Step 4d will compute the bundle CID by hashing the assembled
|
|
||||||
%% byte string across all entries; Step 4e will register the parsed
|
|
||||||
%% definitions in the kernel registry.
|
|
||||||
%%
|
|
||||||
%% Port note: this module does NOT parse the .sx contents. The
|
|
||||||
%% Erlang-on-SX port has no in-Erlang path from binary bytes to SX
|
|
||||||
%% structured terms (same substrate gap that parked Step 3b); the
|
|
||||||
%% bundle CID needs only the raw bytes, and registry registration
|
|
||||||
%% will happen via an SX-side helper that the kernel hands the
|
|
||||||
%% binary contents to. read_genesis/1 ignores its arg in v1 except
|
|
||||||
%% to swap the BasePath — `default_base/0` is "next/genesis".
|
|
||||||
%%
|
|
||||||
%% Port note 2: string-literal binary segments `<<"abc">>` truncate
|
|
||||||
%% to one byte in this port, so all path constants are hand-spelled
|
|
||||||
%% as integer-segment binaries.
|
|
||||||
|
|
||||||
%% ── Public API ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
%% "next/genesis"
|
|
||||||
default_base() ->
|
|
||||||
<<110,101,120,116,47,103,101,110,101,115,105,115>>.
|
|
||||||
|
|
||||||
read_genesis() ->
|
|
||||||
read_genesis(default_base()).
|
|
||||||
|
|
||||||
read_genesis(BasePath) ->
|
|
||||||
{ok, lists:map(
|
|
||||||
fun (S) -> {S, read_section(BasePath, S)} end,
|
|
||||||
sections())}.
|
|
||||||
|
|
||||||
sections() ->
|
|
||||||
[activity_types, object_types, projections,
|
|
||||||
validators, codecs, sig_suites, audience].
|
|
||||||
|
|
||||||
%% "activity-types"
|
|
||||||
section_subdir(activity_types) ->
|
|
||||||
<<97,99,116,105,118,105,116,121,45,116,121,112,101,115>>;
|
|
||||||
%% "object-types"
|
|
||||||
section_subdir(object_types) ->
|
|
||||||
<<111,98,106,101,99,116,45,116,121,112,101,115>>;
|
|
||||||
%% "projections"
|
|
||||||
section_subdir(projections) ->
|
|
||||||
<<112,114,111,106,101,99,116,105,111,110,115>>;
|
|
||||||
%% "validators"
|
|
||||||
section_subdir(validators) ->
|
|
||||||
<<118,97,108,105,100,97,116,111,114,115>>;
|
|
||||||
%% "codecs"
|
|
||||||
section_subdir(codecs) ->
|
|
||||||
<<99,111,100,101,99,115>>;
|
|
||||||
%% "sig-suites"
|
|
||||||
section_subdir(sig_suites) ->
|
|
||||||
<<115,105,103,45,115,117,105,116,101,115>>;
|
|
||||||
%% "audience"
|
|
||||||
section_subdir(audience) ->
|
|
||||||
<<97,117,100,105,101,110,99,101>>.
|
|
||||||
|
|
||||||
read_section(BasePath, Section) ->
|
|
||||||
SubDir = section_subdir(Section),
|
|
||||||
%% 47 = '/'
|
|
||||||
Path = <<BasePath/binary, 47, SubDir/binary>>,
|
|
||||||
case file:list_dir(Path) of
|
|
||||||
{ok, Names} ->
|
|
||||||
SxNames = lists:filter(fun (N) -> ends_with_sx(N) end, Names),
|
|
||||||
lists:map(fun (Name) -> read_one(Path, Name) end, SxNames);
|
|
||||||
{error, _} ->
|
|
||||||
[]
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Suffix check on the .sx extension. 46='.' 115='s' 120='x'.
|
|
||||||
ends_with_sx(<<46, 115, 120>>) -> true;
|
|
||||||
ends_with_sx(<<>>) -> false;
|
|
||||||
ends_with_sx(<<_, Rest/binary>>) -> ends_with_sx(Rest).
|
|
||||||
|
|
||||||
%% ── Internal ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
read_one(DirPath, Name) ->
|
|
||||||
Full = <<DirPath/binary, 47, Name/binary>>,
|
|
||||||
case file:read_file(Full) of
|
|
||||||
{ok, Bytes} -> {Name, Bytes};
|
|
||||||
{error, R} -> {Name, {error, R}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% ── Step 4d: bundle CID compute + verify ────────────────────────
|
|
||||||
%%
|
|
||||||
%% The bundle CID is the canonical content-address of everything in
|
|
||||||
%% read_genesis/0's result. We delegate to the host `cid:to_string/1`
|
|
||||||
%% BIF (Step 1b substrate): it walks the term via `er-format-value`,
|
|
||||||
%% feeds the deterministic textual form into `cid-from-sx`, returns
|
|
||||||
%% a CIDv1 (raw codec, sha2-256 multihash) as a binary.
|
|
||||||
%%
|
|
||||||
%% Design §12.3: at startup the kernel computes this CID and
|
|
||||||
%% compares against a hardcoded value (here: a sibling `.cidhash`
|
|
||||||
%% file). A mismatch is a hard refuse-to-start.
|
|
||||||
|
|
||||||
build_genesis(ReadResult) ->
|
|
||||||
case ReadResult of
|
|
||||||
{ok, Sections} ->
|
|
||||||
Cid = cid:to_string({genesis_bundle, Sections}),
|
|
||||||
{ok, [{cid, Cid}, {sections, Sections}]};
|
|
||||||
Other ->
|
|
||||||
{error, {bad_read_result, Other}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
verify_genesis(ReadResult, ExpectedCid) ->
|
|
||||||
case build_genesis(ReadResult) of
|
|
||||||
{ok, [{cid, Cid}, _]} ->
|
|
||||||
case Cid =:= ExpectedCid of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, {cid_mismatch, Cid, ExpectedCid}}
|
|
||||||
end;
|
|
||||||
Err -> Err
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Sibling-file CID storage. "/.cidhash" appended to BasePath as
|
|
||||||
%% an integer-segment binary (string-literal segments are broken).
|
|
||||||
|
|
||||||
%% "/.cidhash" — 47='/' 46='.' c i d h a s h
|
|
||||||
cidhash_path(BasePath) ->
|
|
||||||
<<BasePath/binary, 47, 46, 99, 105, 100, 104, 97, 115, 104>>.
|
|
||||||
|
|
||||||
write_cidhash(BasePath, Cid) ->
|
|
||||||
file:write_file(cidhash_path(BasePath), Cid).
|
|
||||||
|
|
||||||
read_cidhash(BasePath) ->
|
|
||||||
file:read_file(cidhash_path(BasePath)).
|
|
||||||
|
|
||||||
%% ── Step 4e: load_genesis → registry ────────────────────────────
|
|
||||||
%%
|
|
||||||
%% Walks the read_genesis result and registers each file as a
|
|
||||||
%% registry entry. The section atom is the registry kind directly
|
|
||||||
%% (both name spaces are identical — see Step 4c sections/0 and
|
|
||||||
%% Step 5a registry:kinds/0). The entry Name is the filename minus
|
|
||||||
%% the `.sx` suffix, kept as a binary; the entry value is the
|
|
||||||
%% file's raw bytes.
|
|
||||||
%%
|
|
||||||
%% Returns `{ok, RegistryState}` on success. Later steps (4f / the
|
|
||||||
%% SX-parser bridge) will replace the raw bytes with parsed forms;
|
|
||||||
%% the binary stand-in is enough to prove the bridge works.
|
|
||||||
|
|
||||||
load_genesis(ReadResult) ->
|
|
||||||
case ReadResult of
|
|
||||||
{ok, Sections} ->
|
|
||||||
{ok, load_sections(Sections, registry:new())};
|
|
||||||
Other ->
|
|
||||||
{error, {bad_read_result, Other}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
load_sections([], State) -> State;
|
|
||||||
load_sections([{Kind, Entries} | Rest], State) ->
|
|
||||||
load_sections(Rest, load_entries(Kind, Entries, State)).
|
|
||||||
|
|
||||||
load_entries(_Kind, [], State) -> State;
|
|
||||||
load_entries(Kind, [{Name, Bytes} | Rest], State) ->
|
|
||||||
BaseName = strip_sx_suffix(Name),
|
|
||||||
{ok, NewState} = registry:register(Kind, BaseName, Bytes, State),
|
|
||||||
load_entries(Kind, Rest, NewState).
|
|
||||||
|
|
||||||
%% strip_sx_suffix(Binary) — drops the trailing ".sx" if present.
|
|
||||||
%% 46='.' 115='s' 120='x'.
|
|
||||||
strip_sx_suffix(B) when is_binary(B) ->
|
|
||||||
case ends_with_sx(B) of
|
|
||||||
false -> B;
|
|
||||||
true -> take_prefix(B, byte_size(B) - 3)
|
|
||||||
end.
|
|
||||||
|
|
||||||
take_prefix(_, 0) -> <<>>;
|
|
||||||
take_prefix(<<H, Rest/binary>>, N) when N > 0 ->
|
|
||||||
Tail = take_prefix(Rest, N - 1),
|
|
||||||
<<H, Tail/binary>>.
|
|
||||||
|
|
||||||
%% populate_registry/0 — load the canonical genesis bundle and
|
|
||||||
%% register every entry in the running registry gen_server. The
|
|
||||||
%% caller is expected to have started the registry (via
|
|
||||||
%% registry:start_link/0) before calling this. Returns the count
|
|
||||||
%% of entries registered across all kinds.
|
|
||||||
populate_registry() ->
|
|
||||||
{ok, Sections} = read_genesis(),
|
|
||||||
populate_sections(Sections, 0).
|
|
||||||
|
|
||||||
populate_sections([], Count) -> Count;
|
|
||||||
populate_sections([{Kind, Entries} | Rest], Count) ->
|
|
||||||
populate_sections(Rest, Count + populate_entries(Kind, Entries, 0)).
|
|
||||||
|
|
||||||
populate_entries(_, [], Count) -> Count;
|
|
||||||
populate_entries(Kind, [{Name, Bytes} | Rest], Count) ->
|
|
||||||
BaseName = strip_sx_suffix(Name),
|
|
||||||
ok = registry:register(Kind, BaseName, Bytes),
|
|
||||||
populate_entries(Kind, Rest, Count + 1).
|
|
||||||
|
|
||||||
%% start/3 — one-call bring-up of the kernel substrate. Starts
|
|
||||||
%% the registry gen_server, populates it from the canonical
|
|
||||||
%% genesis bundle, then starts the nx_kernel gen_server with the
|
|
||||||
%% supplied actor identity / key / state. Returns the nx_kernel
|
|
||||||
%% Pid (gen_server start_link convention in this port returns the
|
|
||||||
%% raw Pid, not {ok, Pid}).
|
|
||||||
%%
|
|
||||||
%% Tests + production bring-up share this entry point. The
|
|
||||||
%% caller is still responsible for starting any application-level
|
|
||||||
%% projections and wiring them via nx_kernel:with_projections/1.
|
|
||||||
start(ActorId, KeySpec, ActorState) ->
|
|
||||||
registry:start_link(),
|
|
||||||
populate_registry(),
|
|
||||||
nx_kernel:start_link(ActorId, KeySpec, ActorState).
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
-module(define_registry).
|
|
||||||
-export([fold/2, fold_fn/0, define_kind/1]).
|
|
||||||
|
|
||||||
%% Define-registry projection fold — Erlang-fun stand-in for the
|
|
||||||
%% genesis `define-registry.sx` body. The intent is identical: a
|
|
||||||
%% projection whose state is a registry-shaped property list, fed
|
|
||||||
%% by every `Create{Define*{...}}` activity. The SX body would
|
|
||||||
%% eventually replace this once an SX-source eval bridge lets the
|
|
||||||
%% kernel evaluate the genesis fold directly; until then this
|
|
||||||
%% Erlang module proves the meta-projection mechanism wires
|
|
||||||
%% through `projection:fold_fn` and `nx_kernel` cleanly.
|
|
||||||
%%
|
|
||||||
%% State shape mirrors `registry:new()` exactly:
|
|
||||||
%% [{Kind, [{Name, Entry}, ...]}, ...]
|
|
||||||
%% so callers can use `registry:lookup/3` etc. on the result.
|
|
||||||
%%
|
|
||||||
%% Type discrimination uses atoms (`define_activity`, …). Real SX
|
|
||||||
%% would carry the string forms ("DefineActivity", …); the bridge
|
|
||||||
%% will translate. See define_kind/1 for the mapping.
|
|
||||||
|
|
||||||
fold(Activity, State) ->
|
|
||||||
case envelope:get_field(type, Activity) of
|
|
||||||
{ok, create} -> fold_create(Activity, State);
|
|
||||||
_ -> State
|
|
||||||
end.
|
|
||||||
|
|
||||||
fold_create(Activity, State) ->
|
|
||||||
case envelope:get_field(object, Activity) of
|
|
||||||
{ok, Obj} ->
|
|
||||||
case envelope:get_field(type, Obj) of
|
|
||||||
{ok, ObjType} ->
|
|
||||||
case define_kind(ObjType) of
|
|
||||||
not_a_define -> State;
|
|
||||||
Kind -> fold_register(Kind, Obj, State)
|
|
||||||
end;
|
|
||||||
_ -> State
|
|
||||||
end;
|
|
||||||
_ -> State
|
|
||||||
end.
|
|
||||||
|
|
||||||
fold_register(Kind, Obj, State) ->
|
|
||||||
case envelope:get_field(name, Obj) of
|
|
||||||
{ok, Name} ->
|
|
||||||
case registry:register(Kind, Name, Obj, State) of
|
|
||||||
{ok, NewState} -> NewState;
|
|
||||||
{error, unknown_kind} -> State
|
|
||||||
end;
|
|
||||||
not_found -> State
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% fold_fn/0 — a 2-arity Erlang fun the projection module plants
|
|
||||||
%% in its record's :fold slot. Lets `projection:start_link/3`
|
|
||||||
%% wire define-registry directly.
|
|
||||||
fold_fn() ->
|
|
||||||
fun (Activity, State) -> fold(Activity, State) end.
|
|
||||||
|
|
||||||
%% define_kind/1 — discriminator from the inner Define* object's
|
|
||||||
%% :type atom to the registry kind atom. Anything unrecognised
|
|
||||||
%% returns not_a_define so the fold treats it as a pass-through.
|
|
||||||
|
|
||||||
define_kind(define_activity) -> activity_types;
|
|
||||||
define_kind(define_object) -> object_types;
|
|
||||||
define_kind(define_projection) -> projections;
|
|
||||||
define_kind(define_validator) -> validators;
|
|
||||||
define_kind(define_codec) -> codecs;
|
|
||||||
define_kind(define_sig_suite) -> sig_suites;
|
|
||||||
define_kind(define_audience) -> audience;
|
|
||||||
define_kind(_) -> not_a_define.
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
-module(envelope).
|
|
||||||
-export([validate_shape/1, get_field/2, canonical_bytes/1, verify_signature/2]).
|
|
||||||
|
|
||||||
%% Activity envelope per design §3.1.
|
|
||||||
%%
|
|
||||||
%% Erlang maps (#{...}) are not supported by this port, so envelopes
|
|
||||||
%% are represented as property lists of {atom_key, value} pairs. This
|
|
||||||
%% port's binary syntax also can't carry string literals; values that
|
|
||||||
%% would naturally be binaries in real Erlang are kept as atoms or
|
|
||||||
%% integer-segment binaries in the test corpus.
|
|
||||||
%%
|
|
||||||
%% Required fields: id, type, actor, published, signature.
|
|
||||||
%% The signature value is itself a property list with key_id,
|
|
||||||
%% algorithm, value.
|
|
||||||
%%
|
|
||||||
%% validate_shape/1 returns ok | {error, Reason}. Reasons:
|
|
||||||
%% not_a_proplist
|
|
||||||
%% {missing_field, FieldName}
|
|
||||||
%% {bad_signature, BadSigReason}
|
|
||||||
%%
|
|
||||||
%% get_field/2 returns {ok, Value} | not_found.
|
|
||||||
|
|
||||||
validate_shape(Env) when is_list(Env) ->
|
|
||||||
case check_required([id, type, actor, published, signature], Env) of
|
|
||||||
ok -> validate_signature_shape(Env);
|
|
||||||
Err -> Err
|
|
||||||
end;
|
|
||||||
validate_shape(_) ->
|
|
||||||
{error, not_a_proplist}.
|
|
||||||
|
|
||||||
get_field(_, []) -> not_found;
|
|
||||||
get_field(K, [{K, V} | _]) -> {ok, V};
|
|
||||||
get_field(K, [_ | Rest]) -> get_field(K, Rest).
|
|
||||||
|
|
||||||
check_required([], _) -> ok;
|
|
||||||
check_required([F | Rest], Env) ->
|
|
||||||
case get_field(F, Env) of
|
|
||||||
{ok, _} -> check_required(Rest, Env);
|
|
||||||
not_found -> {error, {missing_field, F}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
validate_signature_shape(Env) ->
|
|
||||||
{ok, Sig} = get_field(signature, Env),
|
|
||||||
case is_list(Sig) of
|
|
||||||
true ->
|
|
||||||
case check_required([key_id, algorithm, value], Sig) of
|
|
||||||
ok -> ok;
|
|
||||||
{error, {missing_field, F}} ->
|
|
||||||
{error, {bad_signature, {missing_field, F}}}
|
|
||||||
end;
|
|
||||||
false ->
|
|
||||||
{error, {bad_signature, not_a_proplist}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% canonical_bytes/1 — the byte string the signature covers.
|
|
||||||
%%
|
|
||||||
%% Real fed-sx will use dag-cbor over a JSON-LD-canonicalised form
|
|
||||||
%% (design §3.2). For milestone 1 we stand in for that with the host
|
|
||||||
%% BIF `cid:to_string/1`, which produces a CIDv1 over the deterministic
|
|
||||||
%% textual form of the term. Two prior steps make this work:
|
|
||||||
%% 1. The signature pair is stripped (sig covers everything except
|
|
||||||
%% itself).
|
|
||||||
%% 2. The top-level property list is sorted by key so field order in
|
|
||||||
%% the source envelope is not load-bearing.
|
|
||||||
%%
|
|
||||||
%% The result is an Erlang binary suitable as the sig-cover input.
|
|
||||||
|
|
||||||
canonical_bytes(Env) when is_list(Env) ->
|
|
||||||
Stripped = strip_signature(Env),
|
|
||||||
Sorted = sort_pairs(Stripped),
|
|
||||||
cid:to_string(Sorted).
|
|
||||||
|
|
||||||
strip_signature([]) -> [];
|
|
||||||
strip_signature([{signature, _} | Rest]) -> strip_signature(Rest);
|
|
||||||
strip_signature([P | Rest]) -> [P | strip_signature(Rest)].
|
|
||||||
|
|
||||||
sort_pairs([]) -> [];
|
|
||||||
sort_pairs([H | T]) -> insert_pair(H, sort_pairs(T)).
|
|
||||||
|
|
||||||
insert_pair(P, []) -> [P];
|
|
||||||
insert_pair({K1, V1}, [{K2, V2} | Rest]) ->
|
|
||||||
case K1 < K2 of
|
|
||||||
true -> [{K1, V1}, {K2, V2} | Rest];
|
|
||||||
false -> [{K2, V2} | insert_pair({K1, V1}, Rest)]
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% verify_signature/2 — time-aware sig verification per design §9.6.
|
|
||||||
%%
|
|
||||||
%% Activity carries a `signature` proplist with `key_id`, `algorithm`,
|
|
||||||
%% `value`. ActorState carries `public_keys` — a list of key proplists
|
|
||||||
%% with `id`, `created`, optionally `superseded_at`, and `value` (the
|
|
||||||
%% key material).
|
|
||||||
%%
|
|
||||||
%% A key is active at time T iff `created =< T` AND
|
|
||||||
%% (no `superseded_at` OR T < `superseded_at`). Verification picks the
|
|
||||||
%% first matching active key whose `id == signature.key_id` at the
|
|
||||||
%% activity's `published` timestamp, then recomputes the MAC
|
|
||||||
%% `crypto:hash(sha256, <<KeyMaterial/binary, CanonicalBytes/binary>>)`
|
|
||||||
%% and compares it to `signature.value`.
|
|
||||||
%%
|
|
||||||
%% Returns ok | {error, Reason}. Reasons:
|
|
||||||
%% no_signature | no_key_id | no_published | no_keys |
|
|
||||||
%% no_active_key | bad_signature
|
|
||||||
%%
|
|
||||||
%% Real RSA-SHA256 / Ed25519 verification is deferred to milestone 2:
|
|
||||||
%% Phase 8 only ships `crypto:hash/2`, so we stand in with an HMAC-shaped
|
|
||||||
%% MAC that exercises the same key-lookup and canonical-bytes pipeline.
|
|
||||||
|
|
||||||
verify_signature(Activity, ActorState) ->
|
|
||||||
case get_field(signature, Activity) of
|
|
||||||
not_found -> {error, no_signature};
|
|
||||||
{ok, Sig} ->
|
|
||||||
case get_field(key_id, Sig) of
|
|
||||||
not_found -> {error, no_key_id};
|
|
||||||
{ok, KeyId} ->
|
|
||||||
case get_field(published, Activity) of
|
|
||||||
not_found -> {error, no_published};
|
|
||||||
{ok, Published} ->
|
|
||||||
verify_with_keys(Activity, Sig, KeyId,
|
|
||||||
Published, ActorState)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
verify_with_keys(Activity, Sig, KeyId, Published, ActorState) ->
|
|
||||||
case get_field(public_keys, ActorState) of
|
|
||||||
not_found -> {error, no_keys};
|
|
||||||
{ok, Keys} ->
|
|
||||||
case find_active_key(KeyId, Published, Keys) of
|
|
||||||
not_found -> {error, no_active_key};
|
|
||||||
{ok, Key} -> verify_mac(Activity, Sig, Key)
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
find_active_key(_, _, []) -> not_found;
|
|
||||||
find_active_key(KeyId, Now, [Key | Rest]) ->
|
|
||||||
case is_matching_active_key(Key, KeyId, Now) of
|
|
||||||
true -> {ok, Key};
|
|
||||||
false -> find_active_key(KeyId, Now, Rest)
|
|
||||||
end.
|
|
||||||
|
|
||||||
is_matching_active_key(Key, WantId, Now) ->
|
|
||||||
case get_field(id, Key) of
|
|
||||||
{ok, WantId} -> is_active_at(Key, Now);
|
|
||||||
_ -> false
|
|
||||||
end.
|
|
||||||
|
|
||||||
is_active_at(Key, Now) ->
|
|
||||||
case get_field(created, Key) of
|
|
||||||
not_found -> false;
|
|
||||||
{ok, Created} ->
|
|
||||||
case Now >= Created of
|
|
||||||
false -> false;
|
|
||||||
true ->
|
|
||||||
case get_field(superseded_at, Key) of
|
|
||||||
not_found -> true;
|
|
||||||
{ok, SupAt} -> Now < SupAt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
verify_mac(Activity, Sig, Key) ->
|
|
||||||
case get_field(value, Sig) of
|
|
||||||
not_found -> {error, bad_signature};
|
|
||||||
{ok, SigValue} ->
|
|
||||||
case get_field(value, Key) of
|
|
||||||
not_found -> {error, bad_signature};
|
|
||||||
{ok, KeyMat} ->
|
|
||||||
Bytes = canonical_bytes(Activity),
|
|
||||||
Computed = crypto:hash(sha256,
|
|
||||||
<<KeyMat/binary, Bytes/binary>>),
|
|
||||||
case SigValue =:= Computed of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_signature}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
@@ -1,586 +0,0 @@
|
|||||||
-module(http_server).
|
|
||||||
-export([route/1, route/2, ok_response/1, not_found_response/0,
|
|
||||||
welcome_body/0, capabilities_body/0,
|
|
||||||
capabilities_path/0,
|
|
||||||
match_prefix/2, actors_prefix/0, actor_doc_response/1,
|
|
||||||
artifacts_prefix/0, artifact_response/1,
|
|
||||||
projections_list_path/0, projections_prefix/0,
|
|
||||||
projections_list_response/0, projection_response/1,
|
|
||||||
activity_path/0, unauthorized_response/0,
|
|
||||||
post_activity_response/0,
|
|
||||||
validation_failed_response/0,
|
|
||||||
cid_response/1,
|
|
||||||
accept_format/1, accept_format_from/1,
|
|
||||||
capabilities_body_for/1,
|
|
||||||
content_type_for/1, ok_response/2,
|
|
||||||
cid_response_for/2, post_activity_response_for/1,
|
|
||||||
actor_doc_response_for/2, artifact_response_for/2,
|
|
||||||
projection_response_for/2, projections_list_response_for/1]).
|
|
||||||
|
|
||||||
%% HTTP request router per design §16.1.
|
|
||||||
%%
|
|
||||||
%% Request shape (mirrors what the SX-side `http-listen` builds and
|
|
||||||
%% the http:listen/2 BIF bridge marshals into a proplist):
|
|
||||||
%% [{method, Binary}, {path, Binary}, {query, Binary},
|
|
||||||
%% {headers, [{Name, Value}, ...]}, {body, Binary}]
|
|
||||||
%%
|
|
||||||
%% Response shape:
|
|
||||||
%% [{status, Integer}, {headers, [{Name, Value}, ...]}, {body, Binary}]
|
|
||||||
%%
|
|
||||||
%% Real dispatch (actor docs, outbox listings, /activity POST,
|
|
||||||
%% /.well-known/sx-capabilities, etc.) lands in Step 8c+. Step 8b
|
|
||||||
%% wires the route/1 shape and a single hello-world handler that
|
|
||||||
%% proves the request→response round-trip.
|
|
||||||
%%
|
|
||||||
%% Method/path comparison uses integer-segment binaries because
|
|
||||||
%% `<<"GET">>` truncates to a single byte in this port.
|
|
||||||
|
|
||||||
route(Req) ->
|
|
||||||
route(Req, []).
|
|
||||||
|
|
||||||
%% route/2 — Cfg proplist carries optional `:publish_token` (binary)
|
|
||||||
%% for POST /activity auth. Other state (logs, projections, etc.) is
|
|
||||||
%% not yet threaded through — POST /activity returns a stub 200
|
|
||||||
%% once auth succeeds; real outbox:publish glue lands separately.
|
|
||||||
route(Req, Cfg) ->
|
|
||||||
M = field(method, Req),
|
|
||||||
P = field(path, Req),
|
|
||||||
F = accept_format_from(Req),
|
|
||||||
case {M, P} of
|
|
||||||
{<<80,79,83,84>>, <<47,97,99,116,105,118,105,116,121>>} ->
|
|
||||||
handle_post_activity(Req, Cfg);
|
|
||||||
{<<71,69,84>>,
|
|
||||||
<<47,46,119,101,108,108,45,107,110,111,119,110,
|
|
||||||
47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>} ->
|
|
||||||
ok_response(capabilities_body_for(F));
|
|
||||||
_ ->
|
|
||||||
dispatch(M, P, F)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Backward-compat /2 wrapper — defaults to text format. Route
|
|
||||||
%% computes Format from the Accept header and calls dispatch/3
|
|
||||||
%% directly; dispatch/2 is kept for callers that don't have a
|
|
||||||
%% format in scope.
|
|
||||||
dispatch(M, P) ->
|
|
||||||
dispatch(M, P, text).
|
|
||||||
|
|
||||||
%% 71 69 84 = "GET" | 47 = "/"
|
|
||||||
dispatch(<<71, 69, 84>>, <<47>>, _F) ->
|
|
||||||
ok_response(welcome_body());
|
|
||||||
%% GET /.well-known/sx-capabilities — Format threaded through
|
|
||||||
dispatch(<<71, 69, 84>>,
|
|
||||||
<<47,46,119,101,108,108,45,107,110,111,119,110,
|
|
||||||
47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>, F) ->
|
|
||||||
ok_response(capabilities_body_for(F));
|
|
||||||
%% GET /projections — list stub. Comes before the /projections/{name}
|
|
||||||
%% prefix clause because the bare path has no trailing slash.
|
|
||||||
dispatch(<<71, 69, 84>>, <<47,112,114,111,106,101,99,116,105,111,110,115>>, F) ->
|
|
||||||
projections_list_response_for(F);
|
|
||||||
%% GET /actors/{id} or /artifacts/{cid} or /projections/{name}
|
|
||||||
dispatch(<<71, 69, 84>>, Path, F) ->
|
|
||||||
case match_prefix(actors_prefix(), Path) of
|
|
||||||
{ok, Id} when byte_size(Id) > 0 ->
|
|
||||||
actor_doc_response_for(Id, F);
|
|
||||||
_ ->
|
|
||||||
case match_prefix(artifacts_prefix(), Path) of
|
|
||||||
{ok, Cid} when byte_size(Cid) > 0 ->
|
|
||||||
artifact_response_for(Cid, F);
|
|
||||||
_ ->
|
|
||||||
case match_prefix(projections_prefix(), Path) of
|
|
||||||
{ok, Name} when byte_size(Name) > 0 ->
|
|
||||||
projection_response_for(Name, F);
|
|
||||||
_ ->
|
|
||||||
not_found_response()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
dispatch(_, _, _) ->
|
|
||||||
not_found_response().
|
|
||||||
|
|
||||||
%% "fed-sx kernel m1\n" — 17 bytes, hand-spelled.
|
|
||||||
%% f e d - s x _ k e r n e l _ m 1 \n
|
|
||||||
welcome_body() ->
|
|
||||||
<<102,101,100,45,115,120,32,107,101,114,110,101,108,32,109,49,10>>.
|
|
||||||
|
|
||||||
%% "/.well-known/sx-capabilities" — exposed for callers that build
|
|
||||||
%% requests in tests or that need the canonical path string.
|
|
||||||
capabilities_path() ->
|
|
||||||
<<47,46,119,101,108,108,45,107,110,111,119,110,
|
|
||||||
47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>.
|
|
||||||
|
|
||||||
%% Capability descriptor body. Returned as plain text per design
|
|
||||||
%% §16; future content-negotiation work (Step 8d) layers JSON /
|
|
||||||
%% dag-cbor / SX representations on top.
|
|
||||||
%%
|
|
||||||
%% Lines (each terminated by \n = 10):
|
|
||||||
%% "kernel: fed-sx-m1\n"
|
|
||||||
%% "version: 0.0.1\n"
|
|
||||||
%% "verbs: Create Update Delete\n"
|
|
||||||
capabilities_body() ->
|
|
||||||
<<107,101,114,110,101,108,58,32,102,101,100,45,115,120,45,109,49,10,
|
|
||||||
118,101,114,115,105,111,110,58,32,48,46,48,46,49,10,
|
|
||||||
118,101,114,98,115,58,32,67,114,101,97,116,101,32,85,112,100,97,116,101,32,68,101,108,101,116,101,10>>.
|
|
||||||
|
|
||||||
ok_response(Body) ->
|
|
||||||
[{status, 200}, {headers, []}, {body, Body}].
|
|
||||||
|
|
||||||
not_found_response() ->
|
|
||||||
[{status, 404}, {headers, []},
|
|
||||||
{body, <<110,111,116,32,102,111,117,110,100,10>>}]. % "not found\n"
|
|
||||||
|
|
||||||
%% Internal property-list field lookup. Returns nil when missing
|
|
||||||
%% so the route falls into the not_found arm gracefully.
|
|
||||||
field(K, [{K, V} | _]) -> V;
|
|
||||||
field(K, [_ | Rest]) -> field(K, Rest);
|
|
||||||
field(_, []) -> nil.
|
|
||||||
|
|
||||||
%% ── Dynamic-segment routing ─────────────────────────────────────
|
|
||||||
%%
|
|
||||||
%% match_prefix(Prefix, Path) — if Path starts with the entire
|
|
||||||
%% Prefix binary, return {ok, Rest} where Rest is the remaining
|
|
||||||
%% bytes; else return nomatch. Pure byte-level pattern match,
|
|
||||||
%% no regex / no parsing. Path-segment splitting comes in later
|
|
||||||
%% sub-deliverables (8c-art, 8c-proj) where it's needed.
|
|
||||||
|
|
||||||
match_prefix(<<>>, Rest) -> {ok, Rest};
|
|
||||||
match_prefix(<<B, PRest/binary>>, <<B, PathRest/binary>>) ->
|
|
||||||
match_prefix(PRest, PathRest);
|
|
||||||
match_prefix(_, _) -> nomatch.
|
|
||||||
|
|
||||||
%% "/actors/" — 8 bytes: 47 97 99 116 111 114 115 47
|
|
||||||
actors_prefix() ->
|
|
||||||
<<47,97,99,116,111,114,115,47>>.
|
|
||||||
|
|
||||||
%% Actor doc stub. Real implementation (Step 8c continuation) will
|
|
||||||
%% fetch the actor-state projection entry and serialise it; v1
|
|
||||||
%% returns the id as the body so route resolution can be exercised
|
|
||||||
%% end-to-end without the projection wiring.
|
|
||||||
actor_doc_response(Id) ->
|
|
||||||
%% "actor: " — 7 bytes
|
|
||||||
Pre = <<97,99,116,111,114,58,32>>,
|
|
||||||
Body = <<Pre/binary, Id/binary, 10>>,
|
|
||||||
ok_response(Body).
|
|
||||||
|
|
||||||
%% "/artifacts/" — 11 bytes
|
|
||||||
artifacts_prefix() ->
|
|
||||||
<<47,97,114,116,105,102,97,99,116,115,47>>.
|
|
||||||
|
|
||||||
%% Artifact stub. Real implementation will fetch the bytes from
|
|
||||||
%% the registry (or a CID-keyed store) and content-negotiate.
|
|
||||||
%% v1 echoes the CID so route resolution can be tested.
|
|
||||||
artifact_response(Cid) ->
|
|
||||||
%% "artifact: " — 10 bytes
|
|
||||||
Pre = <<97,114,116,105,102,97,99,116,58,32>>,
|
|
||||||
Body = <<Pre/binary, Cid/binary, 10>>,
|
|
||||||
ok_response(Body).
|
|
||||||
|
|
||||||
%% "/projections" — 12 bytes (no trailing slash; the list endpoint)
|
|
||||||
projections_list_path() ->
|
|
||||||
<<47,112,114,111,106,101,99,116,105,111,110,115>>.
|
|
||||||
|
|
||||||
%% "/projections/" — 13 bytes (the per-projection prefix)
|
|
||||||
projections_prefix() ->
|
|
||||||
<<47,112,114,111,106,101,99,116,105,111,110,115,47>>.
|
|
||||||
|
|
||||||
%% Stub list response — real implementation queries the registry
|
|
||||||
%% for active projections and serialises the name+CID list.
|
|
||||||
projections_list_response() ->
|
|
||||||
%% "projections: (empty)\n" — hand-spelled
|
|
||||||
Body = <<112,114,111,106,101,99,116,105,111,110,115,58,32,
|
|
||||||
40,101,109,112,116,121,41,10>>,
|
|
||||||
ok_response(Body).
|
|
||||||
|
|
||||||
projection_response(Name) ->
|
|
||||||
%% "projection: " — 12 bytes
|
|
||||||
Pre = <<112,114,111,106,101,99,116,105,111,110,58,32>>,
|
|
||||||
Body = <<Pre/binary, Name/binary, 10>>,
|
|
||||||
ok_response(Body).
|
|
||||||
|
|
||||||
%% "/activity" — 9 bytes
|
|
||||||
activity_path() ->
|
|
||||||
<<47,97,99,116,105,118,105,116,121>>.
|
|
||||||
|
|
||||||
%% 401 Unauthorized response. Body: "unauthorized\n" = 13 bytes.
|
|
||||||
unauthorized_response() ->
|
|
||||||
[{status, 401}, {headers, []},
|
|
||||||
{body, <<117,110,97,117,116,104,111,114,105,122,101,100,10>>}].
|
|
||||||
|
|
||||||
%% Stub success body for POST /activity. Real impl will return
|
|
||||||
%% the published activity's CID once outbox:publish is wired
|
|
||||||
%% through a server-state context (Step 8c-post-publish).
|
|
||||||
post_activity_response() ->
|
|
||||||
%% "published (stub)\n" — hand-spelled
|
|
||||||
Body = <<112,117,98,108,105,115,104,101,100,32,
|
|
||||||
40,115,116,117,98,41,10>>,
|
|
||||||
ok_response(Body).
|
|
||||||
|
|
||||||
%% Auth helpers.
|
|
||||||
|
|
||||||
handle_post_activity(Req, Cfg) ->
|
|
||||||
case check_bearer(Req, Cfg) of
|
|
||||||
ok ->
|
|
||||||
F = accept_format_from(Req),
|
|
||||||
publish_if_kernel(Req, F);
|
|
||||||
{error, _} ->
|
|
||||||
unauthorized_response()
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% publish_if_kernel/2 — if the nx_kernel gen_server is registered,
|
|
||||||
%% delegate the publish there and translate the result. Otherwise
|
|
||||||
%% keep the stub response so the auth-only tests stay green without
|
|
||||||
%% having to spin up a kernel process. Format threads through to
|
|
||||||
%% both stub and CID responses so the Content-Type matches what
|
|
||||||
%% the client asked for via Accept.
|
|
||||||
publish_if_kernel(Req, F) ->
|
|
||||||
case erlang:whereis(nx_kernel) of
|
|
||||||
undefined ->
|
|
||||||
post_activity_response_for(F);
|
|
||||||
_Pid ->
|
|
||||||
Body = field(body, Req),
|
|
||||||
Request = [{type, create}, {object, Body}],
|
|
||||||
case nx_kernel:publish(Request) of
|
|
||||||
{ok, Result} ->
|
|
||||||
case envelope:get_field(cid, Result) of
|
|
||||||
{ok, Cid} -> cid_response_for(Cid, F);
|
|
||||||
_ -> post_activity_response_for(F)
|
|
||||||
end;
|
|
||||||
{error, _} ->
|
|
||||||
validation_failed_response()
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% 200 OK with body "cid: <cid>\n" (5 prefix bytes + cid + newline)
|
|
||||||
cid_response(Cid) ->
|
|
||||||
%% "cid: " — 99 105 100 58 32
|
|
||||||
Pre = <<99,105,100,58,32>>,
|
|
||||||
Body = <<Pre/binary, Cid/binary, 10>>,
|
|
||||||
ok_response(Body).
|
|
||||||
|
|
||||||
%% 422 Unprocessable Entity. Body "validation failed\n" — 18 bytes.
|
|
||||||
validation_failed_response() ->
|
|
||||||
[{status, 422}, {headers, []},
|
|
||||||
{body, <<118,97,108,105,100,97,116,105,111,110,32,
|
|
||||||
102,97,105,108,101,100,10>>}].
|
|
||||||
|
|
||||||
check_bearer(Req, Cfg) ->
|
|
||||||
case bearer_token(Req) of
|
|
||||||
{ok, Got} ->
|
|
||||||
case expected_token(Cfg) of
|
|
||||||
{ok, Want} when Got =:= Want -> ok;
|
|
||||||
_ -> {error, bad_token}
|
|
||||||
end;
|
|
||||||
not_found -> {error, no_auth}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Look up the Authorization header, strip "Bearer ", return token.
|
|
||||||
bearer_token(Req) ->
|
|
||||||
case field(headers, Req) of
|
|
||||||
nil -> not_found;
|
|
||||||
Hs ->
|
|
||||||
%% "authorization" — 13 bytes, lowercase as the BIF wrapper
|
|
||||||
%% normalises headers to lowercase keys.
|
|
||||||
AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>,
|
|
||||||
case find_header(AuthKey, Hs) of
|
|
||||||
not_found -> not_found;
|
|
||||||
{ok, V} -> strip_bearer(V)
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
find_header(_, []) -> not_found;
|
|
||||||
find_header(K, [{K, V} | _]) -> {ok, V};
|
|
||||||
find_header(K, [_ | Rest]) -> find_header(K, Rest).
|
|
||||||
|
|
||||||
%% "Bearer " — 7 bytes — strip and return the rest as the token.
|
|
||||||
%% Anything else returns not_found (treated as missing auth).
|
|
||||||
strip_bearer(V) ->
|
|
||||||
Prefix = <<66,101,97,114,101,114,32>>,
|
|
||||||
case match_prefix(Prefix, V) of
|
|
||||||
{ok, Token} when byte_size(Token) > 0 -> {ok, Token};
|
|
||||||
_ -> not_found
|
|
||||||
end.
|
|
||||||
|
|
||||||
expected_token(Cfg) ->
|
|
||||||
case field(publish_token, Cfg) of
|
|
||||||
nil -> not_found;
|
|
||||||
T -> {ok, T}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% ── Step 8d: Accept-header parsing ──────────────────────────────
|
|
||||||
%%
|
|
||||||
%% accept_format/1 — given an Accept header value, return the
|
|
||||||
%% content-negotiation atom the route should serialise into. The
|
|
||||||
%% first media-type prefix that matches wins, in this priority:
|
|
||||||
%% application/activity+json -> activity_json
|
|
||||||
%% application/json -> json
|
|
||||||
%% application/sx -> sx
|
|
||||||
%% application/cbor -> cbor
|
|
||||||
%% Anything else (including unrecognised, empty, or missing header)
|
|
||||||
%% returns text — current routes default to text/plain bodies.
|
|
||||||
%%
|
|
||||||
%% Per-prefix recognition uses `match_prefix`. The header value is
|
|
||||||
%% NOT split on `,` here; matching against the leading bytes is
|
|
||||||
%% enough for the v1 envelope shapes the kernel currently emits.
|
|
||||||
|
|
||||||
%% Media-type prefix byte sequences — hand-spelled because
|
|
||||||
%% `<<"...">>` string-segments truncate in this port.
|
|
||||||
|
|
||||||
%% "application/activity+json" — 25 bytes
|
|
||||||
activity_json_prefix() ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
|
||||||
97,99,116,105,118,105,116,121,43,106,115,111,110>>.
|
|
||||||
|
|
||||||
%% "application/json" — 16 bytes
|
|
||||||
json_prefix() ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>.
|
|
||||||
|
|
||||||
%% "application/sx" — 14 bytes
|
|
||||||
sx_prefix() ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>.
|
|
||||||
|
|
||||||
%% "application/cbor" — 16 bytes
|
|
||||||
cbor_prefix() ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,99,98,111,114>>.
|
|
||||||
|
|
||||||
accept_format(nil) -> text;
|
|
||||||
accept_format(<<>>) -> text;
|
|
||||||
accept_format(V) when is_binary(V) ->
|
|
||||||
case match_prefix(activity_json_prefix(), V) of
|
|
||||||
{ok, _} -> activity_json;
|
|
||||||
_ ->
|
|
||||||
case match_prefix(json_prefix(), V) of
|
|
||||||
{ok, _} -> json;
|
|
||||||
_ ->
|
|
||||||
case match_prefix(sx_prefix(), V) of
|
|
||||||
{ok, _} -> sx;
|
|
||||||
_ ->
|
|
||||||
case match_prefix(cbor_prefix(), V) of
|
|
||||||
{ok, _} -> cbor;
|
|
||||||
_ -> text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
accept_format(_) -> text.
|
|
||||||
|
|
||||||
%% accept_format_from/1 — pull the Accept header out of a request
|
|
||||||
%% proplist and run accept_format on its value. Lowercase key name
|
|
||||||
%% (matches the BIF wrapper's normalisation).
|
|
||||||
accept_format_from(Req) ->
|
|
||||||
case field(headers, Req) of
|
|
||||||
nil -> text;
|
|
||||||
Hs ->
|
|
||||||
%% "accept" — 6 bytes
|
|
||||||
K = <<97,99,99,101,112,116>>,
|
|
||||||
case find_header(K, Hs) of
|
|
||||||
{ok, V} -> accept_format(V);
|
|
||||||
not_found -> text
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% capabilities_body_for/1 — content-negotiated capability bodies.
|
|
||||||
%% Each format returns a distinct byte sequence so dispatch can be
|
|
||||||
%% observed end-to-end. Real serialisation (JSON-LD, dag-cbor, etc.)
|
|
||||||
%% lands once the corresponding encoder BIFs are wired; v1 uses
|
|
||||||
%% tagged stubs that are syntactically the right shape.
|
|
||||||
capabilities_body_for(text) ->
|
|
||||||
capabilities_body();
|
|
||||||
%% `{"caps":"fed-sx-m1"}\n` — 21 bytes
|
|
||||||
capabilities_body_for(json) ->
|
|
||||||
<<123,34,99,97,112,115,34,58,34,
|
|
||||||
102,101,100,45,115,120,45,109,49,34,125,10>>;
|
|
||||||
capabilities_body_for(activity_json) ->
|
|
||||||
%% Same payload as :json — the difference is the Content-Type
|
|
||||||
%% header (Step 8d-content-type follow-up); body shape matches.
|
|
||||||
capabilities_body_for(json);
|
|
||||||
%% `(caps "fed-sx-m1")\n` — 19 bytes
|
|
||||||
capabilities_body_for(sx) ->
|
|
||||||
<<40,99,97,112,115,32,34,
|
|
||||||
102,101,100,45,115,120,45,109,49,34,41,10>>;
|
|
||||||
%% A minimal CBOR map: 0xA1 0x64 "caps" 0x69 "fed-sx-m1"
|
|
||||||
%% A1 = map(1); 64 = text(4) "caps"; 69 = text(9) "fed-sx-m1"
|
|
||||||
capabilities_body_for(cbor) ->
|
|
||||||
<<161,100,99,97,112,115,105,
|
|
||||||
102,101,100,45,115,120,45,109,49>>;
|
|
||||||
capabilities_body_for(_) ->
|
|
||||||
capabilities_body().
|
|
||||||
|
|
||||||
%% content_type_for/1 — MIME type binary for each format atom.
|
|
||||||
%% "text/plain" — 10 bytes
|
|
||||||
content_type_for(text) ->
|
|
||||||
<<116,101,120,116,47,112,108,97,105,110>>;
|
|
||||||
%% "application/json" — 16 bytes
|
|
||||||
content_type_for(json) ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
|
||||||
106,115,111,110>>;
|
|
||||||
%% "application/activity+json" — 25 bytes
|
|
||||||
content_type_for(activity_json) ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
|
||||||
97,99,116,105,118,105,116,121,43,106,115,111,110>>;
|
|
||||||
%% "application/sx" — 14 bytes
|
|
||||||
content_type_for(sx) ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
|
||||||
115,120>>;
|
|
||||||
%% "application/cbor" — 16 bytes
|
|
||||||
content_type_for(cbor) ->
|
|
||||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
|
||||||
99,98,111,114>>;
|
|
||||||
content_type_for(_) ->
|
|
||||||
content_type_for(text).
|
|
||||||
|
|
||||||
%% ok_response/2 — 200 OK with a Content-Type header derived from
|
|
||||||
%% the Format atom. The header key is lowercase to match how the
|
|
||||||
%% BIF wrapper normalises request headers.
|
|
||||||
%% "content-type" — 12 bytes
|
|
||||||
ok_response(Body, Format) ->
|
|
||||||
CTKey = <<99,111,110,116,101,110,116,45,116,121,112,101>>,
|
|
||||||
[{status, 200},
|
|
||||||
{headers, [{CTKey, content_type_for(Format)}]},
|
|
||||||
{body, Body}].
|
|
||||||
|
|
||||||
%% cid_response_for/2 — format-aware version of cid_response/1.
|
|
||||||
%% Each variant emits a syntactically appropriate body for the
|
|
||||||
%% chosen format and tags the response with the matching
|
|
||||||
%% Content-Type via ok_response/2.
|
|
||||||
|
|
||||||
cid_response_for(Cid, text) ->
|
|
||||||
cid_response(Cid);
|
|
||||||
%% `{"cid":"<cid>"}\n` — 8-byte prefix + cid + 3-byte suffix
|
|
||||||
cid_response_for(Cid, json) ->
|
|
||||||
Pre = <<123,34,99,105,100,34,58,34>>, % '{"cid":"'
|
|
||||||
Suf = <<34,125,10>>, % '"}\n'
|
|
||||||
ok_response(<<Pre/binary, Cid/binary, Suf/binary>>, json);
|
|
||||||
cid_response_for(Cid, activity_json) ->
|
|
||||||
Pre = <<123,34,99,105,100,34,58,34>>,
|
|
||||||
Suf = <<34,125,10>>,
|
|
||||||
ok_response(<<Pre/binary, Cid/binary, Suf/binary>>, activity_json);
|
|
||||||
%% `(cid "<cid>")\n` — 6-byte prefix + cid + 3-byte suffix
|
|
||||||
cid_response_for(Cid, sx) ->
|
|
||||||
Pre = <<40,99,105,100,32,34>>, % '(cid "'
|
|
||||||
Suf = <<34,41,10>>, % '")\n'
|
|
||||||
ok_response(<<Pre/binary, Cid/binary, Suf/binary>>, sx);
|
|
||||||
%% v1 cbor stub: the raw CID bytes with the application/cbor CT.
|
|
||||||
%% Real cbor encoding (A1 63 cid 78 <len> ...) lands later.
|
|
||||||
cid_response_for(Cid, cbor) ->
|
|
||||||
ok_response(Cid, cbor);
|
|
||||||
cid_response_for(Cid, _) ->
|
|
||||||
cid_response(Cid).
|
|
||||||
|
|
||||||
%% post_activity_response_for/1 — format-aware version of
|
|
||||||
%% post_activity_response/0 (the kernel-absent stub).
|
|
||||||
|
|
||||||
post_activity_response_for(text) ->
|
|
||||||
post_activity_response();
|
|
||||||
%% `{"status":"stub"}\n` — hand-spelled
|
|
||||||
post_activity_response_for(json) ->
|
|
||||||
Body = <<123,34,115,116,97,116,117,115,34,58,34,
|
|
||||||
115,116,117,98,34,125,10>>,
|
|
||||||
ok_response(Body, json);
|
|
||||||
post_activity_response_for(activity_json) ->
|
|
||||||
Body = <<123,34,115,116,97,116,117,115,34,58,34,
|
|
||||||
115,116,117,98,34,125,10>>,
|
|
||||||
ok_response(Body, activity_json);
|
|
||||||
%% `(status "stub")\n`
|
|
||||||
post_activity_response_for(sx) ->
|
|
||||||
Body = <<40,115,116,97,116,117,115,32,34,
|
|
||||||
115,116,117,98,34,41,10>>,
|
|
||||||
ok_response(Body, sx);
|
|
||||||
post_activity_response_for(cbor) ->
|
|
||||||
%% Same body as text but with cbor CT — clients see the same
|
|
||||||
%% bytes as the text fallback. Step 8d-cbor encoder will replace.
|
|
||||||
[_, _, {body, Body}] = post_activity_response(),
|
|
||||||
ok_response(Body, cbor);
|
|
||||||
post_activity_response_for(_) ->
|
|
||||||
post_activity_response().
|
|
||||||
|
|
||||||
%% ── 8d-dispatch-get: format-aware GET responses ─────────────────
|
|
||||||
%%
|
|
||||||
%% Each builder mirrors its text-only counterpart but emits a
|
|
||||||
%% format-tagged body and Content-Type. json/activity_json share
|
|
||||||
%% the body shape but differ in CT; sx uses parenthesized form;
|
|
||||||
%% cbor returns the raw payload bytes (encoder follow-up).
|
|
||||||
|
|
||||||
%% actor_doc_response — text body `actor: <id>\n`.
|
|
||||||
|
|
||||||
actor_doc_response_for(Id, text) ->
|
|
||||||
actor_doc_response(Id);
|
|
||||||
actor_doc_response_for(Id, json) ->
|
|
||||||
Pre = <<123,34,97,99,116,111,114,34,58,34>>, % '{"actor":"'
|
|
||||||
Suf = <<34,125,10>>, % '"}\n'
|
|
||||||
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, json);
|
|
||||||
actor_doc_response_for(Id, activity_json) ->
|
|
||||||
Pre = <<123,34,97,99,116,111,114,34,58,34>>,
|
|
||||||
Suf = <<34,125,10>>,
|
|
||||||
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, activity_json);
|
|
||||||
actor_doc_response_for(Id, sx) ->
|
|
||||||
Pre = <<40,97,99,116,111,114,32,34>>, % '(actor "'
|
|
||||||
Suf = <<34,41,10>>, % '")\n'
|
|
||||||
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, sx);
|
|
||||||
actor_doc_response_for(Id, cbor) ->
|
|
||||||
ok_response(Id, cbor);
|
|
||||||
actor_doc_response_for(Id, _) ->
|
|
||||||
actor_doc_response(Id).
|
|
||||||
|
|
||||||
%% artifact_response — text body `artifact: <cid>\n`.
|
|
||||||
|
|
||||||
artifact_response_for(Cid, text) ->
|
|
||||||
artifact_response(Cid);
|
|
||||||
artifact_response_for(Cid, json) ->
|
|
||||||
Pre = <<123,34,97,114,116,105,102,97,99,116,34,58,34>>,
|
|
||||||
Suf = <<34,125,10>>,
|
|
||||||
ok_response(<<Pre/binary, Cid/binary, Suf/binary>>, json);
|
|
||||||
artifact_response_for(Cid, activity_json) ->
|
|
||||||
Pre = <<123,34,97,114,116,105,102,97,99,116,34,58,34>>,
|
|
||||||
Suf = <<34,125,10>>,
|
|
||||||
ok_response(<<Pre/binary, Cid/binary, Suf/binary>>, activity_json);
|
|
||||||
artifact_response_for(Cid, sx) ->
|
|
||||||
Pre = <<40,97,114,116,105,102,97,99,116,32,34>>,
|
|
||||||
Suf = <<34,41,10>>,
|
|
||||||
ok_response(<<Pre/binary, Cid/binary, Suf/binary>>, sx);
|
|
||||||
artifact_response_for(Cid, cbor) ->
|
|
||||||
ok_response(Cid, cbor);
|
|
||||||
artifact_response_for(Cid, _) ->
|
|
||||||
artifact_response(Cid).
|
|
||||||
|
|
||||||
%% projection_response (singular) — text body `projection: <name>\n`.
|
|
||||||
|
|
||||||
projection_response_for(Name, text) ->
|
|
||||||
projection_response(Name);
|
|
||||||
projection_response_for(Name, json) ->
|
|
||||||
Pre = <<123,34,112,114,111,106,101,99,116,105,111,110,34,58,34>>,
|
|
||||||
Suf = <<34,125,10>>,
|
|
||||||
ok_response(<<Pre/binary, Name/binary, Suf/binary>>, json);
|
|
||||||
projection_response_for(Name, activity_json) ->
|
|
||||||
Pre = <<123,34,112,114,111,106,101,99,116,105,111,110,34,58,34>>,
|
|
||||||
Suf = <<34,125,10>>,
|
|
||||||
ok_response(<<Pre/binary, Name/binary, Suf/binary>>, activity_json);
|
|
||||||
projection_response_for(Name, sx) ->
|
|
||||||
Pre = <<40,112,114,111,106,101,99,116,105,111,110,32,34>>,
|
|
||||||
Suf = <<34,41,10>>,
|
|
||||||
ok_response(<<Pre/binary, Name/binary, Suf/binary>>, sx);
|
|
||||||
projection_response_for(Name, cbor) ->
|
|
||||||
ok_response(Name, cbor);
|
|
||||||
projection_response_for(Name, _) ->
|
|
||||||
projection_response(Name).
|
|
||||||
|
|
||||||
%% projections_list_response — empty-list stub.
|
|
||||||
|
|
||||||
projections_list_response_for(text) ->
|
|
||||||
projections_list_response();
|
|
||||||
%% `{"projections":[]}\n`
|
|
||||||
projections_list_response_for(json) ->
|
|
||||||
Body = <<123,34,112,114,111,106,101,99,116,105,111,110,115,
|
|
||||||
34,58,91,93,125,10>>,
|
|
||||||
ok_response(Body, json);
|
|
||||||
projections_list_response_for(activity_json) ->
|
|
||||||
Body = <<123,34,112,114,111,106,101,99,116,105,111,110,115,
|
|
||||||
34,58,91,93,125,10>>,
|
|
||||||
ok_response(Body, activity_json);
|
|
||||||
%% `(projections)\n`
|
|
||||||
projections_list_response_for(sx) ->
|
|
||||||
Body = <<40,112,114,111,106,101,99,116,105,111,110,115,41,10>>,
|
|
||||||
ok_response(Body, sx);
|
|
||||||
projections_list_response_for(cbor) ->
|
|
||||||
[_, _, {body, Body}] = projections_list_response(),
|
|
||||||
ok_response(Body, cbor);
|
|
||||||
projections_list_response_for(_) ->
|
|
||||||
projections_list_response().
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
-module(log).
|
|
||||||
-export([open/2, append/2, tip/1, replay/3, entries/1]).
|
|
||||||
|
|
||||||
%% Per-actor activity log — the canonical record of everything an
|
|
||||||
%% actor has emitted, in chronological order. Per design §15.2 this
|
|
||||||
%% lives on disk as a JSONL segment file; v1 starts with an in-memory
|
|
||||||
%% backend so the API and seq-number machinery can be locked down
|
|
||||||
%% before the on-disk format is added (Step 3b).
|
|
||||||
%%
|
|
||||||
%% State shape (a property list):
|
|
||||||
%% [{actor, ActorId}, {base, BasePath}, {seq, NextSeq}, {entries, [Act|...]}]
|
|
||||||
%%
|
|
||||||
%% `entries` stores activities in append order — i.e. oldest first.
|
|
||||||
%% `seq` is the next sequence number that will be assigned by append.
|
|
||||||
%% `base` is kept on the state for forward-compatibility with 3b
|
|
||||||
%% (where it becomes the segment-file directory).
|
|
||||||
%%
|
|
||||||
%% open/2 takes ActorId + BasePath and returns {ok, LogState} starting
|
|
||||||
%% with seq=0 and no entries.
|
|
||||||
%%
|
|
||||||
%% append/2 returns {ok, NewLogState, AssignedSeq}.
|
|
||||||
%%
|
|
||||||
%% tip/1 returns the next seq the log would assign (== count of entries).
|
|
||||||
%%
|
|
||||||
%% replay/3 folds Fun(Activity, AssignedSeq, Acc) over every entry in
|
|
||||||
%% append order. Three-arity rather than two-arity because the plan's
|
|
||||||
%% example test is "sequence numbers gap-free across replay" — having
|
|
||||||
%% the seq number visible in the fold makes that test direct.
|
|
||||||
%%
|
|
||||||
%% entries/1 is a debug accessor returning [Activity, ...] in append
|
|
||||||
%% order. Not part of the public API contract.
|
|
||||||
|
|
||||||
open(ActorId, BasePath) ->
|
|
||||||
{ok, [{actor, ActorId}, {base, BasePath}, {seq, 0}, {entries, []}]}.
|
|
||||||
|
|
||||||
append(LogState, Activity) ->
|
|
||||||
Seq = field(seq, LogState),
|
|
||||||
Entries = field(entries, LogState),
|
|
||||||
NewState = replace_field(seq, Seq + 1,
|
|
||||||
replace_field(entries, Entries ++ [Activity], LogState)),
|
|
||||||
{ok, NewState, Seq}.
|
|
||||||
|
|
||||||
tip(LogState) ->
|
|
||||||
field(seq, LogState).
|
|
||||||
|
|
||||||
replay(LogState, InitAcc, Fun) ->
|
|
||||||
Entries = field(entries, LogState),
|
|
||||||
replay_loop(Entries, 0, InitAcc, Fun).
|
|
||||||
|
|
||||||
replay_loop([], _, Acc, _) -> Acc;
|
|
||||||
replay_loop([Act | Rest], Seq, Acc, Fun) ->
|
|
||||||
replay_loop(Rest, Seq + 1, Fun(Act, Seq, Acc), Fun).
|
|
||||||
|
|
||||||
entries(LogState) ->
|
|
||||||
field(entries, LogState).
|
|
||||||
|
|
||||||
field(K, [{K, V} | _]) -> V;
|
|
||||||
field(K, [_ | Rest]) -> field(K, Rest);
|
|
||||||
field(_, []) -> erlang:error(badkey).
|
|
||||||
|
|
||||||
replace_field(K, V, []) -> [{K, V}];
|
|
||||||
replace_field(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
|
||||||
replace_field(K, V, [P | Rest]) -> [P | replace_field(K, V, Rest)].
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
-module(nx_cid).
|
|
||||||
-export([from_sx/1, to_string/1, from_string/1, equals/2]).
|
|
||||||
|
|
||||||
%% The kernel-side CID wrapper. The host BIF `cid:to_string/1` already
|
|
||||||
%% produces a canonical CIDv1 (raw codec, sha2-256 multihash) over the
|
|
||||||
%% deterministic textual form of any term (er-format-value); we expose
|
|
||||||
%% it under the kernel namespace and add the equality + round-trip
|
|
||||||
%% helpers the rest of the kernel needs.
|
|
||||||
%%
|
|
||||||
%% Naming note: the BIF module is `cid`, so we use `nx_cid` to avoid
|
|
||||||
%% shadowing. Plans/fed-sx-milestone-1.md §Step 1 spells the file as
|
|
||||||
%% `cid.erl`; the briefing flags Erlang snippets as illustrative.
|
|
||||||
|
|
||||||
from_sx(V) ->
|
|
||||||
cid:to_string(V).
|
|
||||||
|
|
||||||
to_string(Cid) ->
|
|
||||||
Cid.
|
|
||||||
|
|
||||||
from_string(S) ->
|
|
||||||
S.
|
|
||||||
|
|
||||||
equals(A, B) ->
|
|
||||||
A =:= B.
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
-module(nx_kernel).
|
|
||||||
-behaviour(gen_server).
|
|
||||||
-export([new/3, publish/2,
|
|
||||||
actor_id/1, log_state/1, log_tip/1,
|
|
||||||
key_spec/1, actor_state/1, projections/1,
|
|
||||||
next_published/1, with_projections/2]).
|
|
||||||
-export([start_link/3, publish/1, query/0, log_tip/0,
|
|
||||||
with_projections/1, stop/0]).
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
|
||||||
|
|
||||||
%% Kernel orchestrator — the long-lived runtime state held by the
|
|
||||||
%% running fed-sx instance. The HTTP layer (Step 8c-post-publish
|
|
||||||
%% follow-up) will park this in a gen_server and dispatch the POST
|
|
||||||
%% /activity request through `publish/2`.
|
|
||||||
%%
|
|
||||||
%% State shape (property list):
|
|
||||||
%% [{actor_id, A},
|
|
||||||
%% {key_spec, KS}, % proplist: key_id / algorithm / value
|
|
||||||
%% {actor_state, AS}, % proplist: public_keys
|
|
||||||
%% {log, L}, % log:open/2 return value
|
|
||||||
%% {projections, [Name]}, % list of registered projection process names
|
|
||||||
%% {next_published, N}] % monotonic counter we feed as :published
|
|
||||||
%%
|
|
||||||
%% Step 6c's stage_replay catches duplicates by `:id`; the `:id`
|
|
||||||
%% is derived from the unsigned envelope contents. Same Request +
|
|
||||||
%% same `:published` -> same CID, so the next_published counter
|
|
||||||
%% gives every publish a distinct timestamp without needing a
|
|
||||||
%% wall-clock BIF.
|
|
||||||
|
|
||||||
new(ActorId, KeySpec, ActorStateProplist) ->
|
|
||||||
{ok, L0} = log:open(ActorId, base_stub()),
|
|
||||||
[{actor_id, ActorId},
|
|
||||||
{key_spec, KeySpec},
|
|
||||||
{actor_state, ActorStateProplist},
|
|
||||||
{log, L0},
|
|
||||||
{projections, []},
|
|
||||||
{next_published, 1}].
|
|
||||||
|
|
||||||
%% publish/2 — pure state transition. Returns either:
|
|
||||||
%% {ok, Result, NewState} — log + counter advanced
|
|
||||||
%% {error, Reason, State} — state unchanged on validation halt
|
|
||||||
publish(Request, State) ->
|
|
||||||
P = field(next_published, State),
|
|
||||||
Ctx = [{actor_id, field(actor_id, State)},
|
|
||||||
{published, P},
|
|
||||||
{key_spec, field(key_spec, State)},
|
|
||||||
{actor_state, field(actor_state, State)},
|
|
||||||
{log, field(log, State)},
|
|
||||||
{projections, field(projections, State)}],
|
|
||||||
case outbox:publish(Request, Ctx) of
|
|
||||||
{ok, Result, NewLog} ->
|
|
||||||
State1 = set(log, NewLog, State),
|
|
||||||
State2 = set(next_published, P + 1, State1),
|
|
||||||
{ok, Result, State2};
|
|
||||||
{error, Reason, _} ->
|
|
||||||
{error, Reason, State}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Accessors
|
|
||||||
|
|
||||||
actor_id(State) -> field(actor_id, State).
|
|
||||||
key_spec(State) -> field(key_spec, State).
|
|
||||||
actor_state(State) -> field(actor_state, State).
|
|
||||||
log_state(State) -> field(log, State).
|
|
||||||
log_tip(State) -> log:tip(field(log, State)).
|
|
||||||
projections(State) -> field(projections, State).
|
|
||||||
next_published(State) -> field(next_published, State).
|
|
||||||
|
|
||||||
%% with_projections — return a new state with :projections replaced.
|
|
||||||
with_projections(Names, State) ->
|
|
||||||
set(projections, Names, State).
|
|
||||||
|
|
||||||
%% Internal
|
|
||||||
|
|
||||||
%% "base_stub" — placeholder base path for the in-memory log
|
|
||||||
%% in v1 (the in-memory log ignores the base argument).
|
|
||||||
base_stub() ->
|
|
||||||
<<98,97,115,101,95,115,116,117,98>>.
|
|
||||||
|
|
||||||
field(K, [{K, V} | _]) -> V;
|
|
||||||
field(K, [_ | Rest]) -> field(K, Rest);
|
|
||||||
field(_, []) -> nil.
|
|
||||||
|
|
||||||
set(K, V, []) -> [{K, V}];
|
|
||||||
set(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
|
||||||
set(K, V, [P | Rest]) -> [P | set(K, V, Rest)].
|
|
||||||
|
|
||||||
%% ── gen_server wrapper ──────────────────────────────────────────
|
|
||||||
%%
|
|
||||||
%% Mirrors the registry / projection gen_server patterns from
|
|
||||||
%% Steps 5b and 7b. Same port quirks: raw Pid return, no `?MODULE`
|
|
||||||
%% macro, spawned processes don't persist across separate
|
|
||||||
%% erlang-eval-ast calls — tests inline start_link with operations.
|
|
||||||
|
|
||||||
start_link(ActorId, KeySpec, ActorStateProplist) ->
|
|
||||||
Pid = gen_server:start_link(nx_kernel,
|
|
||||||
[ActorId, KeySpec, ActorStateProplist]),
|
|
||||||
erlang:register(nx_kernel, Pid),
|
|
||||||
Pid.
|
|
||||||
|
|
||||||
stop() ->
|
|
||||||
R = gen_server:call(nx_kernel, '$gen_stop'),
|
|
||||||
erlang:unregister(nx_kernel),
|
|
||||||
R.
|
|
||||||
|
|
||||||
publish(Request) ->
|
|
||||||
gen_server:call(nx_kernel, {publish, Request}).
|
|
||||||
|
|
||||||
query() ->
|
|
||||||
gen_server:call(nx_kernel, get_state).
|
|
||||||
|
|
||||||
log_tip() ->
|
|
||||||
gen_server:call(nx_kernel, get_log_tip).
|
|
||||||
|
|
||||||
with_projections(Names) ->
|
|
||||||
gen_server:call(nx_kernel, {set_projections, Names}).
|
|
||||||
|
|
||||||
%% gen_server callbacks
|
|
||||||
|
|
||||||
init([ActorId, KeySpec, AS]) ->
|
|
||||||
{ok, new(ActorId, KeySpec, AS)}.
|
|
||||||
|
|
||||||
handle_call({publish, Request}, _From, State) ->
|
|
||||||
case publish(Request, State) of
|
|
||||||
{ok, Result, NewState} ->
|
|
||||||
{reply, {ok, Result}, NewState};
|
|
||||||
{error, Reason, SameState} ->
|
|
||||||
{reply, {error, Reason}, SameState}
|
|
||||||
end;
|
|
||||||
handle_call(get_state, _From, State) ->
|
|
||||||
{reply, State, State};
|
|
||||||
handle_call(get_log_tip, _From, State) ->
|
|
||||||
{reply, log_tip(State), State};
|
|
||||||
handle_call({set_projections, Names}, _From, State) ->
|
|
||||||
{reply, ok, with_projections(Names, State)}.
|
|
||||||
|
|
||||||
handle_cast(_, S) -> {noreply, S}.
|
|
||||||
|
|
||||||
handle_info(_, S) -> {noreply, S}.
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
-module(outbox).
|
|
||||||
-export([construct/4, sign/2, cid_of/1, publish/2]).
|
|
||||||
|
|
||||||
%% Outbox envelope construction + signing per design §3.1.
|
|
||||||
%%
|
|
||||||
%% construct/4 builds an unsigned activity envelope from caller-supplied
|
|
||||||
%% (Type, ActorId, Published, Object). The envelope's `:id` field is
|
|
||||||
%% derived from the host `cid:to_string` BIF over a skeleton tag, so
|
|
||||||
%% recipients can address the activity by its content hash. The
|
|
||||||
%% returned property list is the canonical key-sorted form that
|
|
||||||
%% `envelope:canonical_bytes/1` operates on.
|
|
||||||
%%
|
|
||||||
%% sign/2 takes the unsigned envelope plus a KeySpec proplist that
|
|
||||||
%% mirrors a `public_keys` entry: `[{key_id, _}, {algorithm, _},
|
|
||||||
%% {value, KeyMaterial}]`. It computes the v1 HMAC stand-in
|
|
||||||
%% `crypto:hash(sha256, <<KeyMaterial/binary, CanonicalBytes/binary>>)`
|
|
||||||
%% — the same scheme `envelope:verify_signature/2` checks — and
|
|
||||||
%% appends a `:signature` pair.
|
|
||||||
%%
|
|
||||||
%% Real Ed25519 / RSA signing arrives in milestone 2 once
|
|
||||||
%% `crypto:sign_ed25519/2` BIFs land; the API shape doesn't change.
|
|
||||||
|
|
||||||
%% construct/4 — Type and ActorId are atoms; Published is an
|
|
||||||
%% integer timestamp the caller supplies (no clock BIF in this
|
|
||||||
%% port; the HTTP layer / outbox:publish caller injects it).
|
|
||||||
%% Object can be any term, including a property list of inner
|
|
||||||
%% fields.
|
|
||||||
construct(Type, ActorId, Published, Object) ->
|
|
||||||
Skeleton = [{actor, ActorId},
|
|
||||||
{object, Object},
|
|
||||||
{published, Published},
|
|
||||||
{type, Type}],
|
|
||||||
Id = cid:to_string({activity_envelope, Skeleton}),
|
|
||||||
[{actor, ActorId},
|
|
||||||
{id, Id},
|
|
||||||
{object, Object},
|
|
||||||
{published, Published},
|
|
||||||
{type, Type}].
|
|
||||||
|
|
||||||
%% sign/2 — KeySpec carries key_id, algorithm, value (key material).
|
|
||||||
sign(Envelope, KeySpec) ->
|
|
||||||
{ok, KeyId} = envelope:get_field(key_id, KeySpec),
|
|
||||||
{ok, Alg} = envelope:get_field(algorithm, KeySpec),
|
|
||||||
{ok, KM} = envelope:get_field(value, KeySpec),
|
|
||||||
CB = envelope:canonical_bytes(Envelope),
|
|
||||||
SigValue = crypto:hash(sha256, <<KM/binary, CB/binary>>),
|
|
||||||
Sig = [{algorithm, Alg}, {key_id, KeyId}, {value, SigValue}],
|
|
||||||
Envelope ++ [{signature, Sig}].
|
|
||||||
|
|
||||||
%% cid_of/1 — extract the :id field from a constructed envelope.
|
|
||||||
%% Convenience for callers that don't want to thread the CID
|
|
||||||
%% separately when both the envelope and its ID matter.
|
|
||||||
cid_of(Envelope) ->
|
|
||||||
{ok, Id} = envelope:get_field(id, Envelope),
|
|
||||||
Id.
|
|
||||||
|
|
||||||
%% publish/2 — the outbound activity pipeline orchestrator.
|
|
||||||
%%
|
|
||||||
%% Request shape: [{type, T}, {object, O}]
|
|
||||||
%% Context shape: [{actor_id, A}, {published, P}, {key_spec, KS},
|
|
||||||
%% {actor_state, AS}, {log, L}]
|
|
||||||
%%
|
|
||||||
%% Returns:
|
|
||||||
%% {ok, [{cid, Cid}, {activity, Signed}], NewLog} — happy path
|
|
||||||
%% {error, Reason, LogState} — validation halted
|
|
||||||
%%
|
|
||||||
%% Stages run in order: envelope shape, signature, replay. The
|
|
||||||
%% replay check uses the log state pre-append, so if the caller
|
|
||||||
%% publishes the same Request twice with the same Published
|
|
||||||
%% timestamp the second call halts with {error, replay, _}.
|
|
||||||
%%
|
|
||||||
%% Projection-scheduler dispatch (the async fold the design calls
|
|
||||||
%% for) is deferred to Step 7 — once the projection gen_server
|
|
||||||
%% exists, this function will broadcast `Signed` to it.
|
|
||||||
|
|
||||||
publish(Request, Context) ->
|
|
||||||
Type = envelope_field(type, Request),
|
|
||||||
Object = envelope_field(object, Request),
|
|
||||||
ActorId = envelope_field(actor_id, Context),
|
|
||||||
Published = envelope_field(published, Context),
|
|
||||||
KeySpec = envelope_field(key_spec, Context),
|
|
||||||
ActorState = envelope_field(actor_state, Context),
|
|
||||||
LogState = envelope_field(log, Context),
|
|
||||||
Unsigned = construct(Type, ActorId, Published, Object),
|
|
||||||
Signed = sign(Unsigned, KeySpec),
|
|
||||||
Stages = [
|
|
||||||
fun (A) -> pipeline:stage_envelope(A) end,
|
|
||||||
pipeline:stage_signature(ActorState),
|
|
||||||
pipeline:stage_replay(LogState)
|
|
||||||
],
|
|
||||||
case pipeline:run_stages(Signed, Stages) of
|
|
||||||
ok ->
|
|
||||||
{ok, NewLog, _Seq} = log:append(LogState, Signed),
|
|
||||||
broadcast(Signed, envelope_field(projections, Context)),
|
|
||||||
Result = [{cid, cid_of(Signed)}, {activity, Signed}],
|
|
||||||
{ok, Result, NewLog};
|
|
||||||
{error, Reason} ->
|
|
||||||
{error, Reason, LogState}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% broadcast/2 — fire-and-forget cast to each named projection.
|
|
||||||
%% Missing/nil/empty list is a no-op; the publish API does not
|
|
||||||
%% require projections to exist. Activity is the post-sign Signed
|
|
||||||
%% envelope (same value that landed in the log).
|
|
||||||
broadcast(_Activity, nil) -> ok;
|
|
||||||
broadcast(_Activity, []) -> ok;
|
|
||||||
broadcast(Activity, [Name | Rest]) ->
|
|
||||||
projection:async_fold(Name, Activity),
|
|
||||||
broadcast(Activity, Rest).
|
|
||||||
|
|
||||||
envelope_field(K, PL) ->
|
|
||||||
case envelope:get_field(K, PL) of
|
|
||||||
{ok, V} -> V;
|
|
||||||
not_found -> nil
|
|
||||||
end.
|
|
||||||
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
-module(pipeline).
|
|
||||||
-export([run_stages/2,
|
|
||||||
validate_inbound/1, validate_outbound/1,
|
|
||||||
inbound_stages/0, outbound_stages/0,
|
|
||||||
stage_envelope/1,
|
|
||||||
stage_signature/1, stage_signature/2,
|
|
||||||
stage_replay/1, stage_replay/2,
|
|
||||||
stage_schema/1, stage_schema/2]).
|
|
||||||
|
|
||||||
%% Validation pipeline per design §14.
|
|
||||||
%%
|
|
||||||
%% A stage is a 1-arity fun `(Activity) -> ok | {error, Reason}`.
|
|
||||||
%% The driver folds the activity through the stage list, halting
|
|
||||||
%% on the first error. The pure-functional driver itself takes a
|
|
||||||
%% stage list directly so tests can inject ad-hoc stage sequences
|
|
||||||
%% without depending on the bundled inbound/outbound lists.
|
|
||||||
%%
|
|
||||||
%% Inbound pipeline (full set per design §14): envelope, signature,
|
|
||||||
%% replay, audience, activity_schema, object_schema, content_validators,
|
|
||||||
%% capabilities, trust. Outbound is a subset (no replay, no trust;
|
|
||||||
%% auth handled at the HTTP layer).
|
|
||||||
%%
|
|
||||||
%% This sub-deliverable (6a) wires only the driver and the empty
|
|
||||||
%% stage lists. Concrete stages land in 6b-6c.
|
|
||||||
|
|
||||||
run_stages(_Activity, []) -> ok;
|
|
||||||
run_stages(Activity, [Stage | Rest]) ->
|
|
||||||
Result = Stage(Activity),
|
|
||||||
case Result of
|
|
||||||
ok -> run_stages(Activity, Rest);
|
|
||||||
{error, _} -> Result
|
|
||||||
end.
|
|
||||||
|
|
||||||
validate_inbound(Activity) ->
|
|
||||||
run_stages(Activity, inbound_stages()).
|
|
||||||
|
|
||||||
validate_outbound(Activity) ->
|
|
||||||
run_stages(Activity, outbound_stages()).
|
|
||||||
|
|
||||||
inbound_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).
|
|
||||||
|
|
||||||
%% stage_signature/2 — direct (Activity, ActorState) check. Wraps
|
|
||||||
%% envelope:verify_signature/2 from Step 2c. Useful for tests and
|
|
||||||
%% for callers that already have ActorState in scope.
|
|
||||||
stage_signature(Activity, ActorState) ->
|
|
||||||
envelope:verify_signature(Activity, ActorState).
|
|
||||||
|
|
||||||
%% stage_signature/1 — factory: takes the ActorState and returns a
|
|
||||||
%% 1-arity stage fun the pipeline driver can fold. This is how
|
|
||||||
%% signature checking gets composed into a stage list at runtime
|
|
||||||
%% (the static `inbound_stages/0` list omits it precisely because
|
|
||||||
%% ActorState isn't available at static-list build time).
|
|
||||||
stage_signature(ActorState) ->
|
|
||||||
fun (Activity) -> envelope:verify_signature(Activity, ActorState) end.
|
|
||||||
|
|
||||||
%% stage_replay/2 — checks the in-memory log for an existing
|
|
||||||
%% activity with the same :id. Returns ok if the activity is new,
|
|
||||||
%% `{error, replay}` if the log already carries it, `{error, no_id}`
|
|
||||||
%% if the activity has no :id field. The check is linear scan of
|
|
||||||
%% log entries; the projection scheduler (Step 7) will eventually
|
|
||||||
%% maintain a CID index that turns this into O(1).
|
|
||||||
stage_replay(Activity, LogState) ->
|
|
||||||
case envelope:get_field(id, Activity) of
|
|
||||||
not_found -> {error, no_id};
|
|
||||||
{ok, Id} ->
|
|
||||||
case log_has_id(Id, log:entries(LogState)) of
|
|
||||||
true -> {error, replay};
|
|
||||||
false -> ok
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
stage_replay(LogState) ->
|
|
||||||
fun (Activity) -> stage_replay(Activity, LogState) end.
|
|
||||||
|
|
||||||
log_has_id(_, []) -> false;
|
|
||||||
log_has_id(Id, [Act | Rest]) ->
|
|
||||||
case envelope:get_field(id, Act) of
|
|
||||||
{ok, Id} -> true;
|
|
||||||
_ -> log_has_id(Id, Rest)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% stage_schema/2 — validates the activity's :object against the
|
|
||||||
%% schema registered for its :type. SchemaLookup is a caller-
|
|
||||||
%% supplied fun (Type) -> {ok, SchemaFn} | not_found; SchemaFn is
|
|
||||||
%% itself a fun (Object) -> bool. Returns:
|
|
||||||
%% ok when the schema accepts the object
|
|
||||||
%% {error, no_type} when the activity has no :type
|
|
||||||
%% {error, schema_mismatch} when SchemaFn returned false
|
|
||||||
%%
|
|
||||||
%% Open-world default: an unregistered Type returns ok so the
|
|
||||||
%% pipeline doesn't block activities the kernel hasn't yet learned
|
|
||||||
%% about. Tightening to strict-world happens later in milestone 2.
|
|
||||||
%%
|
|
||||||
%% Activities with no :object skip the schema check (some verbs
|
|
||||||
%% legitimately carry no object).
|
|
||||||
%%
|
|
||||||
%% The Erlang-fun shape is the substrate-friendly stand-in for the
|
|
||||||
%% SX-source :schema bodies stored in the genesis bundle. Once an
|
|
||||||
%% SX-source eval bridge exists, the same stage shape will dispatch
|
|
||||||
%% through it instead — no API change.
|
|
||||||
stage_schema(Activity, SchemaLookup) ->
|
|
||||||
case envelope:get_field(type, Activity) of
|
|
||||||
not_found -> {error, no_type};
|
|
||||||
{ok, Type} ->
|
|
||||||
case SchemaLookup(Type) of
|
|
||||||
not_found -> ok;
|
|
||||||
{ok, SchemaFn} ->
|
|
||||||
check_object_schema(Activity, SchemaFn)
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
check_object_schema(Activity, SchemaFn) ->
|
|
||||||
case envelope:get_field(object, Activity) of
|
|
||||||
not_found -> ok;
|
|
||||||
{ok, Obj} ->
|
|
||||||
case SchemaFn(Obj) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, schema_mismatch}
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
stage_schema(SchemaLookup) ->
|
|
||||||
fun (Activity) -> stage_schema(Activity, SchemaLookup) end.
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
-module(projection).
|
|
||||||
-behaviour(gen_server).
|
|
||||||
-export([new/2, new/3, fold_activity/2, replay/2,
|
|
||||||
name/1, state/1, fold_fn/1]).
|
|
||||||
-export([start_link/3, async_fold/2, query/1, stop/1]).
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
|
||||||
|
|
||||||
%% Pure-functional projection driver per design §10.
|
|
||||||
%%
|
|
||||||
%% A projection is a property list:
|
|
||||||
%% [{name, atom}, {state, term}, {fold, fun}]
|
|
||||||
%%
|
|
||||||
%% The fold function is `fun (Activity, State) -> NewState`. v1
|
|
||||||
%% uses Erlang funs as the fold body — the genesis bundle's SX
|
|
||||||
%% `:fold` bodies are stored as binaries; an SX-source eval
|
|
||||||
%% bridge will plug them into the same projection record once
|
|
||||||
%% it lands (Step 7d). For now, callers supply Erlang funs
|
|
||||||
%% directly when constructing a projection.
|
|
||||||
%%
|
|
||||||
%% `replay/2` is the cold-start primitive: fold an activity
|
|
||||||
%% list (e.g. `log:entries/1`) through the projection from its
|
|
||||||
%% initial state.
|
|
||||||
|
|
||||||
new(Name, InitialState) ->
|
|
||||||
new(Name, InitialState, fun (_Activity, S) -> S end).
|
|
||||||
|
|
||||||
new(Name, InitialState, FoldFn) ->
|
|
||||||
[{name, Name}, {state, InitialState}, {fold, FoldFn}].
|
|
||||||
|
|
||||||
fold_activity(Proj, Activity) ->
|
|
||||||
Fn = fold_fn(Proj),
|
|
||||||
S0 = state(Proj),
|
|
||||||
S1 = Fn(Activity, S0),
|
|
||||||
set_field(state, S1, Proj).
|
|
||||||
|
|
||||||
replay(Proj, Activities) ->
|
|
||||||
fold_each(Proj, Activities).
|
|
||||||
|
|
||||||
fold_each(Proj, []) -> Proj;
|
|
||||||
fold_each(Proj, [A | Rest]) ->
|
|
||||||
fold_each(fold_activity(Proj, A), Rest).
|
|
||||||
|
|
||||||
%% Accessors
|
|
||||||
|
|
||||||
name(Proj) -> field(name, Proj).
|
|
||||||
state(Proj) -> field(state, Proj).
|
|
||||||
fold_fn(Proj) -> field(fold, Proj).
|
|
||||||
|
|
||||||
%% Internal
|
|
||||||
|
|
||||||
field(K, [{K, V} | _]) -> V;
|
|
||||||
field(K, [_ | Rest]) -> field(K, Rest);
|
|
||||||
field(_, []) -> erlang:error(badkey).
|
|
||||||
|
|
||||||
set_field(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
|
||||||
set_field(K, V, [P | Rest]) -> [P | set_field(K, V, Rest)];
|
|
||||||
set_field(K, V, []) -> [{K, V}].
|
|
||||||
|
|
||||||
%% ── Step 7b: gen_server wrapper ─────────────────────────────────
|
|
||||||
%%
|
|
||||||
%% Each projection runs in its own gen_server, registered under the
|
|
||||||
%% projection's Name atom. `async_fold/2` casts an activity into the
|
|
||||||
%% process; `query/1` synchronously fetches the current state.
|
|
||||||
%%
|
|
||||||
%% Port notes (mirroring Step 5b on the registry): `gen_server:start_link`
|
|
||||||
%% returns the raw Pid; `?MODULE` macro is unsupported; spawned
|
|
||||||
%% processes don't survive across separate `erlang-eval-ast` calls
|
|
||||||
%% so tests must inline start_link with their operations.
|
|
||||||
|
|
||||||
start_link(Name, InitialState, FoldFn) ->
|
|
||||||
Pid = gen_server:start_link(projection, [Name, InitialState, FoldFn]),
|
|
||||||
erlang:register(Name, Pid),
|
|
||||||
Pid.
|
|
||||||
|
|
||||||
async_fold(Name, Activity) ->
|
|
||||||
gen_server:cast(Name, {fold, Activity}).
|
|
||||||
|
|
||||||
query(Name) ->
|
|
||||||
gen_server:call(Name, get_state).
|
|
||||||
|
|
||||||
stop(Name) ->
|
|
||||||
R = gen_server:call(Name, '$gen_stop'),
|
|
||||||
erlang:unregister(Name),
|
|
||||||
R.
|
|
||||||
|
|
||||||
%% gen_server callbacks
|
|
||||||
|
|
||||||
init([Name, InitialState, FoldFn]) ->
|
|
||||||
{ok, new(Name, InitialState, FoldFn)}.
|
|
||||||
|
|
||||||
handle_call(get_state, _From, Proj) ->
|
|
||||||
{reply, state(Proj), Proj}.
|
|
||||||
|
|
||||||
handle_cast({fold, Activity}, Proj) ->
|
|
||||||
{noreply, fold_activity(Proj, Activity)}.
|
|
||||||
|
|
||||||
handle_info(_, Proj) -> {noreply, Proj}.
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
-module(registry).
|
|
||||||
-behaviour(gen_server).
|
|
||||||
-export([new/0, kinds/0, register/4, lookup/3, list/2]).
|
|
||||||
-export([start_link/0, register/3, lookup/2, list/1, stop/0]).
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
|
||||||
|
|
||||||
%% Pure-functional registry for the seven bootstrap kinds.
|
|
||||||
%%
|
|
||||||
%% State is a property list keyed by kind atom; each kind's value
|
|
||||||
%% is itself a property list of {Name, Entry} pairs. Entry is
|
|
||||||
%% opaque — typically a proplist with :cid, :schema, :semantics,
|
|
||||||
%% :supersedes fields, but the registry doesn't enforce that here.
|
|
||||||
%%
|
|
||||||
%% A gen_server wrapper (Step 5b) will own the global registry
|
|
||||||
%% process; the pure functions in this module remain the canonical
|
|
||||||
%% API and are usable for tests and for offline projection-replay.
|
|
||||||
%%
|
|
||||||
%% Return shapes:
|
|
||||||
%% new/0 -> State
|
|
||||||
%% kinds/0 -> [Atom, ...]
|
|
||||||
%% register/4 -> {ok, NewState} | {error, unknown_kind}
|
|
||||||
%% lookup/3 -> {ok, Entry} | not_found | {error, unknown_kind}
|
|
||||||
%% list/2 -> [{Name, Entry}, ...] | {error, unknown_kind}
|
|
||||||
|
|
||||||
new() -> [].
|
|
||||||
|
|
||||||
kinds() ->
|
|
||||||
[activity_types, object_types, projections,
|
|
||||||
validators, codecs, sig_suites, audience].
|
|
||||||
|
|
||||||
register(Kind, Name, Entry, State) ->
|
|
||||||
case is_valid_kind(Kind) of
|
|
||||||
false -> {error, unknown_kind};
|
|
||||||
true ->
|
|
||||||
Entries = kind_entries(Kind, State),
|
|
||||||
Updated = put_pair(Name, Entry, Entries),
|
|
||||||
{ok, set_kind_entries(Kind, Updated, State)}
|
|
||||||
end.
|
|
||||||
|
|
||||||
lookup(Kind, Name, State) ->
|
|
||||||
case is_valid_kind(Kind) of
|
|
||||||
false -> {error, unknown_kind};
|
|
||||||
true ->
|
|
||||||
find_pair(Name, kind_entries(Kind, State))
|
|
||||||
end.
|
|
||||||
|
|
||||||
list(Kind, State) ->
|
|
||||||
case is_valid_kind(Kind) of
|
|
||||||
false -> {error, unknown_kind};
|
|
||||||
true -> kind_entries(Kind, State)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% ── Internal ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
is_valid_kind(K) -> lists:member(K, kinds()).
|
|
||||||
|
|
||||||
kind_entries(Kind, State) ->
|
|
||||||
case find_pair(Kind, State) of
|
|
||||||
not_found -> [];
|
|
||||||
{ok, V} -> V
|
|
||||||
end.
|
|
||||||
|
|
||||||
set_kind_entries(Kind, Entries, State) ->
|
|
||||||
put_pair(Kind, Entries, State).
|
|
||||||
|
|
||||||
put_pair(K, V, []) -> [{K, V}];
|
|
||||||
put_pair(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
|
||||||
put_pair(K, V, [P | Rest]) -> [P | put_pair(K, V, Rest)].
|
|
||||||
|
|
||||||
find_pair(_, []) -> not_found;
|
|
||||||
find_pair(K, [{K, V} | _]) -> {ok, V};
|
|
||||||
find_pair(K, [_ | Rest]) -> find_pair(K, Rest).
|
|
||||||
|
|
||||||
%% ── Step 5b: gen_server wrapper ─────────────────────────────────
|
|
||||||
%%
|
|
||||||
%% The named process owns the registry state; concurrent readers
|
|
||||||
%% and writers serialize through gen_server:call. The pure /3 and
|
|
||||||
%% /4 functions remain available for offline projection-replay and
|
|
||||||
%% for tests that don't need a process at all.
|
|
||||||
%%
|
|
||||||
%% Port notes: gen_server:start_link returns the raw Pid (not
|
|
||||||
%% `{ok, Pid}` as in OTP). `?MODULE` macro is unsupported here, so
|
|
||||||
%% the registered name is the literal `registry` atom in every call.
|
|
||||||
|
|
||||||
start_link() ->
|
|
||||||
Pid = gen_server:start_link(registry, []),
|
|
||||||
erlang:register(registry, Pid),
|
|
||||||
Pid.
|
|
||||||
|
|
||||||
stop() ->
|
|
||||||
R = gen_server:call(registry, '$gen_stop'),
|
|
||||||
erlang:unregister(registry),
|
|
||||||
R.
|
|
||||||
|
|
||||||
register(Kind, Name, Entry) ->
|
|
||||||
gen_server:call(registry, {register, Kind, Name, Entry}).
|
|
||||||
|
|
||||||
lookup(Kind, Name) ->
|
|
||||||
gen_server:call(registry, {lookup, Kind, Name}).
|
|
||||||
|
|
||||||
list(Kind) ->
|
|
||||||
gen_server:call(registry, {list, Kind}).
|
|
||||||
|
|
||||||
%% gen_server callbacks
|
|
||||||
|
|
||||||
init(_) -> {ok, new()}.
|
|
||||||
|
|
||||||
handle_call({register, Kind, Name, Entry}, _From, State) ->
|
|
||||||
case register(Kind, Name, Entry, State) of
|
|
||||||
{ok, NewState} -> {reply, ok, NewState};
|
|
||||||
{error, R} -> {reply, {error, R}, State}
|
|
||||||
end;
|
|
||||||
handle_call({lookup, Kind, Name}, _From, State) ->
|
|
||||||
{reply, lookup(Kind, Name, State), State};
|
|
||||||
handle_call({list, Kind}, _From, State) ->
|
|
||||||
{reply, list(Kind, State), State}.
|
|
||||||
|
|
||||||
handle_cast(_, S) -> {noreply, S}.
|
|
||||||
|
|
||||||
handle_info(_, S) -> {noreply, S}.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
-module(sandbox).
|
|
||||||
-export([eval_pure/2, eval_pure/3]).
|
|
||||||
|
|
||||||
%% Sandboxed evaluation of an Erlang fun.
|
|
||||||
%%
|
|
||||||
%% eval_pure/2(Fun, Arg) -> {ok, Result} | {error, Reason}
|
|
||||||
%% eval_pure/3(Fun, Arg1, Arg2) -> {ok, Result} | {error, Reason}
|
|
||||||
%%
|
|
||||||
%% The 3-arity variant matches the (Activity, State) -> NewState
|
|
||||||
%% shape of projection folds. The projection scheduler can wrap
|
|
||||||
%% every fold call in `sandbox:eval_pure(Fun, Act, State)` to
|
|
||||||
%% ensure a misbehaving fold body can't crash the projection
|
|
||||||
%% gen_server.
|
|
||||||
%%
|
|
||||||
%% v1 sandboxing is just the try/catch envelope: no gas budget,
|
|
||||||
%% no IO denial, no environment stripping. Real sandboxing lands
|
|
||||||
%% with SX-source eval (the fold body would then be an SX form
|
|
||||||
%% evaluated under the spec/harness platform). The API shape is
|
|
||||||
%% stable — callers don't need to change when that arrives.
|
|
||||||
|
|
||||||
%% Port note: this Erlang implementation catches by explicit
|
|
||||||
%% class names (throw, error, exit) rather than the open
|
|
||||||
%% `Class:Reason` pattern. The wrappers below enumerate the three.
|
|
||||||
|
|
||||||
eval_pure(Fun, Arg) ->
|
|
||||||
try Fun(Arg) of
|
|
||||||
Result -> {ok, Result}
|
|
||||||
catch
|
|
||||||
throw:Reason -> {error, {throw, Reason}};
|
|
||||||
error:Reason -> {error, {error, Reason}};
|
|
||||||
exit:Reason -> {error, {exit, Reason}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
eval_pure(Fun, Arg1, Arg2) ->
|
|
||||||
try Fun(Arg1, Arg2) of
|
|
||||||
Result -> {ok, Result}
|
|
||||||
catch
|
|
||||||
throw:Reason -> {error, {throw, Reason}};
|
|
||||||
error:Reason -> {error, {error, Reason}};
|
|
||||||
exit:Reason -> {error, {exit, Reason}}
|
|
||||||
end.
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/bootstrap_build.sh — Step 4d acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises bootstrap:build_genesis/1, verify_genesis/2,
|
|
||||||
# cidhash_path/1, write_cidhash/2, read_cidhash/1. The bundle CID
|
|
||||||
# is computed by delegating to the host cid:to_string BIF (Step 1b
|
|
||||||
# substrate) over the read_genesis result. 11 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
|
|
||||||
|
|
||||||
# Clean any stale .cidhash from previous runs before tests touch
|
|
||||||
# the filesystem.
|
|
||||||
rm -f next/genesis/.cidhash
|
|
||||||
|
|
||||||
VERBOSE="${1:-}"
|
|
||||||
PASS=0; FAIL=0; ERRORS=""
|
|
||||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE; rm -f next/genesis/.cidhash" 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/bootstrap.erl\")) :name)")
|
|
||||||
|
|
||||||
;; build_genesis returns {ok, [{cid, _}, {sections, _}]}
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, B} = bootstrap:build_genesis(bootstrap:read_genesis()), {Tag, _} = hd(B), Tag\")")
|
|
||||||
|
|
||||||
;; The CID is a non-empty binary
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, [{cid, C}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), is_binary(C)\") :name)")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, [{cid, C}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), byte_size(C) > 50\") :name)")
|
|
||||||
|
|
||||||
;; build_genesis is deterministic across calls
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, [{cid, C1}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), {ok, [{cid, C2}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), C1 =:= C2\") :name)")
|
|
||||||
|
|
||||||
;; build_genesis preserves the sections list
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, [_, {sections, S}]} = bootstrap:build_genesis(bootstrap:read_genesis()), length(S)\")")
|
|
||||||
|
|
||||||
;; build_genesis rejects bad input shapes
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"case bootstrap:build_genesis({error, broken}) of {error, {bad_read_result, _}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; verify_genesis returns ok when CID matches
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, [{cid, C}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), bootstrap:verify_genesis(bootstrap:read_genesis(), C) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; verify_genesis returns {error, {cid_mismatch, _, _}} when CID doesn't match
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"case bootstrap:verify_genesis(bootstrap:read_genesis(), <<99,99,99>>) of {error, {cid_mismatch, _, _}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; cidhash_path concatenation
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:cidhash_path(<<110,101,120,116>>) =:= <<110,101,120,116,47,46,99,105,100,104,97,115,104>>\") :name)")
|
|
||||||
|
|
||||||
;; write_cidhash + read_cidhash round-trip the bundle CID
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, [{cid, C}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), Base = bootstrap:default_base(), ok = bootstrap:write_cidhash(Base, C), {ok, Stored} = bootstrap:read_cidhash(Base), Stored =:= C\") :name)")
|
|
||||||
|
|
||||||
;; Full verify path against the persisted .cidhash
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(get (erlang-eval-ast \"Base = bootstrap:default_base(), {ok, [{cid, C}, _]} = bootstrap:build_genesis(bootstrap:read_genesis()), ok = bootstrap:write_cidhash(Base, C), {ok, Stored} = bootstrap:read_cidhash(Base), bootstrap:verify_genesis(bootstrap:read_genesis(), Stored) =:= ok\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 180 "$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 "module load name" "bootstrap"
|
|
||||||
check 10 "build_genesis head tag" "cid"
|
|
||||||
check 11 "CID is a binary" "true"
|
|
||||||
check 12 "CID length > 50" "true"
|
|
||||||
check 13 "build_genesis deterministic" "true"
|
|
||||||
check 14 "sections preserved (7 entries)" "7"
|
|
||||||
check 15 "build_genesis rejects bad shape" "ok"
|
|
||||||
check 20 "verify_genesis ok when match" "true"
|
|
||||||
check 21 "verify_genesis errs on mismatch" "ok"
|
|
||||||
check 22 "cidhash_path concatenation" "true"
|
|
||||||
check 23 "write/read_cidhash round-trip" "true"
|
|
||||||
check 24 "verify against persisted hash" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/bootstrap_build.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/bootstrap_load.sh — Step 4e acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises bootstrap:load_genesis/1 + strip_sx_suffix/1.
|
|
||||||
# Walks bootstrap:read_genesis output, strips .sx from each
|
|
||||||
# filename, registers raw bytes as entries under the matching
|
|
||||||
# kind. 13 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/registry.erl\")) :name)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/bootstrap.erl\")) :name)")
|
|
||||||
|
|
||||||
;; strip_sx_suffix on "create.sx" -> "create"
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:strip_sx_suffix(<<99,114,101,97,116,101,46,115,120>>) =:= <<99,114,101,97,116,101>>\") :name)")
|
|
||||||
|
|
||||||
;; strip_sx_suffix unchanged on names without .sx
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:strip_sx_suffix(<<104,101,108,108,111>>) =:= <<104,101,108,108,111>>\") :name)")
|
|
||||||
|
|
||||||
;; strip_sx_suffix on exactly ".sx" -> empty binary
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:strip_sx_suffix(<<46,115,120>>) =:= <<>>\") :name)")
|
|
||||||
|
|
||||||
;; load_genesis on bad input rejects with proper tag
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"case bootstrap:load_genesis({error, broken}) of {error, {bad_read_result, _}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Per-kind counts after load match the section file counts
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(activity_types, S))\")")
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(object_types, S))\")")
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(projections, S))\")")
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(validators, S))\")")
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(codecs, S))\")")
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(sig_suites, S))\")")
|
|
||||||
(epoch 26)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), length(registry:list(audience, S))\")")
|
|
||||||
|
|
||||||
;; registry:lookup retrieves a known entry's bytes
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, S} = bootstrap:load_genesis(bootstrap:read_genesis()), case registry:lookup(activity_types, <<99,114,101,97,116,101>>, S) of {ok, B} -> is_binary(B) and (byte_size(B) > 100); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; load_genesis is deterministic — compare via cid:to_string of state
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = bootstrap:read_genesis(), {ok, S1} = bootstrap:load_genesis(R), {ok, S2} = bootstrap:load_genesis(R), cid:to_string(S1) =:= cid:to_string(S2)\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 300 "$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 "registry module loaded" "registry"
|
|
||||||
check 3 "bootstrap module loaded" "bootstrap"
|
|
||||||
check 10 "strip suffix create.sx -> create" "true"
|
|
||||||
check 11 "strip suffix hello unchanged" "true"
|
|
||||||
check 12 "strip suffix .sx -> empty" "true"
|
|
||||||
check 13 "load_genesis rejects bad shape" "ok"
|
|
||||||
check 20 "loaded activity_types count = 3" "3"
|
|
||||||
check 21 "loaded object_types count = 10" "10"
|
|
||||||
check 22 "loaded projections count = 7" "7"
|
|
||||||
check 23 "loaded validators count = 3" "3"
|
|
||||||
check 24 "loaded codecs count = 3" "3"
|
|
||||||
check 25 "loaded sig_suites count = 2" "2"
|
|
||||||
check 26 "loaded audience count = 3" "3"
|
|
||||||
check 30 "registry:lookup activity_types/create" "true"
|
|
||||||
check 31 "load_genesis deterministic" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/bootstrap_load.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/bootstrap_populate.sh — Step 5c-populate acceptance test.
|
|
||||||
#
|
|
||||||
# Closes the bootstrap → registry loop end-to-end. Each test
|
|
||||||
# inlines registry:start_link() with bootstrap:populate_registry()
|
|
||||||
# because spawned processes don't survive separate erlang-eval-ast
|
|
||||||
# invocations. 11 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
|
|
||||||
|
|
||||||
# Shared prelude: starts registry, runs populate.
|
|
||||||
PRELUDE='registry:start_link(), N = bootstrap:populate_registry(),'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/registry.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/bootstrap.erl\")) :name)")
|
|
||||||
|
|
||||||
;; populate returns the total count
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} N\")")
|
|
||||||
|
|
||||||
;; Per-kind counts match the manifest authored in Step 4
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(activity_types))\")")
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(object_types))\")")
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(projections))\")")
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(validators))\")")
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(codecs))\")")
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(sig_suites))\")")
|
|
||||||
(epoch 26)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(audience))\")")
|
|
||||||
|
|
||||||
;; Lookup of a known entry returns its bytes
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} case registry:lookup(activity_types, <<99,114,101,97,116,101>>) of {ok, B} -> is_binary(B) and (byte_size(B) > 100); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; A known object-type entry registered correctly
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} case registry:lookup(object_types, <<100,101,102,105,110,101,45,97,99,116,105,118,105,116,121>>) of {ok, B} -> is_binary(B); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; A known validator entry
|
|
||||||
(epoch 32)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} case registry:lookup(validators, <<101,110,118,101,108,111,112,101,45,115,104,97,112,101>>) of {ok, B} -> is_binary(B); _ -> false end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 300 "$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 "gen_server loaded" "gen_server"
|
|
||||||
check 3 "registry loaded" "registry"
|
|
||||||
check 4 "bootstrap loaded" "bootstrap"
|
|
||||||
check 10 "populate returns total 31" "31"
|
|
||||||
check 20 "activity_types count = 3" "3"
|
|
||||||
check 21 "object_types count = 10" "10"
|
|
||||||
check 22 "projections count = 7" "7"
|
|
||||||
check 23 "validators count = 3" "3"
|
|
||||||
check 24 "codecs count = 3" "3"
|
|
||||||
check 25 "sig_suites count = 2" "2"
|
|
||||||
check 26 "audience count = 3" "3"
|
|
||||||
check 30 "lookup activity_types/create" "true"
|
|
||||||
check 31 "lookup object_types/define-activity" "true"
|
|
||||||
check 32 "lookup validators/envelope-shape" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/bootstrap_populate.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/bootstrap_read.sh — Step 4c acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises bootstrap:read_genesis/0, read_section/2, sections/0,
|
|
||||||
# section_subdir/1, ends_with_sx/1. Verifies per-section file
|
|
||||||
# counts match the manifest authored in Steps 4a/4b. 14 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/bootstrap.erl\")) :name)")
|
|
||||||
|
|
||||||
;; sections/0 returns 7 atoms
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:sections())\")")
|
|
||||||
|
|
||||||
;; ends_with_sx — positive on "create.sx", negative on "hello"
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:ends_with_sx(<<99,114,101,97,116,101,46,115,120>>)\") :name)")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:ends_with_sx(<<104,101,108,108,111>>)\") :name)")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"bootstrap:ends_with_sx(<<>>)\") :name)")
|
|
||||||
|
|
||||||
;; Per-section file counts match the manifest (3/10/7/3/3/2/3)
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), activity_types))\")")
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), object_types))\")")
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), projections))\")")
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), validators))\")")
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), codecs))\")")
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), sig_suites))\")")
|
|
||||||
(epoch 26)
|
|
||||||
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), audience))\")")
|
|
||||||
|
|
||||||
;; read_genesis/0 returns {ok, [{Section, Entries}, ...]} with 7 entries
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, G} = bootstrap:read_genesis(), length(G)\")")
|
|
||||||
|
|
||||||
;; First entry is {activity_types, [_,_,_]}
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, G} = bootstrap:read_genesis(), {S, Entries} = hd(G), S\") :name)")
|
|
||||||
|
|
||||||
;; Each entry has the right number of files
|
|
||||||
(epoch 32)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, G} = bootstrap:read_genesis(), {_, E} = hd(G), length(E)\")")
|
|
||||||
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 "module load name" "bootstrap"
|
|
||||||
check 10 "sections/0 length" "7"
|
|
||||||
check 11 "ends_with_sx create.sx" "true"
|
|
||||||
check 12 "ends_with_sx hello" "false"
|
|
||||||
check 13 "ends_with_sx empty" "false"
|
|
||||||
check 20 "section activity_types count" "3"
|
|
||||||
check 21 "section object_types count" "10"
|
|
||||||
check 22 "section projections count" "7"
|
|
||||||
check 23 "section validators count" "3"
|
|
||||||
check 24 "section codecs count" "3"
|
|
||||||
check 25 "section sig_suites count" "2"
|
|
||||||
check 26 "section audience count" "3"
|
|
||||||
check 30 "read_genesis returns 7 sections" "7"
|
|
||||||
check 31 "first section name" "activity_types"
|
|
||||||
check 32 "first section entry count" "3"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/bootstrap_read.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/bootstrap_start.sh — Step 4f-consolidate test.
|
|
||||||
#
|
|
||||||
# bootstrap:start/3 is the one-call kernel bring-up: starts the
|
|
||||||
# registry gen_server, populates it from the genesis bundle,
|
|
||||||
# and starts the nx_kernel gen_server. Each test inlines the
|
|
||||||
# start call with downstream operations because spawned
|
|
||||||
# processes don't survive across separate erlang-eval-ast calls.
|
|
||||||
# 11 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
|
|
||||||
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], bootstrap:start(alice, KS, AS),'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/registry.erl\")) :name)")
|
|
||||||
(epoch 8)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 9)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/bootstrap.erl\")) :name)")
|
|
||||||
|
|
||||||
;; bootstrap:start returns a Pid
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} is_pid(whereis(nx_kernel))\") :name)")
|
|
||||||
|
|
||||||
;; Registry has 3 activity types after start
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(activity_types))\")")
|
|
||||||
|
|
||||||
;; Registry has 10 object types
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(object_types))\")")
|
|
||||||
|
|
||||||
;; Registry has 7 projections
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} length(registry:list(projections))\")")
|
|
||||||
|
|
||||||
;; Total entries across all kinds = 31
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} L = lists:map(fun (K) -> length(registry:list(K)) end, registry:kinds()), lists:foldl(fun (X, A) -> X + A end, 0, L)\")")
|
|
||||||
|
|
||||||
;; nx_kernel fresh log_tip = 0
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; nx_kernel publish advances log_tip
|
|
||||||
(epoch 26)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish([{type, create}, {object, nil}]), nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; nx_kernel state carries the supplied actor_id
|
|
||||||
(epoch 27)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:actor_id(nx_kernel:query()) =:= alice\") :name)")
|
|
||||||
|
|
||||||
;; Registry lookup works after start (canonical entry: Create)
|
|
||||||
(epoch 28)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} case registry:lookup(activity_types, <<99,114,101,97,116,101>>) of {ok, _} -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 300 "$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 10 "bootstrap module loaded" "bootstrap"
|
|
||||||
check 20 "whereis(nx_kernel) is Pid" "true"
|
|
||||||
check 21 "activity_types count = 3" "3"
|
|
||||||
check 22 "object_types count = 10" "10"
|
|
||||||
check 23 "projections count = 7" "7"
|
|
||||||
check 24 "total entries = 31" "31"
|
|
||||||
check 25 "fresh log_tip = 0" "0"
|
|
||||||
check 26 "publish advances tip to 1" "1"
|
|
||||||
check 27 "actor_id = alice" "true"
|
|
||||||
check 28 "registry has create" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/bootstrap_start.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/cid.sh — Step 1b acceptance test.
|
|
||||||
#
|
|
||||||
# Loads next/kernel/nx_cid.erl into the Erlang-on-SX runtime and checks
|
|
||||||
# the canonical CID contract: determinism, uniqueness, equality, and
|
|
||||||
# to_string/from_string round-trip. 12 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/nx_cid.erl\")) :name)")
|
|
||||||
|
|
||||||
;; from_sx returns a binary
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_binary(nx_cid:from_sx(foo))\") :name)")
|
|
||||||
|
|
||||||
;; from_sx is deterministic on atoms / ints / compound terms
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:from_sx(foo) =:= nx_cid:from_sx(foo)\") :name)")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:from_sx(42) =:= nx_cid:from_sx(42)\") :name)")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:from_sx({a, [1, 2, 3]}) =:= nx_cid:from_sx({a, [1, 2, 3]})\") :name)")
|
|
||||||
|
|
||||||
;; from_sx is collision-resistant on distinct terms
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:from_sx(foo) =/= nx_cid:from_sx(bar)\") :name)")
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:from_sx(1) =/= nx_cid:from_sx(2)\") :name)")
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:from_sx([1, 2]) =/= nx_cid:from_sx([1, 2, 3])\") :name)")
|
|
||||||
|
|
||||||
;; equals/2 is alias for =:=
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:equals(nx_cid:from_sx(foo), nx_cid:from_sx(foo))\") :name)")
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:equals(nx_cid:from_sx(foo), nx_cid:from_sx(bar))\") :name)")
|
|
||||||
|
|
||||||
;; to_string + from_string round-trip
|
|
||||||
(epoch 40)
|
|
||||||
(eval "(get (erlang-eval-ast \"nx_cid:equals(nx_cid:from_string(nx_cid:to_string(nx_cid:from_sx(foo))), nx_cid:from_sx(foo))\") :name)")
|
|
||||||
(epoch 41)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_binary(nx_cid:to_string(nx_cid:from_sx({tuple, 1, 2})))\") :name)")
|
|
||||||
|
|
||||||
;; CIDv1 raw codec sha256 base32 form is around 59 chars; sanity-check length
|
|
||||||
(epoch 50)
|
|
||||||
(eval "(get (erlang-eval-ast \"byte_size(nx_cid:from_sx(hello)) > 50\") :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 "module load name" "nx_cid"
|
|
||||||
check 10 "from_sx returns binary" "true"
|
|
||||||
check 11 "from_sx atom deterministic" "true"
|
|
||||||
check 12 "from_sx int deterministic" "true"
|
|
||||||
check 13 "from_sx compound deterministic" "true"
|
|
||||||
check 20 "from_sx atoms distinct" "true"
|
|
||||||
check 21 "from_sx ints distinct" "true"
|
|
||||||
check 22 "from_sx lists distinct" "true"
|
|
||||||
check 30 "equals same CIDs" "true"
|
|
||||||
check 31 "equals different CIDs" "false"
|
|
||||||
check 40 "to_string/from_string round-trip" "true"
|
|
||||||
check 41 "to_string returns binary" "true"
|
|
||||||
check 50 "CIDv1 base32 length sanity" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/cid.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/define_registry_pure.sh — Step 5d-pure test.
|
|
||||||
#
|
|
||||||
# Exercises the Erlang-fun stand-in for the define-registry
|
|
||||||
# projection fold. Activities flow: Create{Define*{...}} ->
|
|
||||||
# registry:register/4 keyed by define_kind/1. 14 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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/registry.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/define_registry.erl\")) :name)")
|
|
||||||
|
|
||||||
;; define_kind covers all seven kinds
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_activity) =:= activity_types\") :name)")
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_object) =:= object_types\") :name)")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_projection) =:= projections\") :name)")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_validator) =:= validators\") :name)")
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_codec) =:= codecs\") :name)")
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_sig_suite) =:= sig_suites\") :name)")
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(define_audience) =:= audience\") :name)")
|
|
||||||
|
|
||||||
;; Unknown type returns not_a_define
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:define_kind(some_other_type) =:= not_a_define\") :name)")
|
|
||||||
|
|
||||||
;; Non-Create activity is a pass-through
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:fold([{type, update}, {object, [{type, define_activity}, {name, pin}]}], registry:new()) =:= registry:new()\") :name)")
|
|
||||||
|
|
||||||
;; Create{non-Define} is a pass-through
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:fold([{type, create}, {object, [{type, note}, {name, x}]}], registry:new()) =:= registry:new()\") :name)")
|
|
||||||
|
|
||||||
;; Create{Define*} without :name is a pass-through (preserves State)
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"define_registry:fold([{type, create}, {object, [{type, define_activity}]}], registry:new()) =:= registry:new()\") :name)")
|
|
||||||
|
|
||||||
;; Happy path: Create{DefineActivity{name: pin}} registers under activity_types
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(get (erlang-eval-ast \"Act = [{type, create}, {object, [{type, define_activity}, {name, pin}]}], S = define_registry:fold(Act, registry:new()), {ok, _} = registry:lookup(activity_types, pin, S), ok\") :name)")
|
|
||||||
|
|
||||||
;; Multi-fold accumulates across kinds
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(get (erlang-eval-ast \"A1 = [{type, create}, {object, [{type, define_activity}, {name, pin}]}], A2 = [{type, create}, {object, [{type, define_object}, {name, pin_spec}]}], A3 = [{type, create}, {object, [{type, define_projection}, {name, pin_state}]}], S = define_registry:fold(A3, define_registry:fold(A2, define_registry:fold(A1, registry:new()))), {length(registry:list(activity_types, S)), length(registry:list(object_types, S)), length(registry:list(projections, S))} =:= {1, 1, 1}\") :name)")
|
|
||||||
|
|
||||||
;; Override: re-defining same name does not duplicate entry
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(get (erlang-eval-ast \"A1 = [{type, create}, {object, [{type, define_activity}, {name, pin}, {v, 1}]}], A2 = [{type, create}, {object, [{type, define_activity}, {name, pin}, {v, 2}]}], S = define_registry:fold(A2, define_registry:fold(A1, registry:new())), case registry:lookup(activity_types, pin, S) of {ok, Entry} -> (length(registry:list(activity_types, S)) =:= 1) and (envelope:get_field(v, Entry) =:= {ok, 2}); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; Integration with the projection driver: define_registry as fold_fn
|
|
||||||
(epoch 26)
|
|
||||||
(eval "(get (erlang-eval-ast \"projection:start_link(dr, registry:new(), define_registry:fold_fn()), projection:async_fold(dr, [{type, create}, {object, [{type, define_activity}, {name, pin}]}]), S = projection:query(dr), case registry:lookup(activity_types, pin, S) of {ok, _} -> 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 6 "define_registry module loaded" "define_registry"
|
|
||||||
check 10 "kind: define_activity" "true"
|
|
||||||
check 11 "kind: define_object" "true"
|
|
||||||
check 12 "kind: define_projection" "true"
|
|
||||||
check 13 "kind: define_validator" "true"
|
|
||||||
check 14 "kind: define_codec" "true"
|
|
||||||
check 15 "kind: define_sig_suite" "true"
|
|
||||||
check 16 "kind: define_audience" "true"
|
|
||||||
check 17 "kind: other -> not_a_define" "true"
|
|
||||||
check 20 "non-Create -> pass-through" "true"
|
|
||||||
check 21 "Create{non-Define} pass-through" "true"
|
|
||||||
check 22 "Define{} without :name no-op" "true"
|
|
||||||
check 23 "Create{DefineActivity} registers" "ok"
|
|
||||||
check 24 "multi-fold accumulates" "true"
|
|
||||||
check 25 "override preserves single entry" "true"
|
|
||||||
check 26 "projection integration" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/define_registry_pure.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/envelope_canonical.sh — Step 2b acceptance test.
|
|
||||||
#
|
|
||||||
# Loads next/kernel/envelope.erl and checks canonical_bytes/1 contract:
|
|
||||||
# returns a binary, deterministic across runs, invariant under
|
|
||||||
# field-order permutation, invariant under signature changes, and
|
|
||||||
# different for different covered content. 7 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)")
|
|
||||||
|
|
||||||
;; canonical_bytes returns a binary
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_binary(envelope:canonical_bytes([{id,1},{type,create},{actor,alice},{published,1000},{signature,whatever}]))\") :name)")
|
|
||||||
|
|
||||||
;; Determinism: same envelope twice -> same bytes
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:canonical_bytes([{id,1},{type,create},{actor,alice}]) =:= envelope:canonical_bytes([{id,1},{type,create},{actor,alice}])\") :name)")
|
|
||||||
|
|
||||||
;; Signature stripping: different signatures -> same canonical bytes
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:canonical_bytes([{id,1},{type,create},{actor,alice},{signature,sig_one}]) =:= envelope:canonical_bytes([{id,1},{type,create},{actor,alice},{signature,sig_two}])\") :name)")
|
|
||||||
|
|
||||||
;; No signature vs some signature -> same canonical bytes
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:canonical_bytes([{id,1},{type,create},{actor,alice}]) =:= envelope:canonical_bytes([{id,1},{type,create},{actor,alice},{signature,whatever}])\") :name)")
|
|
||||||
|
|
||||||
;; Key-order invariance: reordering top-level fields -> same bytes
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:canonical_bytes([{id,1},{type,create},{actor,alice}]) =:= envelope:canonical_bytes([{actor,alice},{type,create},{id,1}])\") :name)")
|
|
||||||
|
|
||||||
;; Changing a covered field changes the bytes
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:canonical_bytes([{id,1},{type,create},{actor,alice}]) =/= envelope:canonical_bytes([{id,2},{type,create},{actor,alice}])\") :name)")
|
|
||||||
|
|
||||||
;; Distinct envelopes -> distinct bytes
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:canonical_bytes([{id,1},{type,create},{actor,alice}]) =/= envelope:canonical_bytes([{id,1},{type,update},{actor,bob}])\") :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 "module load name" "envelope"
|
|
||||||
check 10 "canonical_bytes returns binary" "true"
|
|
||||||
check 11 "deterministic" "true"
|
|
||||||
check 12 "signature stripped (changes)" "true"
|
|
||||||
check 13 "signature stripped (absent)" "true"
|
|
||||||
check 14 "key-order invariant" "true"
|
|
||||||
check 15 "covered field change visible" "true"
|
|
||||||
check 16 "distinct envelopes distinct" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/envelope_canonical.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/envelope_shape.sh — Step 2a acceptance test.
|
|
||||||
#
|
|
||||||
# Loads next/kernel/envelope.erl into the Erlang-on-SX runtime and
|
|
||||||
# checks validate_shape/1 / get_field/2 against the design §3.1 shape
|
|
||||||
# contract. 13 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)")
|
|
||||||
|
|
||||||
;; Reusable valid envelope as Erlang text. The signature itself is a
|
|
||||||
;; property list with key_id, algorithm, value.
|
|
||||||
;; E0 = [{id,1},{type,create},{actor,alice},{published,1000},
|
|
||||||
;; {signature,[{key_id,k1},{algorithm,ed25519},{value,v}]}]
|
|
||||||
|
|
||||||
;; Complete valid envelope
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{published,1000},{signature,[{key_id,k1},{algorithm,ed25519},{value,v}]}]) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Missing each top-level required field
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{type,create},{actor,alice},{published,1000},{signature,[{key_id,k1},{algorithm,ed25519},{value,v}]}]) =:= {error,{missing_field,id}}\") :name)")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{actor,alice},{published,1000},{signature,[{key_id,k1},{algorithm,ed25519},{value,v}]}]) =:= {error,{missing_field,type}}\") :name)")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{published,1000},{signature,[{key_id,k1},{algorithm,ed25519},{value,v}]}]) =:= {error,{missing_field,actor}}\") :name)")
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{signature,[{key_id,k1},{algorithm,ed25519},{value,v}]}]) =:= {error,{missing_field,published}}\") :name)")
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{published,1000}]) =:= {error,{missing_field,signature}}\") :name)")
|
|
||||||
|
|
||||||
;; Non-list inputs
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape(42) =:= {error,not_a_proplist}\") :name)")
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape(some_atom) =:= {error,not_a_proplist}\") :name)")
|
|
||||||
|
|
||||||
;; Signature sub-shape
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{published,1000},{signature,[{algorithm,ed25519},{value,v}]}]) =:= {error,{bad_signature,{missing_field,key_id}}}\") :name)")
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{published,1000},{signature,[{key_id,k1},{value,v}]}]) =:= {error,{bad_signature,{missing_field,algorithm}}}\") :name)")
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{published,1000},{signature,[{key_id,k1},{algorithm,ed25519}]}]) =:= {error,{bad_signature,{missing_field,value}}}\") :name)")
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:validate_shape([{id,1},{type,create},{actor,alice},{published,1000},{signature,not_a_proplist}]) =:= {error,{bad_signature,not_a_proplist}}\") :name)")
|
|
||||||
|
|
||||||
;; get_field
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:get_field(actor,[{id,1},{actor,alice}]) =:= {ok,alice}\") :name)")
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (erlang-eval-ast \"envelope:get_field(missing,[{id,1},{actor,alice}]) =:= not_found\") :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 "module load name" "envelope"
|
|
||||||
check 10 "complete envelope -> ok" "true"
|
|
||||||
check 11 "missing id" "true"
|
|
||||||
check 12 "missing type" "true"
|
|
||||||
check 13 "missing actor" "true"
|
|
||||||
check 14 "missing published" "true"
|
|
||||||
check 15 "missing signature" "true"
|
|
||||||
check 16 "non-list (integer)" "true"
|
|
||||||
check 17 "non-list (atom)" "true"
|
|
||||||
check 20 "signature missing key_id" "true"
|
|
||||||
check 21 "signature missing algorithm" "true"
|
|
||||||
check 22 "signature missing value" "true"
|
|
||||||
check 23 "signature not a proplist" "true"
|
|
||||||
check 30 "get_field hit" "true"
|
|
||||||
check 31 "get_field miss" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/envelope_shape.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/envelope_sig.sh — Step 2c acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises envelope:verify_signature/2 against the full sig pipeline:
|
|
||||||
# canonical_bytes + crypto:hash MAC + time-aware key validity per design
|
|
||||||
# §9.6. 10 cases.
|
|
||||||
#
|
|
||||||
# The signature stand-in is HMAC-shaped:
|
|
||||||
# sig.value = crypto:hash(sha256, <<KeyMaterial/binary, CanonicalBytes/binary>>)
|
|
||||||
# Real Ed25519/RSA verification is deferred to milestone 2 once the
|
|
||||||
# corresponding crypto BIFs are wired.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Shared Erlang prelude builds a valid-signed envelope template and an
|
|
||||||
# actor state with one active key. Each test reuses these and asserts
|
|
||||||
# against an Erlang =:= comparison so the result is a bare boolean.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, U = [{actor,alice},{id,1},{published,100},{type,create}], CB = envelope:canonical_bytes(U), Sig = crypto:hash(sha256, <<KM/binary, CB/binary>>), Env = [{actor,alice},{id,1},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,Sig}]}], AS = [{public_keys, [[{id,k1},{created,50},{value,KM}]]}],'
|
|
||||||
|
|
||||||
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)")
|
|
||||||
|
|
||||||
;; valid sig + active key -> ok
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} envelope:verify_signature(Env, AS) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; tampered envelope (id mutated post-sign) -> bad_signature
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Tampered = [{actor,alice},{id,999},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,Sig}]}], envelope:verify_signature(Tampered, AS) =:= {error,bad_signature}\") :name)")
|
|
||||||
|
|
||||||
;; wrong sig value (random bytes) -> bad_signature
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} BadEnv = [{actor,alice},{id,1},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,<<0,0,0,0>>}]}], envelope:verify_signature(BadEnv, AS) =:= {error,bad_signature}\") :name)")
|
|
||||||
|
|
||||||
;; unknown key_id -> no_active_key
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} OtherAS = [{public_keys, [[{id,k_other},{created,50},{value,KM}]]}], envelope:verify_signature(Env, OtherAS) =:= {error,no_active_key}\") :name)")
|
|
||||||
|
|
||||||
;; key superseded BEFORE published -> no_active_key
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} SupAS = [{public_keys, [[{id,k1},{created,50},{superseded_at,80},{value,KM}]]}], envelope:verify_signature(Env, SupAS) =:= {error,no_active_key}\") :name)")
|
|
||||||
|
|
||||||
;; key superseded AFTER published -> ok (historical valid)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} SupAS2 = [{public_keys, [[{id,k1},{created,50},{superseded_at,200},{value,KM}]]}], envelope:verify_signature(Env, SupAS2) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; key not yet created at published -> no_active_key
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} FutAS = [{public_keys, [[{id,k1},{created,150},{value,KM}]]}], envelope:verify_signature(Env, FutAS) =:= {error,no_active_key}\") :name)")
|
|
||||||
|
|
||||||
;; missing signature field -> no_signature
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} envelope:verify_signature(U, AS) =:= {error,no_signature}\") :name)")
|
|
||||||
|
|
||||||
;; actor state with no public_keys field -> no_keys
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} envelope:verify_signature(Env, []) =:= {error,no_keys}\") :name)")
|
|
||||||
|
|
||||||
;; second key in list matches when first doesn't (lookup walks list)
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} TwoKeys = [{public_keys, [[{id,k_other},{created,50},{value,<<9,9,9>>}], [{id,k1},{created,50},{value,KM}]]}], envelope:verify_signature(Env, TwoKeys) =:= ok\") :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 "module load name" "envelope"
|
|
||||||
check 10 "valid sig active key" "true"
|
|
||||||
check 11 "tampered envelope" "true"
|
|
||||||
check 12 "wrong sig value" "true"
|
|
||||||
check 13 "unknown key_id" "true"
|
|
||||||
check 14 "key superseded before published" "true"
|
|
||||||
check 15 "key superseded after published" "true"
|
|
||||||
check 16 "key not yet created" "true"
|
|
||||||
check 17 "missing signature field" "true"
|
|
||||||
check 18 "actor state no keys" "true"
|
|
||||||
check 19 "match second key in list" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/envelope_sig.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/genesis_parse.sh — Step 4a acceptance test.
|
|
||||||
#
|
|
||||||
# Confirms the seed genesis SX files parse cleanly and have the
|
|
||||||
# expected top-level head form. The bundler (Step 4c+) consumes
|
|
||||||
# these forms directly as data. 50 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 10)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/manifest.sx\")))")
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/activity-types/create.sx\")))")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(first (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :activity-types))")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/activity-types/create.sx\")))) :name)")
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :version)")
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/activity-types/update.sx\")))")
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/activity-types/update.sx\")))) :name)")
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/activity-types/delete.sx\")))")
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/activity-types/delete.sx\")))) :name)")
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :activity-types))")
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/object-types/sx-artifact.sx\")))")
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/sx-artifact.sx\")))) :name)")
|
|
||||||
(epoch 32)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/note.sx\")))) :name)")
|
|
||||||
(epoch 33)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/tombstone.sx\")))) :name)")
|
|
||||||
(epoch 34)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-activity.sx\")))) :name)")
|
|
||||||
(epoch 35)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-object.sx\")))) :name)")
|
|
||||||
(epoch 36)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-projection.sx\")))) :name)")
|
|
||||||
(epoch 37)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-validator.sx\")))) :name)")
|
|
||||||
(epoch 38)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-codec.sx\")))) :name)")
|
|
||||||
(epoch 39)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-sig-suite.sx\")))) :name)")
|
|
||||||
(epoch 40)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/snapshot.sx\")))) :name)")
|
|
||||||
(epoch 41)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :object-types))")
|
|
||||||
(epoch 50)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/projections/activity-log.sx\")))")
|
|
||||||
(epoch 51)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/activity-log.sx\")))) :name)")
|
|
||||||
(epoch 52)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/by-type.sx\")))) :name)")
|
|
||||||
(epoch 53)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/by-actor.sx\")))) :name)")
|
|
||||||
(epoch 54)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/by-object.sx\")))) :name)")
|
|
||||||
(epoch 55)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/actor-state.sx\")))) :name)")
|
|
||||||
(epoch 56)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/define-registry.sx\")))) :name)")
|
|
||||||
(epoch 57)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/audience-graph.sx\")))) :name)")
|
|
||||||
(epoch 58)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :projections))")
|
|
||||||
(epoch 60)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/validators/envelope-shape.sx\")))")
|
|
||||||
(epoch 61)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/validators/envelope-shape.sx\")))) :name)")
|
|
||||||
(epoch 62)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/validators/signature.sx\")))) :name)")
|
|
||||||
(epoch 63)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/validators/type-schema.sx\")))) :name)")
|
|
||||||
(epoch 64)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :validators))")
|
|
||||||
(epoch 70)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/codecs/dag-cbor.sx\")))")
|
|
||||||
(epoch 71)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/codecs/dag-cbor.sx\")))) :name)")
|
|
||||||
(epoch 72)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/codecs/raw.sx\")))) :name)")
|
|
||||||
(epoch 73)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/codecs/dag-json.sx\")))) :name)")
|
|
||||||
(epoch 74)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :codecs))")
|
|
||||||
(epoch 80)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/sig-suites/rsa-sha256-2018.sx\")))")
|
|
||||||
(epoch 81)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/sig-suites/rsa-sha256-2018.sx\")))) :name)")
|
|
||||||
(epoch 82)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/sig-suites/ed25519-2020.sx\")))) :name)")
|
|
||||||
(epoch 83)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :sig-suites))")
|
|
||||||
(epoch 90)
|
|
||||||
(eval "(first (parse (file-read \"next/genesis/audience/public.sx\")))")
|
|
||||||
(epoch 91)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/audience/public.sx\")))) :name)")
|
|
||||||
(epoch 92)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/audience/followers.sx\")))) :name)")
|
|
||||||
(epoch 93)
|
|
||||||
(eval "(get (apply dict (rest (parse (file-read \"next/genesis/audience/direct.sx\")))) :name)")
|
|
||||||
(epoch 94)
|
|
||||||
(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :audience))")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 30 "$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 10 "manifest.sx head form" "GenesisManifest"
|
|
||||||
check 11 "create.sx head form" "DefineActivity"
|
|
||||||
check 12 "manifest lists create.sx" "activity-types/create.sx"
|
|
||||||
check 13 "create.sx name is Create" "Create"
|
|
||||||
check 14 "manifest version present" "0.0.1"
|
|
||||||
check 15 "update.sx head form" "DefineActivity"
|
|
||||||
check 16 "update.sx name is Update" "Update"
|
|
||||||
check 17 "delete.sx head form" "DefineActivity"
|
|
||||||
check 18 "delete.sx name is Delete" "Delete"
|
|
||||||
check 19 "manifest has 3 activity-types" "3"
|
|
||||||
check 30 "sx-artifact.sx head form" "DefineObject"
|
|
||||||
check 31 "sx-artifact.sx name" "SXArtifact"
|
|
||||||
check 32 "note.sx name" "Note"
|
|
||||||
check 33 "tombstone.sx name" "Tombstone"
|
|
||||||
check 34 "define-activity.sx name" "DefineActivity"
|
|
||||||
check 35 "define-object.sx name" "DefineObject"
|
|
||||||
check 36 "define-projection.sx name" "DefineProjection"
|
|
||||||
check 37 "define-validator.sx name" "DefineValidator"
|
|
||||||
check 38 "define-codec.sx name" "DefineCodec"
|
|
||||||
check 39 "define-sig-suite.sx name" "DefineSigSuite"
|
|
||||||
check 40 "snapshot.sx name" "Snapshot"
|
|
||||||
check 41 "manifest has 10 object-types" "10"
|
|
||||||
check 50 "activity-log.sx head form" "DefineProjection"
|
|
||||||
check 51 "activity-log.sx name" "activity-log"
|
|
||||||
check 52 "by-type.sx name" "by-type"
|
|
||||||
check 53 "by-actor.sx name" "by-actor"
|
|
||||||
check 54 "by-object.sx name" "by-object"
|
|
||||||
check 55 "actor-state.sx name" "actor-state"
|
|
||||||
check 56 "define-registry.sx name" "define-registry"
|
|
||||||
check 57 "audience-graph.sx name" "audience-graph"
|
|
||||||
check 58 "manifest has 7 projections" "7"
|
|
||||||
check 60 "envelope-shape.sx head form" "DefineValidator"
|
|
||||||
check 61 "envelope-shape.sx name" "envelope-shape"
|
|
||||||
check 62 "signature.sx name" "signature"
|
|
||||||
check 63 "type-schema.sx name" "type-schema"
|
|
||||||
check 64 "manifest has 3 validators" "3"
|
|
||||||
check 70 "dag-cbor.sx head form" "DefineCodec"
|
|
||||||
check 71 "dag-cbor.sx name" "dag-cbor"
|
|
||||||
check 72 "raw.sx name" "raw"
|
|
||||||
check 73 "dag-json.sx name" "dag-json"
|
|
||||||
check 74 "manifest has 3 codecs" "3"
|
|
||||||
check 80 "rsa-sha256-2018.sx head form" "DefineSigSuite"
|
|
||||||
check 81 "rsa-sha256-2018.sx name" "rsa-sha256-2018"
|
|
||||||
check 82 "ed25519-2020.sx name" "ed25519-2020"
|
|
||||||
check 83 "manifest has 2 sig-suites" "2"
|
|
||||||
check 90 "public.sx head form" "DefineAudience"
|
|
||||||
check 91 "public.sx name" "Public"
|
|
||||||
check 92 "followers.sx name" "Followers"
|
|
||||||
check 93 "direct.sx name" "Direct"
|
|
||||||
check 94 "manifest has 3 audience" "3"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/genesis_parse.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_accept.sh — Step 8d-accept acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises accept_format/1 + accept_format_from/1. 12 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; activity_json
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<97,112,112,108,105,99,97,116,105,111,110,47,97,99,116,105,118,105,116,121,43,106,115,111,110>>)\") :name)")
|
|
||||||
|
|
||||||
;; json
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>)\") :name)")
|
|
||||||
|
|
||||||
;; sx
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>)\") :name)")
|
|
||||||
|
|
||||||
;; cbor
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<97,112,112,108,105,99,97,116,105,111,110,47,99,98,111,114>>)\") :name)")
|
|
||||||
|
|
||||||
;; text/plain -> text
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<116,101,120,116,47,112,108,97,105,110>>)\") :name)")
|
|
||||||
|
|
||||||
;; nil -> text
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(nil)\") :name)")
|
|
||||||
|
|
||||||
;; empty binary -> text
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<>>)\") :name)")
|
|
||||||
|
|
||||||
;; activity_json wins over json when both present at the start
|
|
||||||
;; "application/activity+json, application/json"
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<97,112,112,108,105,99,97,116,105,111,110,47,97,99,116,105,118,105,116,121,43,106,115,111,110,44,32,97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>)\") :name)")
|
|
||||||
|
|
||||||
;; accept_format_from with no header field -> text
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format_from([])\") :name)")
|
|
||||||
|
|
||||||
;; accept_format_from with Accept header
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"AK = <<97,99,99,101,112,116>>, AV = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>, http_server:accept_format_from([{headers, [{AK, AV}]}])\") :name)")
|
|
||||||
|
|
||||||
;; accept_format_from with headers but no Accept -> text
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"OK = <<102,111,111>>, http_server:accept_format_from([{headers, [{OK, <<98,97,114>>}]}])\") :name)")
|
|
||||||
|
|
||||||
;; accept_format on a non-binary returns text
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(some_atom)\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "activity+json -> activity_json" "activity_json"
|
|
||||||
check 11 "json -> json" "json"
|
|
||||||
check 12 "sx -> sx" "sx"
|
|
||||||
check 13 "cbor -> cbor" "cbor"
|
|
||||||
check 14 "text/plain -> text" "text"
|
|
||||||
check 15 "nil -> text" "text"
|
|
||||||
check 16 "empty binary -> text" "text"
|
|
||||||
check 17 "activity+json wins over json" "activity_json"
|
|
||||||
check 18 "no headers -> text" "text"
|
|
||||||
check 19 "Accept: application/sx -> sx" "sx"
|
|
||||||
check 20 "no Accept header -> text" "text"
|
|
||||||
check 21 "non-binary input -> text" "text"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_accept.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_actors.sh — Step 8c-actors acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises match_prefix/2 + GET /actors/{id} route. The id is
|
|
||||||
# carried back in the response body so callers can confirm the
|
|
||||||
# right segment was extracted. 12 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; match_prefix on a clean match returns the rest
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:match_prefix(<<97,98>>, <<97,98,99,100>>) =:= {ok, <<99,100>>}\") :name)")
|
|
||||||
|
|
||||||
;; Empty prefix matches everything
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:match_prefix(<<>>, <<97,98,99>>) =:= {ok, <<97,98,99>>}\") :name)")
|
|
||||||
|
|
||||||
;; No common bytes -> nomatch
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:match_prefix(<<97,98>>, <<120,121>>) =:= nomatch\") :name)")
|
|
||||||
|
|
||||||
;; Prefix longer than path -> nomatch
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:match_prefix(<<97,98,99,100>>, <<97,98>>) =:= nomatch\") :name)")
|
|
||||||
|
|
||||||
;; Exact match yields empty rest
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:match_prefix(<<97,98>>, <<97,98>>) =:= {ok, <<>>}\") :name)")
|
|
||||||
|
|
||||||
;; actors_prefix is "/actors/" — 8 bytes
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:actors_prefix())\")")
|
|
||||||
|
|
||||||
;; GET /actors/alice -> 200
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101>>}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; The id appears in the body
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101>>}], R = http_server:route(Req), case R of [_, _, {body, B}] -> http_server:match_prefix(<<97,99,116,111,114,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; GET /actors/ (empty id) -> 404
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; POST /actors/alice -> 404 (only GET)
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; GET /unrelated still 404
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,102,111,111>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Existing routes (GET /, capabilities) still work
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req1 = [{method, <<71,69,84>>}, {path, <<47>>}], Req2 = [{method, <<71,69,84>>}, {path, http_server:capabilities_path()}], R1 = case http_server:route(Req1) of [{status, 200} | _] -> ok; _ -> bad end, R2 = case http_server:route(Req2) of [{status, 200} | _] -> ok; _ -> bad end, {R1, R2} =:= {ok, ok}\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "match_prefix clean match" "true"
|
|
||||||
check 11 "empty prefix matches all" "true"
|
|
||||||
check 12 "no common bytes -> nomatch" "true"
|
|
||||||
check 13 "prefix > path -> nomatch" "true"
|
|
||||||
check 14 "exact match -> empty rest" "true"
|
|
||||||
check 15 "actors_prefix size = 8" "8"
|
|
||||||
check 16 "GET /actors/alice -> 200" "ok"
|
|
||||||
check 17 "body carries 'actor: ' prefix" "true"
|
|
||||||
check 18 "GET /actors/ (empty id) -> 404" "ok"
|
|
||||||
check 19 "POST /actors/alice -> 404" "ok"
|
|
||||||
check 20 "GET /unrelated still 404" "ok"
|
|
||||||
check 21 "existing routes intact" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_actors.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_artifacts.sh — Step 8c-art acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises GET /artifacts/{cid} via the shared match_prefix
|
|
||||||
# machinery. Mirrors the actors-route test shape. 9 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; artifacts_prefix is "/artifacts/" — 11 bytes
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:artifacts_prefix())\")")
|
|
||||||
|
|
||||||
;; GET /artifacts/<cid> -> 200
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"Cid = <<98,97,102,107,114,101,49>>, Req = [{method, <<71,69,84>>}, {path, <<(http_server:artifacts_prefix())/binary, Cid/binary>>}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; The cid is echoed in the body (carries 'artifact: ' prefix)
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"Cid = <<98,97,102,107,114,101,49>>, Req = [{method, <<71,69,84>>}, {path, <<(http_server:artifacts_prefix())/binary, Cid/binary>>}], R = http_server:route(Req), case R of [_, _, {body, B}] -> http_server:match_prefix(<<97,114,116,105,102,97,99,116,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; GET /artifacts/ (empty cid) -> 404
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:artifacts_prefix()}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; POST /artifacts/<cid> -> 404 (only GET)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"Cid = <<98,97,102>>, Req = [{method, <<80,79,83,84>>}, {path, <<(http_server:artifacts_prefix())/binary, Cid/binary>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Actor and artifact routes don't collide
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"R1 = http_server:route([{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97>>}]), R2 = http_server:route([{method, <<71,69,84>>}, {path, <<(http_server:artifacts_prefix())/binary, 98>>}]), case {R1, R2} of {[{status, 200} | _], [{status, 200} | _]} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Existing routes (GET /, capabilities) still work
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"R1 = case http_server:route([{method, <<71,69,84>>}, {path, <<47>>}]) of [{status, 200} | _] -> ok; _ -> bad end, R2 = case http_server:route([{method, <<71,69,84>>}, {path, http_server:capabilities_path()}]) of [{status, 200} | _] -> ok; _ -> bad end, {R1, R2} =:= {ok, ok}\") :name)")
|
|
||||||
|
|
||||||
;; artifacts_prefix starts with '/'
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:artifacts_prefix() of <<47, _/binary>> -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "artifacts_prefix size = 11" "11"
|
|
||||||
check 11 "GET /artifacts/<cid> -> 200" "ok"
|
|
||||||
check 12 "body carries 'artifact: '" "true"
|
|
||||||
check 13 "GET /artifacts/ (empty) -> 404" "ok"
|
|
||||||
check 14 "POST /artifacts/<cid> -> 404" "ok"
|
|
||||||
check 15 "actors + artifacts no collision" "ok"
|
|
||||||
check 16 "static routes still 200" "true"
|
|
||||||
check 17 "artifacts_prefix leading /" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_artifacts.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_capabilities.sh — Step 8c-cap acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises GET /.well-known/sx-capabilities — kernel-version
|
|
||||||
# descriptor per design §16. The path is exposed as
|
|
||||||
# http_server:capabilities_path/0 so tests don't have to spell
|
|
||||||
# it byte-by-byte. 7 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; capabilities_path is exposed and non-empty
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"byte_size(http_server:capabilities_path()) > 10\") :name)")
|
|
||||||
|
|
||||||
;; GET capabilities_path returns 200
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = http_server:capabilities_path(), Req = [{method, <<71,69,84>>}, {path, P}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Capabilities body is non-empty and contains the verb names
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"B = http_server:capabilities_body(), byte_size(B) > 30\") :name)")
|
|
||||||
|
|
||||||
;; POST to capabilities path returns 404 (only GET dispatched)
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = http_server:capabilities_path(), Req = [{method, <<80,79,83,84>>}, {path, P}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Route returns capabilities_body when matching
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = http_server:capabilities_path(), Req = [{method, <<71,69,84>>}, {path, P}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= http_server:capabilities_body(); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; capabilities_path starts with '/' (47)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:capabilities_path() of <<47, _/binary>> -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Existing GET / route still works (no regression from the new clause)
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47>>}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "capabilities_path non-empty" "true"
|
|
||||||
check 11 "GET capabilities -> 200" "ok"
|
|
||||||
check 12 "capabilities body non-empty" "true"
|
|
||||||
check 13 "POST capabilities -> 404" "ok"
|
|
||||||
check 14 "route body matches capabilities" "true"
|
|
||||||
check 15 "capabilities_path leading /" "ok"
|
|
||||||
check 16 "GET / still works" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_capabilities.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_capabilities_format.sh — Step 8d-dispatch-cap test.
|
|
||||||
#
|
|
||||||
# Proves Accept header dispatch end-to-end on the
|
|
||||||
# /.well-known/sx-capabilities route. 12 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
|
|
||||||
|
|
||||||
# Shared bindings for the test:
|
|
||||||
# AK = "accept" header key
|
|
||||||
# CapPath = capabilities path (looked up from the module)
|
|
||||||
PRELUDE='AK = <<97,99,99,101,112,116>>, CapPath = http_server:capabilities_path(),'
|
|
||||||
|
|
||||||
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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; capabilities_body_for(text) == capabilities_body()
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:capabilities_body_for(text) =:= http_server:capabilities_body()\") :name)")
|
|
||||||
|
|
||||||
;; All format stubs are distinct
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"T = http_server:capabilities_body_for(text), J = http_server:capabilities_body_for(json), S = http_server:capabilities_body_for(sx), C = http_server:capabilities_body_for(cbor), (T =/= J) and (J =/= S) and (S =/= C) and (T =/= C)\") :name)")
|
|
||||||
|
|
||||||
;; json body starts with '{' (123)
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:capabilities_body_for(json) of <<123, _/binary>> -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; sx body starts with '(' (40)
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:capabilities_body_for(sx) of <<40, _/binary>> -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; cbor body starts with 0xA1 (161) — map(1)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:capabilities_body_for(cbor) of <<161, _/binary>> -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; activity_json shares its body with json
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:capabilities_body_for(activity_json) =:= http_server:capabilities_body_for(json)\") :name)")
|
|
||||||
|
|
||||||
;; Unknown format falls back to text
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:capabilities_body_for(weird_format) =:= http_server:capabilities_body()\") :name)")
|
|
||||||
|
|
||||||
;; Route with Accept: application/json -> json body
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} AV = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Req = [{method, <<71,69,84>>}, {path, CapPath}, {headers, [{AK, AV}]}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= http_server:capabilities_body_for(json); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; Route with Accept: application/sx -> sx body
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} AV = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>, Req = [{method, <<71,69,84>>}, {path, CapPath}, {headers, [{AK, AV}]}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= http_server:capabilities_body_for(sx); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; Route with Accept: application/cbor -> cbor body
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} AV = <<97,112,112,108,105,99,97,116,105,111,110,47,99,98,111,114>>, Req = [{method, <<71,69,84>>}, {path, CapPath}, {headers, [{AK, AV}]}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= http_server:capabilities_body_for(cbor); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; No Accept header -> text body
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Req = [{method, <<71,69,84>>}, {path, CapPath}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= http_server:capabilities_body(); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; POST capabilities still 404
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Req = [{method, <<80,79,83,84>>}, {path, CapPath}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "text format = existing body" "true"
|
|
||||||
check 11 "all format stubs distinct" "true"
|
|
||||||
check 12 "json body starts with '{'" "ok"
|
|
||||||
check 13 "sx body starts with '('" "ok"
|
|
||||||
check 14 "cbor body starts with 0xA1" "ok"
|
|
||||||
check 15 "activity_json == json body" "true"
|
|
||||||
check 16 "unknown format -> text" "true"
|
|
||||||
check 17 "Accept: json -> json body" "true"
|
|
||||||
check 18 "Accept: sx -> sx body" "true"
|
|
||||||
check 19 "Accept: cbor -> cbor body" "true"
|
|
||||||
check 20 "no Accept -> text body" "true"
|
|
||||||
check 21 "POST capabilities still 404" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_capabilities_format.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_content_type.sh — Step 8d-content-type test.
|
|
||||||
#
|
|
||||||
# Exercises content_type_for/1 and ok_response/2. 12 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; content_type_for returns the right byte size per format
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:content_type_for(text))\")")
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:content_type_for(json))\")")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:content_type_for(activity_json))\")")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:content_type_for(sx))\")")
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:content_type_for(cbor))\")")
|
|
||||||
|
|
||||||
;; All content types are distinct
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"T = http_server:content_type_for(text), J = http_server:content_type_for(json), AJ = http_server:content_type_for(activity_json), S = http_server:content_type_for(sx), C = http_server:content_type_for(cbor), (T =/= J) and (J =/= AJ) and (AJ =/= S) and (S =/= C) and (T =/= C)\") :name)")
|
|
||||||
|
|
||||||
;; Unknown format -> text Content-Type
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:content_type_for(weird) =:= http_server:content_type_for(text)\") :name)")
|
|
||||||
|
|
||||||
;; ok_response/2 has shape [{status, 200}, {headers, [{ct, ...}]}, {body, ...}]
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:ok_response(<<1,2>>, json), case R of [{status, 200}, {headers, [{<<99,111,110,116,101,110,116,45,116,121,112,101>>, _}]}, {body, <<1,2>>}] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; ok_response/2's CT value matches content_type_for for that format
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:ok_response(<<>>, sx), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(sx); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; ok_response/2 carries the body unchanged
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:ok_response(<<104,105>>, cbor), case R of [_, _, {body, <<104,105>>}] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; activity_json starts with 'application' (97)
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:content_type_for(activity_json) of <<97, _/binary>> -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Existing ok_response/1 still works (backwards compat)
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:ok_response(<<1,2,3>>), case R of [{status, 200}, {headers, []}, {body, <<1,2,3>>}] -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "text -> 'text/plain' (10b)" "10"
|
|
||||||
check 11 "json -> 'application/json' (16b)" "16"
|
|
||||||
check 12 "activity_json (25b)" "25"
|
|
||||||
check 13 "sx (14b)" "14"
|
|
||||||
check 14 "cbor (16b)" "16"
|
|
||||||
check 15 "all CTs distinct" "true"
|
|
||||||
check 16 "unknown -> text" "true"
|
|
||||||
check 17 "ok_response/2 shape" "ok"
|
|
||||||
check 18 "ok_response/2 CT matches" "true"
|
|
||||||
check 19 "body carried through" "ok"
|
|
||||||
check 20 "activity_json starts 'a'" "ok"
|
|
||||||
check 21 "ok_response/1 backward-compat" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_content_type.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_get_format.sh — Step 8d-dispatch-get test.
|
|
||||||
#
|
|
||||||
# Verifies actor/artifact/projection/projections_list GET routes
|
|
||||||
# return format-specific bodies + the right Content-Type. 16 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
|
|
||||||
|
|
||||||
# Common: accept key + several Accept values
|
|
||||||
PRELUDE='AK = <<97,99,99,101,112,116>>, JsonAV = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, SxAV = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>,'
|
|
||||||
|
|
||||||
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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; actor_doc_response_for(text) matches text-only counterpart
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:actor_doc_response_for(<<97>>, text) =:= http_server:actor_doc_response(<<97>>)\") :name)")
|
|
||||||
|
|
||||||
;; actor_doc_response_for(json) body: {"actor":"a"}\n
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:actor_doc_response_for(<<97>>, json), case R of [_, _, {body, B}] -> B =:= <<123,34,97,99,116,111,114,34,58,34,97,34,125,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; artifact_response_for(sx) body: (artifact "X")\n
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:artifact_response_for(<<120>>, sx), case R of [_, _, {body, B}] -> B =:= <<40,97,114,116,105,102,97,99,116,32,34,120,34,41,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; projection_response_for(json) body: {"projection":"foo"}\n
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:projection_response_for(<<102,111,111>>, json), case R of [_, _, {body, B}] -> B =:= <<123,34,112,114,111,106,101,99,116,105,111,110,34,58,34,102,111,111,34,125,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; projections_list_response_for(json) body: {"projections":[]}\n
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:projections_list_response_for(json), case R of [_, _, {body, B}] -> B =:= <<123,34,112,114,111,106,101,99,116,105,111,110,115,34,58,91,93,125,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; projections_list_response_for(sx) body: (projections)\n
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:projections_list_response_for(sx), case R of [_, _, {body, B}] -> B =:= <<40,112,114,111,106,101,99,116,105,111,110,115,41,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; cbor variants pass payload bytes through unchanged
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:actor_doc_response_for(<<97,98>>, cbor), case R of [_, _, {body, B}] -> B =:= <<97,98>>; _ -> false end\") :name)")
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:artifact_response_for(<<99,100>>, cbor), case R of [_, _, {body, B}] -> B =:= <<99,100>>; _ -> false end\") :name)")
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:projection_response_for(<<101>>, cbor), case R of [_, _, {body, B}] -> B =:= <<101>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end: GET /actors/a with Accept: application/json returns json body
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97>>}, {headers, [{AK, JsonAV}]}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= <<123,34,97,99,116,111,114,34,58,34,97,34,125,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end: GET /artifacts/X with Accept: application/sx returns sx body
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Req = [{method, <<71,69,84>>}, {path, <<(http_server:artifacts_prefix())/binary, 120>>}, {headers, [{AK, SxAV}]}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= <<40,97,114,116,105,102,97,99,116,32,34,120,34,41,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end: GET /projections with Accept: application/json returns json list body
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Req = [{method, <<71,69,84>>}, {path, http_server:projections_list_path()}, {headers, [{AK, JsonAV}]}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= <<123,34,112,114,111,106,101,99,116,105,111,110,115,34,58,91,93,125,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end: Content-Type matches for actor GET with json Accept
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97>>}, {headers, [{AK, JsonAV}]}], R = http_server:route(Req), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(json); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; GET without Accept still returns the text body (no Content-Type header)
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97>>}], R = http_server:route(Req), R =:= http_server:actor_doc_response(<<97>>)\") :name)")
|
|
||||||
|
|
||||||
;; activity_json shares body with json for actor
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(get (erlang-eval-ast \"[_, _, {body, BJ}] = http_server:actor_doc_response_for(<<122>>, json), [_, _, {body, BAJ}] = http_server:actor_doc_response_for(<<122>>, activity_json), BJ =:= BAJ\") :name)")
|
|
||||||
|
|
||||||
;; Unknown format falls back to text
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:projection_response_for(<<97>>, weird) =:= http_server:projection_response(<<97>>)\") :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 "module load name" "http_server"
|
|
||||||
check 10 "actor text preserves" "true"
|
|
||||||
check 11 "actor json body" "true"
|
|
||||||
check 12 "artifact sx body" "true"
|
|
||||||
check 13 "projection json body" "true"
|
|
||||||
check 14 "projections list json body" "true"
|
|
||||||
check 15 "projections list sx body" "true"
|
|
||||||
check 16 "actor cbor body = id" "true"
|
|
||||||
check 17 "artifact cbor body = cid" "true"
|
|
||||||
check 18 "projection cbor body = name" "true"
|
|
||||||
check 19 "E2E GET actor with json Accept" "true"
|
|
||||||
check 20 "E2E GET artifact with sx Accept" "true"
|
|
||||||
check 21 "E2E GET projections with json" "true"
|
|
||||||
check 22 "E2E actor json CT" "true"
|
|
||||||
check 23 "no Accept -> text shape" "true"
|
|
||||||
check 24 "activity_json body == json body" "true"
|
|
||||||
check 25 "unknown -> text" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_get_format.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_listen_bif.sh — Step 8a acceptance test.
|
|
||||||
#
|
|
||||||
# Verifies the http:listen/2 BIF wrapper is registered and
|
|
||||||
# validates its arguments. We do NOT exercise the actual listen
|
|
||||||
# loop — http-listen blocks forever, so production callers spawn
|
|
||||||
# an Erlang process to host the call. The BIF wrapper itself is
|
|
||||||
# tested for: registration, integer port enforcement, function
|
|
||||||
# handler enforcement.
|
|
||||||
#
|
|
||||||
# This BIF is the briefing's allowed-exception scope addition
|
|
||||||
# to lib/erlang/runtime.sx. 5 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")
|
|
||||||
|
|
||||||
;; BIF registered under http/listen/2
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(not (= (er-lookup-bif \"http\" \"listen\" 2) nil))")
|
|
||||||
|
|
||||||
;; BIF is non-pure (side effect: opens a socket)
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (er-lookup-bif \"http\" \"listen\" 2) :pure?)")
|
|
||||||
|
|
||||||
;; Non-integer port -> badarg
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"try http:listen(not_a_number, fun () -> ok end) catch error:badarg -> ok end\") :name)")
|
|
||||||
|
|
||||||
;; Non-fun handler -> badarg
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"try http:listen(8080, not_a_fun) catch error:badarg -> ok end\") :name)")
|
|
||||||
|
|
||||||
;; Wrong arity not registered (http/listen/1 should be nil)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(= (er-lookup-bif \"http\" \"listen\" 1) nil)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 10 "BIF registered under http/listen/2" "true"
|
|
||||||
check 11 "BIF marked non-pure" "false"
|
|
||||||
check 12 "non-integer port -> badarg" "ok"
|
|
||||||
check 13 "non-fun handler -> badarg" "ok"
|
|
||||||
check 14 "no /1 arity registered" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_listen_bif.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_post_activity.sh — Step 8c-post-auth acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises route/2 with bearer-token auth on POST /activity.
|
|
||||||
# Cfg :publish_token is the expected token; mismatched / missing /
|
|
||||||
# malformed Authorization header all 401. Real outbox:publish
|
|
||||||
# wiring lands in a follow-up sub-deliverable. 12 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
|
|
||||||
|
|
||||||
# Convenience: the bearer header name = "authorization"; "Bearer "
|
|
||||||
# prefix = 7 bytes; a sample token = "foo".
|
|
||||||
# Compose the right shapes inline in each test.
|
|
||||||
|
|
||||||
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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; activity_path is 9 bytes
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:activity_path())\")")
|
|
||||||
|
|
||||||
;; Authorized POST -> 200
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], Cfg = [{publish_token, Token}], case http_server:route(Req, Cfg) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Authorized body has 'published' prefix
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], Cfg = [{publish_token, Token}], R = http_server:route(Req, Cfg), case R of [_, _, {body, B}] -> http_server:match_prefix(<<112,117,98,108,105,115,104,101,100>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; No Authorization header -> 401
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, []}, {body, <<>>}], Cfg = [{publish_token, <<102,111,111>>}], case http_server:route(Req, Cfg) of [{status, 401} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Wrong bearer token -> 401
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,98,97,100>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], Cfg = [{publish_token, <<102,111,111>>}], case http_server:route(Req, Cfg) of [{status, 401} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Malformed Authorization (missing 'Bearer ') -> 401
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], Cfg = [{publish_token, <<102,111,111>>}], case http_server:route(Req, Cfg) of [{status, 401} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Cfg without :publish_token -> 401 even with a bearer token present
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], case http_server:route(Req, []) of [{status, 401} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; route/1 (no Cfg) treats POST /activity as 401 (no token configured)
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], case http_server:route(Req) of [{status, 401} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; GET /activity -> 404 (only POST is /activity)
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:activity_path()}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Other authorized routes still work via route/2
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"Cfg = [{publish_token, <<102,111,111>>}], Req = [{method, <<71,69,84>>}, {path, <<47>>}], case http_server:route(Req, Cfg) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; unauthorized_response shape sanity
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(erlang-eval-ast \"R = http_server:unauthorized_response(), case R of [{status, 401} | _] -> 401; _ -> nope end\")")
|
|
||||||
|
|
||||||
;; Empty bearer token (just \"Bearer \") -> 401
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], Cfg = [{publish_token, <<102,111,111>>}], case http_server:route(Req, Cfg) of [{status, 401} | _] -> 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 "module load name" "http_server"
|
|
||||||
check 10 "activity_path = 9 bytes" "9"
|
|
||||||
check 11 "authorized POST -> 200" "ok"
|
|
||||||
check 12 "body has 'published' prefix" "true"
|
|
||||||
check 13 "no Authorization -> 401" "ok"
|
|
||||||
check 14 "wrong token -> 401" "ok"
|
|
||||||
check 15 "malformed Authorization -> 401" "ok"
|
|
||||||
check 16 "Cfg without token -> 401" "ok"
|
|
||||||
check 17 "route/1 rejects POST /activity" "ok"
|
|
||||||
check 18 "GET /activity -> 404" "ok"
|
|
||||||
check 19 "other GETs work via route/2" "ok"
|
|
||||||
check 20 "unauthorized_response status 401" "401"
|
|
||||||
check 21 "empty bearer token -> 401" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_post_activity.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_post_format.sh — Step 8d-dispatch-post test.
|
|
||||||
#
|
|
||||||
# Verifies POST /activity returns format-specific bodies + the
|
|
||||||
# right Content-Type, both for the kernel-absent stub path and
|
|
||||||
# the kernel-present cid response. 14 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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
(epoch 8)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(json) body: {"cid":"foo"}\n
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:cid_response_for(<<102,111,111>>, json), case R of [_, _, {body, B}] -> B =:= <<123,34,99,105,100,34,58,34,102,111,111,34,125,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(json) CT is application/json
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:cid_response_for(<<102,111,111>>, json), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(json); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(sx) body: (cid "foo")\n
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:cid_response_for(<<102,111,111>>, sx), case R of [_, _, {body, B}] -> B =:= <<40,99,105,100,32,34,102,111,111,34,41,10>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(text) matches cid_response/1
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:cid_response_for(<<102,111,111>>, text) =:= http_server:cid_response(<<102,111,111>>)\") :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(activity_json) body == cid_response_for(json) body
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"[_, _, {body, BJ}] = http_server:cid_response_for(<<102,111,111>>, json), [_, _, {body, BAJ}] = http_server:cid_response_for(<<102,111,111>>, activity_json), BJ =:= BAJ\") :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(activity_json) CT is application/activity+json
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:cid_response_for(<<102,111,111>>, activity_json), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(activity_json); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; cid_response_for(cbor) carries the raw CID as body
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:cid_response_for(<<102,111,111>>, cbor), case R of [_, _, {body, B}] -> B =:= <<102,111,111>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; post_activity_response_for(json) has json CT
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:post_activity_response_for(json), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(json); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; post_activity_response_for(text) matches the original
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"http_server:post_activity_response_for(text) =:= http_server:post_activity_response()\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end: POST /activity with Accept: application/json returns
|
|
||||||
;; the json stub when nx_kernel is not running
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}, {AcceptKey, AcceptVal}]}, {body, <<>>}], Cfg = [{publish_token, Token}], R = http_server:route(Req, Cfg), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(json); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end: POST /activity with kernel running + Accept: application/sx
|
|
||||||
;; returns body shaped as (cid "...")
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], nx_kernel:start_link(alice, KS, AS), Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}, {AcceptKey, AcceptVal}]}, {body, <<104,105>>}], Cfg = [{publish_token, Token}], R = http_server:route(Req, Cfg), case R of [_, _, {body, B}] -> http_server:match_prefix(<<40,99,105,100,32,34>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; End-to-end CT for kernel-publish with json Accept matches application/json
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], nx_kernel:start_link(alice, KS, AS), Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}, {AcceptKey, AcceptVal}]}, {body, <<104,105>>}], Cfg = [{publish_token, Token}], R = http_server:route(Req, Cfg), case R of [_, {headers, [{_, CT}]}, _] -> CT =:= http_server:content_type_for(json); _ -> false end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$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 8 "http_server loaded" "http_server"
|
|
||||||
check 10 "cid_response_for(json) body" "true"
|
|
||||||
check 11 "cid_response_for(json) CT" "true"
|
|
||||||
check 12 "cid_response_for(sx) body" "true"
|
|
||||||
check 13 "cid_response_for(text) preserves" "true"
|
|
||||||
check 14 "activity_json body == json body" "true"
|
|
||||||
check 15 "activity_json CT differs" "true"
|
|
||||||
check 16 "cbor carries raw cid" "true"
|
|
||||||
check 17 "post_activity stub json CT" "true"
|
|
||||||
check 18 "post_activity stub text preserves" "true"
|
|
||||||
check 19 "POST kernel-absent json CT" "true"
|
|
||||||
check 20 "POST kernel-publish sx body" "true"
|
|
||||||
check 21 "POST kernel-publish json CT" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_post_format.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_projections.sh — Step 8c-proj acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises GET /projections (list stub) and GET /projections/{name}
|
|
||||||
# via the shared match_prefix machinery. 11 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; projections_list_path is 12 bytes
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:projections_list_path())\")")
|
|
||||||
|
|
||||||
;; projections_prefix is 13 bytes (adds trailing slash)
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"byte_size(http_server:projections_prefix())\")")
|
|
||||||
|
|
||||||
;; GET /projections -> 200 (list stub)
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:projections_list_path()}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; List body has 'projections: ' prefix
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:projections_list_path()}], R = http_server:route(Req), case R of [_, _, {body, B}] -> http_server:match_prefix(<<112,114,111,106,101,99,116,105,111,110,115,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; GET /projections/foo -> 200
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"Name = <<102,111,111>>, Req = [{method, <<71,69,84>>}, {path, <<(http_server:projections_prefix())/binary, Name/binary>>}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Projection body has 'projection: ' prefix (singular)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"Name = <<102,111,111>>, Req = [{method, <<71,69,84>>}, {path, <<(http_server:projections_prefix())/binary, Name/binary>>}], R = http_server:route(Req), case R of [_, _, {body, B}] -> http_server:match_prefix(<<112,114,111,106,101,99,116,105,111,110,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; GET /projections/ (empty name) -> 404
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:projections_prefix()}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; POST /projections -> 404
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, http_server:projections_list_path()}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; POST /projections/foo -> 404
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"Name = <<102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, <<(http_server:projections_prefix())/binary, Name/binary>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; No collision: actors / artifacts / projections all return 200 simultaneously
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"R1 = http_server:route([{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97>>}]), R2 = http_server:route([{method, <<71,69,84>>}, {path, <<(http_server:artifacts_prefix())/binary, 98>>}]), R3 = http_server:route([{method, <<71,69,84>>}, {path, <<(http_server:projections_prefix())/binary, 99>>}]), case {R1, R2, R3} of {[{status, 200} | _], [{status, 200} | _], [{status, 200} | _]} -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "projections_list_path = 12" "12"
|
|
||||||
check 11 "projections_prefix = 13" "13"
|
|
||||||
check 12 "GET /projections -> 200" "ok"
|
|
||||||
check 13 "list body 'projections: '" "true"
|
|
||||||
check 14 "GET /projections/foo -> 200" "ok"
|
|
||||||
check 15 "single body 'projection: '" "true"
|
|
||||||
check 16 "GET /projections/ -> 404" "ok"
|
|
||||||
check 17 "POST /projections -> 404" "ok"
|
|
||||||
check 18 "POST /projections/foo -> 404" "ok"
|
|
||||||
check 19 "all three /-routes 200" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_projections.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_publish.sh — Step 8c-post-publish-http test.
|
|
||||||
#
|
|
||||||
# Exercises the HTTP -> nx_kernel publish bridge: authorized
|
|
||||||
# POST /activity with the kernel gen_server running gets routed
|
|
||||||
# through nx_kernel:publish/1; the response carries the
|
|
||||||
# resulting CID. Without the kernel running, the route falls
|
|
||||||
# back to the auth-only stub (covered by http_post_activity.sh).
|
|
||||||
# 9 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
|
|
||||||
|
|
||||||
# Shared prelude: kernel started, auth header, valid request shape.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], nx_kernel:start_link(alice, KS, AS), Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Token}],'
|
|
||||||
|
|
||||||
# Body builder helper appended into each test:
|
|
||||||
BUILDREQ='Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, Body}],'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
(epoch 8)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Authorized POST -> 200 with body starting with "cid: "
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Body = <<104,101,108,108,111>>, ${BUILDREQ} case http_server:route(Req, Cfg) of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<99,105,100,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; Log tip advances after authorized POST
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Body = <<104,105>>, ${BUILDREQ} http_server:route(Req, Cfg), nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; Two authorized POSTs -> tip = 2
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Body = <<104,105>>, ${BUILDREQ} http_server:route(Req, Cfg), http_server:route(Req, Cfg), nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; Same POST twice produces two distinct CIDs (next_published counter)
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Body = <<104,105>>, ${BUILDREQ} [{status, 200}, _, {body, B1}] = http_server:route(Req, Cfg), [{status, 200}, _, {body, B2}] = http_server:route(Req, Cfg), B1 =/= B2\") :name)")
|
|
||||||
|
|
||||||
;; Unauthorized POST does NOT advance the kernel log
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} BadAuth = <<66,101,97,114,101,114,32,98,97,100>>, BadReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, BadAuth}]}, {body, <<>>}], http_server:route(BadReq, Cfg), nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; Sig-failure publish surfaces as 422 (when key material doesn't match)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"OtherKM = <<9,9,9,9>>, BadKS = [{key_id,k1},{algorithm,ed25519},{value,OtherKM}], AS = [{public_keys,[[{id,k1},{created,0},{value,<<1,2,3,4>>}]]}], nx_kernel:start_link(alice, BadKS, AS), Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Token}], Body = <<104,105>>, Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, Body}], case http_server:route(Req, Cfg) of [{status, 422} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Without the kernel running, the auth-only stub still works
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Token}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], R = http_server:route(Req, Cfg), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<112,117,98,108,105,115,104,101,100>>, B) =/= nomatch; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; validation_failed_response shape sanity
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(erlang-eval-ast \"R = http_server:validation_failed_response(), case R of [{status, 422} | _] -> 422; _ -> nope end\")")
|
|
||||||
|
|
||||||
;; cid_response wraps a cid with the right prefix
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:cid_response(<<102,111,111>>), case R of [_, _, {body, B}] -> B =:= <<99,105,100,58,32,102,111,111,10>>; _ -> false end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$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 8 "http_server loaded" "http_server"
|
|
||||||
check 10 "POST -> 200 with 'cid: '" "true"
|
|
||||||
check 11 "log_tip = 1 after POST" "1"
|
|
||||||
check 12 "two POSTs -> tip = 2" "2"
|
|
||||||
check 13 "same POST -> distinct CIDs" "true"
|
|
||||||
check 14 "unauthorized POST -> tip = 0" "0"
|
|
||||||
check 15 "sig failure -> 422" "ok"
|
|
||||||
check 16 "kernel-absent fallback stub" "true"
|
|
||||||
check 17 "validation_failed_response 422" "422"
|
|
||||||
check 18 "cid_response wraps cid" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_publish.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_publish_fold.sh — Step 9-pre-fold integration.
|
|
||||||
#
|
|
||||||
# Proves the full POST → publish → broadcast → projection-fold
|
|
||||||
# chain through HTTP without a real TCP socket. The kernel
|
|
||||||
# orchestrator threads :projections into the publish Context,
|
|
||||||
# so outbox:publish broadcasts the signed activity to every
|
|
||||||
# registered projection process and each fold runs.
|
|
||||||
#
|
|
||||||
# Step 9a/b smoke tests will exercise the same path via curl
|
|
||||||
# once Step 8b-start lights up actual TCP. 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
|
|
||||||
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], projection:start_link(p_count, 0, fun (_A, S) -> S + 1 end), projection:start_link(p_collect, [], fun (A, S) -> [A | S] end), nx_kernel:start_link(alice, KS, AS), nx_kernel:with_projections([p_count, p_collect]), Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Token}], BuildReq = fun (B) -> [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, B}] end,'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 8)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
(epoch 9)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Single authorized POST advances both projection counters
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<104,105>>), Cfg), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<104,105>>), Cfg), length(projection:query(p_collect))\")")
|
|
||||||
|
|
||||||
;; Three POSTs -> both projections at 3
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<104,105>>), Cfg), http_server:route(BuildReq(<<104,105>>), Cfg), http_server:route(BuildReq(<<104,105>>), Cfg), {projection:query(p_count), length(projection:query(p_collect))} =:= {3, 3}\") :name)")
|
|
||||||
|
|
||||||
;; Log tip and projection counter agree
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<104,105>>), Cfg), http_server:route(BuildReq(<<104,105>>), Cfg), {nx_kernel:log_tip(), projection:query(p_count)} =:= {2, 2}\") :name)")
|
|
||||||
|
|
||||||
;; Unauthorized POST does NOT advance projection state
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} BadAuth = <<66,101,97,114,101,114,32,98,97,100>>, BadReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, BadAuth}]}, {body, <<104,105>>}], http_server:route(BadReq, Cfg), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
;; Sig-failed POST does NOT advance projection state (kernel rejects)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(erlang-eval-ast \"OtherKM = <<9,9,9,9>>, BadKS = [{key_id,k1},{algorithm,ed25519},{value,OtherKM}], AS = [{public_keys,[[{id,k1},{created,0},{value,<<1,2,3,4>>}]]}], projection:start_link(p_count, 0, fun (_A, S) -> S + 1 end), nx_kernel:start_link(alice, BadKS, AS), nx_kernel:with_projections([p_count]), Token = <<102,111,111>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AuthVal = <<66,101,97,114,101,114,32,102,111,111>>, Cfg = [{publish_token, Token}], Req = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AuthVal}]}, {body, <<>>}], http_server:route(Req, Cfg), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
;; The body posted is what the projection sees inside the activity's :object
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<120,121,122>>), Cfg), [Act] = projection:query(p_collect), case envelope:get_field(object, Act) of {ok, <<120,121,122>>} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Three POSTs -> log entries match (round-trip via the kernel log)
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<104,105>>), Cfg), http_server:route(BuildReq(<<104,105>>), Cfg), http_server:route(BuildReq(<<104,105>>), Cfg), length(log:entries(nx_kernel:log_state(nx_kernel:query())))\")")
|
|
||||||
|
|
||||||
;; Single POST: projection seq number proves fold ran (state changed)
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} http_server:route(BuildReq(<<104,105>>), Cfg), projection:query(p_count) =/= 0\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 300 "$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 9 "http_server loaded" "http_server"
|
|
||||||
check 10 "POST -> p_count = 1" "1"
|
|
||||||
check 11 "POST -> p_collect length = 1" "1"
|
|
||||||
check 12 "three POSTs -> both at 3" "true"
|
|
||||||
check 13 "log_tip == p_count" "true"
|
|
||||||
check 14 "unauthorized POST no fold" "0"
|
|
||||||
check 15 "sig failure no fold" "0"
|
|
||||||
check 16 "projection sees body as :object" "ok"
|
|
||||||
check 17 "log entries = 3 after 3 POSTs" "3"
|
|
||||||
check 18 "single POST changes proj state" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_publish_fold.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/http_route.sh — Step 8b acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises http_server:route/1 — pure (Request) -> Response
|
|
||||||
# proplist dispatch. The actual HTTP listener (which would call
|
|
||||||
# this via the http:listen/2 BIF bridge) is wired in Step 8c+.
|
|
||||||
# 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/http_server.erl\")) :name)")
|
|
||||||
|
|
||||||
;; GET / -> 200
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47>>}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; GET / body is the welcome message
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47>>}], R = http_server:route(Req), case R of [_, _, {body, B}] -> B =:= http_server:welcome_body(); _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; POST / -> 404 (only GET / is known)
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, <<47>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; GET /unknown -> 404
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,102,111,111>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Missing fields -> 404 (graceful)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"case http_server:route([]) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Response always has :status, :headers, :body
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(erlang-eval-ast \"R = http_server:not_found_response(), length(R)\")")
|
|
||||||
|
|
||||||
;; ok_response sets the right status
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(erlang-eval-ast \"R = http_server:ok_response(<<104,105>>), case R of [{status, 200} | _] -> 200; _ -> nope end\")")
|
|
||||||
|
|
||||||
;; ok_response carries the supplied body
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:ok_response(<<104,105>>), case R of [_, _, {body, B}] -> B =:= <<104,105>>; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; not_found body present (non-empty)
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"R = http_server:not_found_response(), case R of [_, _, {body, B}] -> byte_size(B) > 0; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; welcome_body is non-empty
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"byte_size(http_server:welcome_body()) > 0\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "http_server"
|
|
||||||
check 10 "GET / -> 200" "ok"
|
|
||||||
check 11 "GET / body is welcome" "true"
|
|
||||||
check 12 "POST / -> 404" "ok"
|
|
||||||
check 13 "GET /unknown -> 404" "ok"
|
|
||||||
check 14 "missing fields -> 404" "ok"
|
|
||||||
check 15 "response has 3 entries" "3"
|
|
||||||
check 16 "ok_response status = 200" "200"
|
|
||||||
check 17 "ok_response carries body" "true"
|
|
||||||
check 18 "not_found body non-empty" "true"
|
|
||||||
check 19 "welcome body non-empty" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/http_route.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/log_memory.sh — Step 3a acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises the in-memory log API: open/2, append/2, tip/1, replay/3,
|
|
||||||
# entries/1. On-disk persistence is the job of Step 3b. 11 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/log.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Fresh log: tip is 0
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), log:tip(L) =:= 0\") :name)")
|
|
||||||
|
|
||||||
;; Fresh log: entries empty
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), log:entries(L) =:= []\") :name)")
|
|
||||||
|
|
||||||
;; First append returns seq 0; tip advances to 1
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, S} = log:append(L0, act_a), {S, log:tip(L1)} =:= {0, 1}\") :name)")
|
|
||||||
|
|
||||||
;; Two appends: seq 0,1; tip = 2
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, S0} = log:append(L0, a), {ok, L2, S1} = log:append(L1, b), {S0, S1, log:tip(L2)} =:= {0, 1, 2}\") :name)")
|
|
||||||
|
|
||||||
;; Five appends: seq sequence gap-free
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, S0} = log:append(L0, a), {ok, L2, S1} = log:append(L1, b), {ok, L3, S2} = log:append(L2, c), {ok, L4, S3} = log:append(L3, d), {ok, L5, S4} = log:append(L4, e), {S0,S1,S2,S3,S4,log:tip(L5)} =:= {0,1,2,3,4,5}\") :name)")
|
|
||||||
|
|
||||||
;; entries/1 returns activities in append order
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, _} = log:append(L0, a), {ok, L2, _} = log:append(L1, b), {ok, L3, _} = log:append(L2, c), log:entries(L3) =:= [a, b, c]\") :name)")
|
|
||||||
|
|
||||||
;; Round-trip: appended activity is recoverable byte-for-byte
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"Act = [{id,1},{type,create},{actor,alice}], {ok, L0} = log:open(alice, base), {ok, L1, _} = log:append(L0, Act), log:entries(L1) =:= [Act]\") :name)")
|
|
||||||
|
|
||||||
;; Per-actor isolation: two logs are independent
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, LA0} = log:open(alice, base), {ok, LB0} = log:open(bob, base), {ok, LA1, _} = log:append(LA0, a), {ok, LB1, _} = log:append(LB0, b1), {ok, LB2, _} = log:append(LB1, b2), {log:tip(LA1), log:tip(LB2)} =:= {1, 2}\") :name)")
|
|
||||||
|
|
||||||
;; replay/3 visits all activities in append order with monotonic seqs
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, _} = log:append(L0, a), {ok, L2, _} = log:append(L1, b), {ok, L3, _} = log:append(L2, c), log:replay(L3, [], fun (A, S, Acc) -> [{S, A} | Acc] end) =:= [{2,c},{1,b},{0,a}]\") :name)")
|
|
||||||
|
|
||||||
;; replay over empty log: InitAcc returned unchanged
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), log:replay(L, init_acc, fun (_, _, A) -> A end) =:= init_acc\") :name)")
|
|
||||||
|
|
||||||
;; replay can compute a derived state (sum of integer activities)
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, _} = log:append(L0, 10), {ok, L2, _} = log:append(L1, 20), {ok, L3, _} = log:append(L2, 30), log:replay(L3, 0, fun (V, _, Acc) -> V + Acc end) =:= 60\") :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 "module load name" "log"
|
|
||||||
check 10 "fresh log tip is 0" "true"
|
|
||||||
check 11 "fresh log entries empty" "true"
|
|
||||||
check 12 "append returns seq 0, tip 1" "true"
|
|
||||||
check 13 "two appends seq 0,1; tip 2" "true"
|
|
||||||
check 14 "five appends gap-free" "true"
|
|
||||||
check 15 "entries in append order" "true"
|
|
||||||
check 16 "round-trip activity" "true"
|
|
||||||
check 17 "per-actor isolation" "true"
|
|
||||||
check 18 "replay visits all in order" "true"
|
|
||||||
check 19 "replay over empty log" "true"
|
|
||||||
check 20 "replay computes derived state" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/log_memory.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/nx_kernel_pure.sh — Step 8c-post-publish-pure tests.
|
|
||||||
#
|
|
||||||
# Exercises pure-functional nx_kernel:new/3, publish/2, and the
|
|
||||||
# accessors. Verifies the state advances correctly across multiple
|
|
||||||
# publishes and that the next_published counter prevents replay
|
|
||||||
# collisions when the same Request is published twice. 11 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
|
|
||||||
|
|
||||||
# Shared prelude: key material + actor state + an initial nx_kernel
|
|
||||||
# state bound to S0. Each test builds from S0.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], S0 = nx_kernel:new(alice, KS, AS), Req = [{type,create},{object,nil}],'
|
|
||||||
|
|
||||||
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/log.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
|
|
||||||
;; new/3 — fresh state has log_tip 0 and next_published 1
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:log_tip(S0)\")")
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:next_published(S0)\")")
|
|
||||||
|
|
||||||
;; Accessors return the expected values
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:actor_id(S0) =:= alice\") :name)")
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:key_spec(S0) =:= KS\") :name)")
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:actor_state(S0) =:= AS\") :name)")
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:projections(S0) =:= []\") :name)")
|
|
||||||
|
|
||||||
;; publish/2 happy path: log_tip advances to 1, next_published to 2
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, _, S1} = nx_kernel:publish(Req, S0), {nx_kernel:log_tip(S1), nx_kernel:next_published(S1)} =:= {1, 2}\") :name)")
|
|
||||||
|
|
||||||
;; Two sequential publishes (same Request) succeed because the
|
|
||||||
;; next_published counter makes each canonical envelope distinct
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} {ok, _, S1} = nx_kernel:publish(Req, S0), {ok, _, S2} = nx_kernel:publish(Req, S1), nx_kernel:log_tip(S2)\")")
|
|
||||||
|
|
||||||
;; Two publishes also bump next_published to 3
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} {ok, _, S1} = nx_kernel:publish(Req, S0), {ok, _, S2} = nx_kernel:publish(Req, S1), nx_kernel:next_published(S2)\")")
|
|
||||||
|
|
||||||
;; Bad key in state -> publish fails, state unchanged
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} OtherKM = <<9,9,9,9>>, BadKS = [{key_id,k1},{algorithm,ed25519},{value,OtherKM}], BadS = nx_kernel:new(alice, BadKS, AS), case nx_kernel:publish(Req, BadS) of {error, bad_signature, S} -> nx_kernel:log_tip(S) =:= 0; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; with_projections replaces the :projections list
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} S = nx_kernel:with_projections([p_count], S0), nx_kernel:projections(S) =:= [p_count]\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$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 6 "nx_kernel module loaded" "nx_kernel"
|
|
||||||
check 10 "fresh log_tip = 0" "0"
|
|
||||||
check 11 "next_published starts at 1" "1"
|
|
||||||
check 12 "actor_id accessor" "true"
|
|
||||||
check 13 "key_spec accessor" "true"
|
|
||||||
check 14 "actor_state accessor" "true"
|
|
||||||
check 15 "projections defaults to []" "true"
|
|
||||||
check 20 "publish advances tip + counter" "true"
|
|
||||||
check 21 "two publishes advance tip to 2" "2"
|
|
||||||
check 22 "two publishes -> counter = 3" "3"
|
|
||||||
check 23 "bad key fails, state unchanged" "true"
|
|
||||||
check 24 "with_projections sets list" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/nx_kernel_pure.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/nx_kernel_server.sh — Step 8c-post-publish-srv tests.
|
|
||||||
#
|
|
||||||
# Exercises the gen_server-wrapped nx_kernel. Same port quirks
|
|
||||||
# as registry/projection gen_servers: each test inlines start_link
|
|
||||||
# with operations. 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
|
|
||||||
|
|
||||||
# Shared prelude — KS/AS bindings + start_link + a Req binding.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], nx_kernel:start_link(alice, KS, AS), Req = [{type,create},{object,nil}],'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
|
|
||||||
;; start_link returns a Pid registered under nx_kernel
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} is_pid(whereis(nx_kernel))\") :name)")
|
|
||||||
|
|
||||||
;; log_tip starts at 0
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; publish/1 happy path returns {ok, _}
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} case nx_kernel:publish(Req) of {ok, _} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; After one publish, log_tip = 1
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish(Req), nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; Two publishes -> log_tip = 2 (next_published counter avoids replay)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish(Req), nx_kernel:publish(Req), nx_kernel:log_tip()\")")
|
|
||||||
|
|
||||||
;; query/0 returns a state proplist with the right actor_id
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} S = nx_kernel:query(), nx_kernel:actor_id(S) =:= alice\") :name)")
|
|
||||||
|
|
||||||
;; with_projections/1 sets the projection list, visible via query
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:with_projections([px]), S = nx_kernel:query(), nx_kernel:projections(S) =:= [px]\") :name)")
|
|
||||||
|
|
||||||
;; Bad key in state -> publish returns {error, bad_signature}; log_tip unchanged
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"OtherKM = <<9,9,9,9>>, KS = [{key_id,k1},{algorithm,ed25519},{value,OtherKM}], AS = [{public_keys,[[{id,k1},{created,0},{value,<<1,2,3,4>>}]]}], nx_kernel:start_link(alice, KS, AS), Req = [{type,create},{object,nil}], R = nx_kernel:publish(Req), Tip = nx_kernel:log_tip(), case {R, Tip} of {{error, bad_signature}, 0} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; State persists across multiple gen_server calls in one expression
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(Req), Tip1 = nx_kernel:log_tip(), nx_kernel:publish(Req), Tip2 = nx_kernel:log_tip(), {Tip1, Tip2} =:= {1, 2}\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$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 "gen_server loaded" "gen_server"
|
|
||||||
check 7 "nx_kernel module loaded" "nx_kernel"
|
|
||||||
check 10 "start_link registered Pid" "true"
|
|
||||||
check 11 "fresh log_tip = 0" "0"
|
|
||||||
check 12 "publish/1 happy path" "ok"
|
|
||||||
check 13 "tip = 1 after one publish" "1"
|
|
||||||
check 14 "tip = 2 after two publishes" "2"
|
|
||||||
check 15 "query returns state w/ actor_id" "true"
|
|
||||||
check 16 "with_projections persists" "true"
|
|
||||||
check 17 "bad key fails, tip unchanged" "ok"
|
|
||||||
check 18 "state persists across calls" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/nx_kernel_server.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/outbox_broadcast.sh — Step 7c acceptance test.
|
|
||||||
#
|
|
||||||
# Verifies outbox:publish/2 fans out to projection processes
|
|
||||||
# listed in Context's :projections entry. Each test inlines
|
|
||||||
# start_link with publish + query because spawned processes
|
|
||||||
# don't survive across erlang-eval-ast invocations. 9 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
|
|
||||||
|
|
||||||
# Shared prelude: KM/KS/AS/L0 + projections registered + Ctx with
|
|
||||||
# the named projections wired through. Each test threads from
|
|
||||||
# this state.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,50},{value,KM}]]}], {ok, L0} = log:open(alice, base), projection:start_link(p_count, 0, fun (_A, S) -> S + 1 end), projection:start_link(p_collect, [], fun (A, S) -> [A | S] end),'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Single publish fans out to one projection -> count = 1
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0},{projections,[p_count]}], outbox:publish([{type,create},{object,nil}], Ctx), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
;; Single publish fans out to TWO projections -> both advance
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0},{projections,[p_count, p_collect]}], outbox:publish([{type,create},{object,nil}], Ctx), C = projection:query(p_count), L = projection:query(p_collect), {C, length(L)} =:= {1, 1}\") :name)")
|
|
||||||
|
|
||||||
;; Empty :projections list -> no fan-out, projections stay at initial state
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0},{projections,[]}], outbox:publish([{type,create},{object,nil}], Ctx), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
;; Missing :projections field -> no fan-out
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0}], outbox:publish([{type,create},{object,nil}], Ctx), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
;; Three sequential publishes -> projection count = 3 (state persisted across casts)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Ctx0 = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0},{projections,[p_count]}], {ok, _, L1} = outbox:publish([{type,create},{object,nil}], Ctx0), Ctx1 = [{actor_id,alice},{published,200},{key_spec,KS},{actor_state,AS},{log,L1},{projections,[p_count]}], {ok, _, L2} = outbox:publish([{type,create},{object,nil}], Ctx1), Ctx2 = [{actor_id,alice},{published,300},{key_spec,KS},{actor_state,AS},{log,L2},{projections,[p_count]}], outbox:publish([{type,create},{object,nil}], Ctx2), projection:query(p_count)\")")
|
|
||||||
|
|
||||||
;; Replay-halted publish does NOT broadcast
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0},{projections,[p_count]}], Req = [{type,create},{object,nil}], {ok, _, L1} = outbox:publish(Req, Ctx), Ctx2 = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L1},{projections,[p_count]}], outbox:publish(Req, Ctx2), projection:query(p_count) =:= 1\") :name)")
|
|
||||||
|
|
||||||
;; Sig-failed publish does NOT broadcast
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} BadKS = [{key_id,k1},{algorithm,ed25519},{value,<<9,9,9,9>>}], Ctx = [{actor_id,alice},{published,100},{key_spec,BadKS},{actor_state,AS},{log,L0},{projections,[p_count]}], outbox:publish([{type,create},{object,nil}], Ctx), projection:query(p_count) =:= 0\") :name)")
|
|
||||||
|
|
||||||
;; Projections receive the Signed activity (collect-fold sees envelope structure)
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0},{projections,[p_collect]}], {ok, Result, _} = outbox:publish([{type,create},{object,nil}], Ctx), {ok, ExpectedAct} = envelope:get_field(activity, Result), [Got] = projection:query(p_collect), Got =:= ExpectedAct\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$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 "gen_server loaded" "gen_server"
|
|
||||||
check 3 "envelope module loaded" "envelope"
|
|
||||||
check 4 "log module loaded" "log"
|
|
||||||
check 5 "pipeline module loaded" "pipeline"
|
|
||||||
check 6 "projection module loaded" "projection"
|
|
||||||
check 7 "outbox module loaded" "outbox"
|
|
||||||
check 10 "single publish -> count = 1" "1"
|
|
||||||
check 11 "fan-out to two projections" "true"
|
|
||||||
check 12 "empty :projections -> no fanout" "0"
|
|
||||||
check 13 "missing :projections -> no fan" "0"
|
|
||||||
check 14 "three publishes -> count = 3" "3"
|
|
||||||
check 15 "replay halt skips broadcast" "true"
|
|
||||||
check 16 "sig failure skips broadcast" "true"
|
|
||||||
check 17 "projection sees Signed activity" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/outbox_broadcast.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/outbox_construct.sh — Step 6d-cs acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises outbox:construct/4, outbox:sign/2, outbox:cid_of/1.
|
|
||||||
# Closes the loop by verifying that construct→sign produces an
|
|
||||||
# envelope that envelope:verify_signature/2 accepts. 11 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/outbox.erl\")) :name)")
|
|
||||||
|
|
||||||
;; construct: required fields present
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), envelope:get_field(actor, Env) =:= {ok, alice}\") :name)")
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), envelope:get_field(type, Env) =:= {ok, create}\") :name)")
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), envelope:get_field(published, Env) =:= {ok, 100}\") :name)")
|
|
||||||
|
|
||||||
;; construct: :id is a non-trivial CID
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), {ok, Id} = envelope:get_field(id, Env), is_binary(Id) and (byte_size(Id) > 50)\") :name)")
|
|
||||||
|
|
||||||
;; construct deterministic across calls with same args
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"E1 = outbox:construct(create, alice, 100, nil), E2 = outbox:construct(create, alice, 100, nil), outbox:cid_of(E1) =:= outbox:cid_of(E2)\") :name)")
|
|
||||||
|
|
||||||
;; construct distinct CIDs for distinct types
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"E1 = outbox:construct(create, alice, 100, nil), E2 = outbox:construct(update, alice, 100, nil), outbox:cid_of(E1) =/= outbox:cid_of(E2)\") :name)")
|
|
||||||
|
|
||||||
;; construct distinct CIDs for distinct timestamps
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"E1 = outbox:construct(create, alice, 100, nil), E2 = outbox:construct(create, alice, 101, nil), outbox:cid_of(E1) =/= outbox:cid_of(E2)\") :name)")
|
|
||||||
|
|
||||||
;; sign adds a :signature field
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"KS = [{key_id, k1}, {algorithm, ed25519}, {value, <<1,2,3>>}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), envelope:get_field(signature, Signed) =/= not_found\") :name)")
|
|
||||||
|
|
||||||
;; signed envelope passes envelope:verify_signature with matching key
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), AS = [{public_keys, [[{id, k1}, {created, 50}, {value, KM}]]}], envelope:verify_signature(Signed, AS) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; signed envelope fails verify with a wrong key
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, OtherKM = <<9,9,9,9>>, KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), AS = [{public_keys, [[{id, k1}, {created, 50}, {value, OtherKM}]]}], envelope:verify_signature(Signed, AS) =:= {error, bad_signature}\") :name)")
|
|
||||||
|
|
||||||
;; Round-trip through the full pipeline:
|
|
||||||
;; construct → sign → stage_envelope → stage_signature → ok
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), AS = [{public_keys, [[{id, k1}, {created, 50}, {value, KM}]]}], envelope:validate_shape(Signed) =:= ok and envelope:verify_signature(Signed, AS) =:= ok\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 180 "$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 "outbox module loaded" "outbox"
|
|
||||||
check 10 "construct sets :actor" "true"
|
|
||||||
check 11 "construct sets :type" "true"
|
|
||||||
check 12 "construct sets :published" "true"
|
|
||||||
check 13 "construct :id is a CID" "true"
|
|
||||||
check 14 "construct deterministic" "true"
|
|
||||||
check 15 "distinct types -> distinct CIDs" "true"
|
|
||||||
check 16 "distinct ts -> distinct CIDs" "true"
|
|
||||||
check 17 "sign adds :signature" "true"
|
|
||||||
check 18 "signed verifies against key" "true"
|
|
||||||
check 19 "signed fails against wrong key" "true"
|
|
||||||
check 20 "full pipeline round-trip" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/outbox_construct.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/outbox_publish.sh — Step 6d-publish acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises outbox:publish/2 across the happy path, sig failure,
|
|
||||||
# replay halt, and envelope-shape failure. Returns shape:
|
|
||||||
# {ok, [{cid, _}, {activity, _}], NewLogState}
|
|
||||||
# {error, Reason, LogState}
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Shared prelude builds a fresh actor state, key spec, empty log,
|
|
||||||
# and a context proplist. Each test inlines it.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,50},{value,KM}]]}], {ok, L0} = log:open(alice, base), Ctx = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0}], Req = [{type,create},{object,nil}],'
|
|
||||||
|
|
||||||
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/log.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Happy path: publish returns {ok, Result, NewLog}, log tip advances
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} case outbox:publish(Req, Ctx) of {ok, _, NewLog} -> log:tip(NewLog) =:= 1; _ -> false end\") :name)")
|
|
||||||
|
|
||||||
;; Result has :cid pointing at the activity's CID
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, Result, _} = outbox:publish(Req, Ctx), {ok, Cid} = envelope:get_field(cid, Result), {ok, Act} = envelope:get_field(activity, Result), outbox:cid_of(Act) =:= Cid\") :name)")
|
|
||||||
|
|
||||||
;; The signed activity is in the log
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, Result, NewLog} = outbox:publish(Req, Ctx), {ok, Act} = envelope:get_field(activity, Result), log:entries(NewLog) =:= [Act]\") :name)")
|
|
||||||
|
|
||||||
;; Replay: second publish of identical Request halts the pipeline
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, _, L1} = outbox:publish(Req, Ctx), Ctx2 = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L1}], case outbox:publish(Req, Ctx2) of {error, replay, _} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Replay returns the pre-append LogState unchanged
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, _, L1} = outbox:publish(Req, Ctx), Ctx2 = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L1}], {error, _, L2} = outbox:publish(Req, Ctx2), log:tip(L2) =:= 1\") :name)")
|
|
||||||
|
|
||||||
;; Bad key material (sig fails) -> {error, bad_signature, LogState}
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} OtherKM = <<9,9,9,9>>, BadKS = [{key_id,k1},{algorithm,ed25519},{value,OtherKM}], BadCtx = [{actor_id,alice},{published,100},{key_spec,BadKS},{actor_state,AS},{log,L0}], case outbox:publish(Req, BadCtx) of {error, bad_signature, _} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Distinct timestamps -> two activities in log
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, _, L1} = outbox:publish(Req, Ctx), Ctx2 = [{actor_id,alice},{published,200},{key_spec,KS},{actor_state,AS},{log,L1}], {ok, _, L2} = outbox:publish(Req, Ctx2), log:tip(L2) =:= 2\") :name)")
|
|
||||||
|
|
||||||
;; Distinct types -> distinct CIDs
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, R1, L1} = outbox:publish(Req, Ctx), R2 = [{type,update},{object,nil}], Ctx2 = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L1}], {ok, R, _} = outbox:publish(R2, Ctx2), {ok, C1} = envelope:get_field(cid, R1), {ok, C2} = envelope:get_field(cid, R), C1 =/= C2\") :name)")
|
|
||||||
|
|
||||||
;; CID stable: same Request twice (across fresh logs) -> same CID
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, R1, _} = outbox:publish(Req, Ctx), {ok, L0b} = log:open(alice, base), Ctx_b = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0b}], {ok, R2, _} = outbox:publish(Req, Ctx_b), {ok, C1} = envelope:get_field(cid, R1), {ok, C2} = envelope:get_field(cid, R2), C1 =:= C2\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$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 "log module loaded" "log"
|
|
||||||
check 4 "pipeline module loaded" "pipeline"
|
|
||||||
check 5 "outbox module loaded" "outbox"
|
|
||||||
check 10 "happy path tip advances to 1" "true"
|
|
||||||
check 11 "result :cid matches activity" "true"
|
|
||||||
check 12 "signed activity in log entries" "true"
|
|
||||||
check 13 "duplicate publish -> replay" "ok"
|
|
||||||
check 14 "replay leaves log tip at 1" "true"
|
|
||||||
check 15 "bad key material -> bad_signature" "ok"
|
|
||||||
check 16 "distinct timestamps -> tip 2" "true"
|
|
||||||
check 17 "distinct types -> distinct CIDs" "true"
|
|
||||||
check 18 "same request -> same CID" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/outbox_publish.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/pipeline_driver.sh — Step 6a acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises the pipeline driver: pipeline:run_stages/2,
|
|
||||||
# validate_inbound/1, validate_outbound/1, inbound_stages/0,
|
|
||||||
# outbound_stages/0. Concrete stages land in 6b+. 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/pipeline.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Empty stage list returns ok
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"pipeline:run_stages(anything, []) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; All-ok stages return ok
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"pipeline:run_stages(anything, [fun (_) -> ok end, fun (_) -> ok end, fun (_) -> ok end]) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; First failing stage halts; later stages do not run
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"pipeline:run_stages(anything, [fun (_) -> ok end, fun (_) -> {error, halt_here} end, fun (_) -> {error, after_halt} end]) =:= {error, halt_here}\") :name)")
|
|
||||||
|
|
||||||
;; Single failing stage returns its error
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"pipeline:run_stages(anything, [fun (_) -> {error, bad} end]) =:= {error, bad}\") :name)")
|
|
||||||
|
|
||||||
;; Stage receives the activity verbatim
|
|
||||||
(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)")
|
|
||||||
|
|
||||||
;; inbound_stages / outbound_stages are lists (concrete stages
|
|
||||||
;; tested in pipeline_envelope.sh; we just confirm they're lists).
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_list(pipeline:inbound_stages())\") :name)")
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_list(pipeline:outbound_stages())\") :name)")
|
|
||||||
|
|
||||||
;; Driver-only invariants: explicit empty list with the wrappers
|
|
||||||
;; semantics is exercised via run_stages directly.
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"pipeline:run_stages(anything, []) =:= ok\") :name)")
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"pipeline:run_stages(my_act, [fun (_) -> ok end]) =:= ok\") :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 "module load name" "pipeline"
|
|
||||||
check 10 "empty stage list -> ok" "true"
|
|
||||||
check 11 "all-ok stages -> ok" "true"
|
|
||||||
check 12 "first failure halts pipeline" "true"
|
|
||||||
check 13 "single failing stage" "true"
|
|
||||||
check 14 "stage receives activity verbatim" "true"
|
|
||||||
check 15 "inbound_stages is a list" "true"
|
|
||||||
check 16 "outbound_stages is a list" "true"
|
|
||||||
check 17 "run_stages empty -> ok" "true"
|
|
||||||
check 18 "run_stages single ok stage" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/pipeline_driver.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/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 ]
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/pipeline_replay.sh — Step 6c acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises pipeline:stage_replay/2 (direct) and stage_replay/1
|
|
||||||
# (factory) against the in-memory log from Step 3a. Composability
|
|
||||||
# with stage_envelope verified. 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/log.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
|
|
||||||
;; New activity in an empty log is ok
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), Act = [{id, a1}, {type, create}], pipeline:stage_replay(Act, L) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Same activity already in log -> {error, replay}
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), Act = [{id, a1}, {type, create}], {ok, L1, _} = log:append(L0, Act), pipeline:stage_replay(Act, L1) =:= {error, replay}\") :name)")
|
|
||||||
|
|
||||||
;; Different :id is still ok even if log non-empty
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, _} = log:append(L0, [{id, a1}, {type, create}]), pipeline:stage_replay([{id, a2}, {type, create}], L1) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; No :id field -> {error, no_id}
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), pipeline:stage_replay([{type, create}], L) =:= {error, no_id}\") :name)")
|
|
||||||
|
|
||||||
;; Match against the second log entry (linear scan walks all entries)
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), {ok, L1, _} = log:append(L0, [{id, a1}, {type, create}]), {ok, L2, _} = log:append(L1, [{id, a2}, {type, create}]), pipeline:stage_replay([{id, a2}, {type, update}], L2) =:= {error, replay}\") :name)")
|
|
||||||
|
|
||||||
;; stage_replay/1 factory returns a fun
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), is_function(pipeline:stage_replay(L))\") :name)")
|
|
||||||
|
|
||||||
;; Factory + run_stages: fresh activity flows through
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), Act = [{id, a1}, {type, create}], Stages = [pipeline:stage_replay(L)], pipeline:run_stages(Act, Stages) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Factory + run_stages: replay halts the pipeline
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), Act = [{id, a1}, {type, create}], {ok, L1, _} = log:append(L0, Act), Stages = [pipeline:stage_replay(L1)], pipeline:run_stages(Act, Stages) =:= {error, replay}\") :name)")
|
|
||||||
|
|
||||||
;; Composed with stage_envelope: envelope error precedes replay check
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, L0} = log:open(alice, base), Act = [{id, a1}, {type, create}, {actor, a}, {published, 1}, {signature, [{key_id, k}, {algorithm, e}, {value, v}]}], {ok, L1, _} = log:append(L0, Act), Stages = [fun (A) -> pipeline:stage_envelope(A) end, pipeline:stage_replay(L1)], pipeline:run_stages(Act, Stages) =:= {error, replay}\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 180 "$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 "log module loaded" "log"
|
|
||||||
check 4 "pipeline module loaded" "pipeline"
|
|
||||||
check 10 "new activity in empty log -> ok" "true"
|
|
||||||
check 11 "same id -> {error, replay}" "true"
|
|
||||||
check 12 "different id still ok" "true"
|
|
||||||
check 13 "no :id -> {error, no_id}" "true"
|
|
||||||
check 14 "match second log entry" "true"
|
|
||||||
check 15 "stage_replay/1 returns fun" "true"
|
|
||||||
check 16 "factory + run_stages: ok" "true"
|
|
||||||
check 17 "factory + run_stages: halts" "true"
|
|
||||||
check 18 "composed envelope+replay halts" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/pipeline_replay.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/pipeline_schema.sh — Step 6c-schema-pure test.
|
|
||||||
#
|
|
||||||
# Exercises stage_schema/2 (direct call) and stage_schema/1
|
|
||||||
# (factory). The SchemaLookup callback returns either
|
|
||||||
# {ok, SchemaFn} or not_found; open-world default means
|
|
||||||
# not_found resolves to ok. 12 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
|
|
||||||
|
|
||||||
# Common: a strict Pin schema requires Object to have :path and :cid
|
|
||||||
# `PinSchema = fun (Obj) -> ...`.
|
|
||||||
PRELUDE='PinSchema = fun (Obj) -> case envelope:get_field(path, Obj) of {ok, _} -> case envelope:get_field(cid, Obj) of {ok, _} -> true; _ -> false end; _ -> false end end, PinLookup = fun (pin) -> {ok, PinSchema}; (_) -> not_found end,'
|
|
||||||
|
|
||||||
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)")
|
|
||||||
|
|
||||||
;; Open-world default: unknown type returns ok
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"NoLookup = fun (_) -> not_found end, pipeline:stage_schema([{type, foo}, {object, bar}], NoLookup) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Activity without :type -> {error, no_type}
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"NoLookup = fun (_) -> not_found end, pipeline:stage_schema([{object, x}], NoLookup) =:= {error, no_type}\") :name)")
|
|
||||||
|
|
||||||
;; Known type, schema passes -> ok
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Act = [{type, pin}, {object, [{path, <<47,97>>}, {cid, <<98>>}]}], pipeline:stage_schema(Act, PinLookup) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Known type, schema fails -> {error, schema_mismatch}
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Act = [{type, pin}, {object, [{path, <<47,97>>}]}], pipeline:stage_schema(Act, PinLookup) =:= {error, schema_mismatch}\") :name)")
|
|
||||||
|
|
||||||
;; Activity with no :object skips schema check
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} pipeline:stage_schema([{type, pin}], PinLookup) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; stage_schema/1 returns a function
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_function(pipeline:stage_schema(fun (_) -> not_found end))\") :name)")
|
|
||||||
|
|
||||||
;; Factory + activity -> applies the lookup
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Stage = pipeline:stage_schema(PinLookup), Stage([{type, pin}, {object, [{path, <<1>>}, {cid, <<2>>}]}]) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Factory + bad activity -> schema_mismatch
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Stage = pipeline:stage_schema(PinLookup), Stage([{type, pin}, {object, [{path, <<1>>}]}]) =:= {error, schema_mismatch}\") :name)")
|
|
||||||
|
|
||||||
;; Composed with stage_envelope via run_stages: bad envelope halts first
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Stages = [fun (A) -> pipeline:stage_envelope(A) end, pipeline:stage_schema(PinLookup)], case pipeline:run_stages([{type, pin}], Stages) of {error, {missing_field, _}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Composed: envelope ok + schema fail -> schema_mismatch
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Act = [{id, 1}, {type, pin}, {actor, alice}, {published, 1}, {signature, [{key_id, k}, {algorithm, e}, {value, v}]}, {object, [{path, <<1>>}]}], Stages = [fun (A) -> pipeline:stage_envelope(A) end, pipeline:stage_schema(PinLookup)], pipeline:run_stages(Act, Stages) =:= {error, schema_mismatch}\") :name)")
|
|
||||||
|
|
||||||
;; Schema fn receives the object (verify by mutating an Erlang process flag isn't reliable; instead capture & test inside the schema)
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"Captor = fun (Obj) -> envelope:get_field(target, Obj) =:= {ok, mark} end, Lookup = fun (_) -> {ok, Captor} end, pipeline:stage_schema([{type, t}, {object, [{target, mark}]}], Lookup) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Multiple types registered: only matching one consulted
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"PinF = fun (_) -> true end, NoteF = fun (_) -> false end, Multi = fun (pin) -> {ok, PinF}; (note) -> {ok, NoteF}; (_) -> not_found end, {pipeline:stage_schema([{type, pin}, {object, ignored}], Multi), pipeline:stage_schema([{type, note}, {object, ignored}], Multi), pipeline:stage_schema([{type, other}, {object, ignored}], Multi)} =:= {ok, {error, schema_mismatch}, ok}\") :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 "open-world default for unknown" "true"
|
|
||||||
check 11 "no :type -> no_type error" "true"
|
|
||||||
check 12 "schema accepts -> ok" "true"
|
|
||||||
check 13 "schema rejects -> mismatch" "true"
|
|
||||||
check 14 "no :object skips check" "true"
|
|
||||||
check 15 "stage_schema/1 returns fun" "true"
|
|
||||||
check 16 "factory + ok" "true"
|
|
||||||
check 17 "factory + mismatch" "true"
|
|
||||||
check 18 "envelope halt before schema" "ok"
|
|
||||||
check 19 "envelope ok + schema mismatch" "true"
|
|
||||||
check 20 "schema fn receives object" "true"
|
|
||||||
check 21 "multi-type lookup dispatches" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/pipeline_schema.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/pipeline_signature.sh — Step 6b-sig acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises pipeline:stage_signature/2 (direct) and stage_signature/1
|
|
||||||
# (factory). The factory returns a 1-arity stage fun bound to the
|
|
||||||
# given actor-state so it can be folded into a stage list by the
|
|
||||||
# pipeline driver alongside stage_envelope. 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
|
|
||||||
|
|
||||||
# Shared Erlang prelude builds a valid signed envelope + actor
|
|
||||||
# state — same shape as next/tests/envelope_sig.sh from Step 2c.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, U = [{actor,alice},{id,1},{published,100},{type,create}], CB = envelope:canonical_bytes(U), Sig = crypto:hash(sha256, <<KM/binary, CB/binary>>), Env = [{actor,alice},{id,1},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,Sig}]}], AS = [{public_keys, [[{id,k1},{created,50},{value,KM}]]}],'
|
|
||||||
|
|
||||||
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)")
|
|
||||||
|
|
||||||
;; Direct 2-arity stage_signature on a valid signed envelope returns ok
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} pipeline:stage_signature(Env, AS) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Tampered envelope returns the proper error tag
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Tampered = [{actor,alice},{id,999},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,Sig}]}], pipeline:stage_signature(Tampered, AS) =:= {error,bad_signature}\") :name)")
|
|
||||||
|
|
||||||
;; Missing signature -> no_signature
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} pipeline:stage_signature(U, AS) =:= {error,no_signature}\") :name)")
|
|
||||||
|
|
||||||
;; stage_signature/1 returns a function
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_function(pipeline:stage_signature([{public_keys, []}]))\") :name)")
|
|
||||||
|
|
||||||
;; stage_signature/1 factory: built stage returns ok on valid input
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Stage = pipeline:stage_signature(AS), Stage(Env) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; stage_signature/1 factory: built stage returns error on tampered input
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Stage = pipeline:stage_signature(AS), Tampered = [{actor,alice},{id,999},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,Sig}]}], Stage(Tampered) =:= {error,bad_signature}\") :name)")
|
|
||||||
|
|
||||||
;; Composable: envelope + signature stages folded together via run_stages
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Stages = [fun (A) -> pipeline:stage_envelope(A) end, pipeline:stage_signature(AS)], pipeline:run_stages(Env, Stages) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; Composable + halt: envelope stage fails first, signature never runs
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} BadShape = [{type,create}], Stages = [fun (A) -> pipeline:stage_envelope(A) end, pipeline:stage_signature(AS)], case pipeline:run_stages(BadShape, Stages) of {error, {missing_field, _}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Composable + halt: envelope OK, signature fails -> sig error surfaces
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Tampered = [{actor,alice},{id,999},{published,100},{type,create},{signature,[{algorithm,ed25519},{key_id,k1},{value,Sig}]}], Stages = [fun (A) -> pipeline:stage_envelope(A) end, pipeline:stage_signature(AS)], pipeline:run_stages(Tampered, Stages) =:= {error,bad_signature}\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 180 "$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 "stage_signature/2 valid -> ok" "true"
|
|
||||||
check 11 "stage_signature/2 tampered" "true"
|
|
||||||
check 12 "stage_signature/2 no sig" "true"
|
|
||||||
check 13 "stage_signature/1 returns fun" "true"
|
|
||||||
check 14 "factory stage valid -> ok" "true"
|
|
||||||
check 15 "factory stage tampered" "true"
|
|
||||||
check 16 "envelope+sig composed ok" "true"
|
|
||||||
check 17 "halt on envelope before sig" "ok"
|
|
||||||
check 18 "sig error after envelope ok" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/pipeline_signature.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/projection_pure.sh — Step 7a acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises the pure-functional projection driver:
|
|
||||||
# new/2,3, fold_activity/2, replay/2, name/1, state/1, fold_fn/1.
|
|
||||||
# Fold bodies are Erlang funs in v1; SX-source eval bridge will
|
|
||||||
# plug into the same record later. 12 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/projection.erl\")) :name)")
|
|
||||||
|
|
||||||
;; new/2 sets initial state to the supplied value
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = projection:new(activity_log, init_state), projection:state(P) =:= init_state\") :name)")
|
|
||||||
|
|
||||||
;; new/2 default fold is identity
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = projection:new(activity_log, base), P1 = projection:fold_activity(P, anything), projection:state(P1) =:= base\") :name)")
|
|
||||||
|
|
||||||
;; new/3 stores supplied fold
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = projection:new(counter, 0, fun (_A, S) -> S + 1 end), is_function(projection:fold_fn(P))\") :name)")
|
|
||||||
|
|
||||||
;; fold_activity threads through the fold fn
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(erlang-eval-ast \"P = projection:new(counter, 0, fun (_A, S) -> S + 1 end), P1 = projection:fold_activity(P, x), projection:state(P1)\")")
|
|
||||||
|
|
||||||
;; Two fold_activity calls accumulate
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"P = projection:new(counter, 0, fun (_A, S) -> S + 1 end), P1 = projection:fold_activity(P, a), P2 = projection:fold_activity(P1, b), projection:state(P2)\")")
|
|
||||||
|
|
||||||
;; replay over a list
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(erlang-eval-ast \"P = projection:new(counter, 0, fun (_A, S) -> S + 1 end), P1 = projection:replay(P, [a, b, c, d, e]), projection:state(P1)\")")
|
|
||||||
|
|
||||||
;; replay over [] returns the projection unchanged (state preserved)
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(erlang-eval-ast \"P = projection:new(counter, 99, fun (_A, S) -> S + 1 end), P1 = projection:replay(P, []), projection:state(P1)\")")
|
|
||||||
|
|
||||||
;; Fold can read activity content (here append it)
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"P = projection:new(byname, [], fun (A, S) -> [A | S] end), P1 = projection:replay(P, [a, b, c]), projection:state(P1) =:= [c, b, a]\") :name)")
|
|
||||||
|
|
||||||
;; Different projections are independent (different fold bodies)
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"P1 = projection:new(p_count, 0, fun (_A, S) -> S + 1 end), P2 = projection:new(p_collect, [], fun (A, S) -> [A | S] end), R1 = projection:replay(P1, [a, b, c]), R2 = projection:replay(P2, [a, b, c]), {projection:state(R1), projection:state(R2)} =:= {3, [c, b, a]}\") :name)")
|
|
||||||
|
|
||||||
;; Name accessor
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"projection:name(projection:new(some_name, init)) =:= some_name\") :name)")
|
|
||||||
|
|
||||||
;; Multi-step replay: aggregator by activity tag
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"By = fun (A, S) -> case A of {tag, T} -> [T | S]; _ -> S end end, P = projection:new(tag_log, [], By), P1 = projection:replay(P, [{tag, foo}, plain, {tag, bar}, {tag, baz}]), projection:state(P1) =:= [baz, bar, foo]\") :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 "module load name" "projection"
|
|
||||||
check 10 "new/2 stores initial state" "true"
|
|
||||||
check 11 "default fold is identity" "true"
|
|
||||||
check 12 "new/3 stores fold fn" "true"
|
|
||||||
check 13 "fold_activity threads fn" "1"
|
|
||||||
check 14 "two folds accumulate" "2"
|
|
||||||
check 15 "replay over 5 activities" "5"
|
|
||||||
check 16 "replay over [] preserves state" "99"
|
|
||||||
check 17 "fold can read activity content" "true"
|
|
||||||
check 18 "different projections indep." "true"
|
|
||||||
check 19 "name accessor" "true"
|
|
||||||
check 20 "tag-aware fold (replay)" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/projection_pure.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/projection_server.sh — Step 7b acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises gen_server-per-projection: start_link/3, async_fold/2,
|
|
||||||
# query/1. Each test inlines start_link with operations because
|
|
||||||
# the Erlang-on-SX scheduler doesn't preserve processes across
|
|
||||||
# separate erlang-eval-ast invocations. 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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
|
|
||||||
;; start_link returns a Pid registered under the given name
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_pid(projection:start_link(p1, 0, fun (_A, S) -> S + 1 end))\") :name)")
|
|
||||||
|
|
||||||
;; query before any async_fold returns initial state
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"projection:start_link(p1, 0, fun (_A, S) -> S + 1 end), projection:query(p1)\")")
|
|
||||||
|
|
||||||
;; Single async_fold + query returns new state
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(erlang-eval-ast \"projection:start_link(p1, 0, fun (_A, S) -> S + 1 end), projection:async_fold(p1, a), projection:query(p1)\")")
|
|
||||||
|
|
||||||
;; Five async_folds accumulate
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(erlang-eval-ast \"projection:start_link(p1, 0, fun (_A, S) -> S + 1 end), projection:async_fold(p1, 1), projection:async_fold(p1, 2), projection:async_fold(p1, 3), projection:async_fold(p1, 4), projection:async_fold(p1, 5), projection:query(p1)\")")
|
|
||||||
|
|
||||||
;; Custom initial state preserved
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"projection:start_link(p1, 42, fun (A, S) -> S + A end), projection:query(p1)\")")
|
|
||||||
|
|
||||||
;; Fold can read the activity (sum activities)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(erlang-eval-ast \"projection:start_link(p1, 0, fun (A, S) -> S + A end), projection:async_fold(p1, 10), projection:async_fold(p1, 20), projection:async_fold(p1, 30), projection:query(p1)\")")
|
|
||||||
|
|
||||||
;; List-append fold preserves insertion order (newest-first)
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"projection:start_link(p1, [], fun (A, S) -> [A | S] end), projection:async_fold(p1, a), projection:async_fold(p1, b), projection:async_fold(p1, c), projection:query(p1) =:= [c, b, a]\") :name)")
|
|
||||||
|
|
||||||
;; Two named projections are independent
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"projection:start_link(p1, 0, fun (_A, S) -> S + 1 end), projection:start_link(p2, [], fun (A, S) -> [A | S] end), projection:async_fold(p1, x), projection:async_fold(p1, y), projection:async_fold(p2, x), {projection:query(p1), projection:query(p2)} =:= {2, [x]}\") :name)")
|
|
||||||
|
|
||||||
;; Conditional fold (filter on activity tag)
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(erlang-eval-ast \"projection:start_link(p1, 0, fun (A, S) -> case A of {keep, _} -> S + 1; _ -> S end end), projection:async_fold(p1, {keep, a}), projection:async_fold(p1, plain), projection:async_fold(p1, {keep, b}), projection:async_fold(p1, plain), projection:query(p1)\")")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 180 "$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 "gen_server loaded" "gen_server"
|
|
||||||
check 3 "projection module loaded" "projection"
|
|
||||||
check 10 "start_link returns Pid" "true"
|
|
||||||
check 11 "initial state via query" "0"
|
|
||||||
check 12 "async_fold + query" "1"
|
|
||||||
check 13 "five async_folds accumulate" "5"
|
|
||||||
check 14 "custom initial state" "42"
|
|
||||||
check 15 "fold reads activity (sum)" "60"
|
|
||||||
check 16 "list-append fold order" "true"
|
|
||||||
check 17 "two named projections indep." "true"
|
|
||||||
check 18 "conditional fold (filter)" "2"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/projection_server.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/registry_pure.sh — Step 5a acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises the pure-functional registry API: new/0, kinds/0,
|
|
||||||
# register/4, lookup/3, list/2. State threading is verified
|
|
||||||
# by chaining register calls and inspecting the final state.
|
|
||||||
# 13 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/registry.erl\")) :name)")
|
|
||||||
|
|
||||||
;; new/0 returns []
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:new() =:= []\") :name)")
|
|
||||||
|
|
||||||
;; kinds/0 has 7 entries
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"length(registry:kinds())\")")
|
|
||||||
|
|
||||||
;; kinds/0 includes activity_types
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"lists:member(activity_types, registry:kinds())\") :name)")
|
|
||||||
|
|
||||||
;; register + lookup round-trip
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, S} = registry:register(activity_types, create, [{cid, c1}], registry:new()), registry:lookup(activity_types, create, S) =:= {ok, [{cid, c1}]}\") :name)")
|
|
||||||
|
|
||||||
;; lookup on empty registry returns not_found
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:lookup(activity_types, anything, registry:new()) =:= not_found\") :name)")
|
|
||||||
|
|
||||||
;; lookup on unknown kind returns {error, unknown_kind}
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"case registry:lookup(bogus_kind, foo, registry:new()) of {error, unknown_kind} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; register on unknown kind returns {error, unknown_kind}
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"case registry:register(bogus_kind, foo, bar, registry:new()) of {error, unknown_kind} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; list of empty kind returns []
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:list(activity_types, registry:new()) =:= []\") :name)")
|
|
||||||
|
|
||||||
;; Three registers + list returns 3 pairs
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S1} = registry:register(activity_types, create, e1, registry:new()), {ok, S2} = registry:register(activity_types, update, e2, S1), {ok, S3} = registry:register(activity_types, delete, e3, S2), length(registry:list(activity_types, S3))\")")
|
|
||||||
|
|
||||||
;; Re-registering same name overrides previous entry
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"{ok, S1} = registry:register(activity_types, create, v1, registry:new()), {ok, S2} = registry:register(activity_types, create, v2, S1), registry:lookup(activity_types, create, S2) =:= {ok, v2}\") :name)")
|
|
||||||
|
|
||||||
;; Re-registering same name keeps list length at 1
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S1} = registry:register(activity_types, create, v1, registry:new()), {ok, S2} = registry:register(activity_types, create, v2, S1), length(registry:list(activity_types, S2))\")")
|
|
||||||
|
|
||||||
;; Different kinds are independent
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(erlang-eval-ast \"{ok, S1} = registry:register(activity_types, x, 1, registry:new()), {ok, S2} = registry:register(object_types, x, 2, S1), {registry:lookup(activity_types, x, S2), registry:lookup(object_types, x, S2)} =:= {{ok, 1}, {ok, 2}}\")")
|
|
||||||
|
|
||||||
;; list on unknown kind returns {error, unknown_kind}
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"case registry:list(bogus_kind, registry:new()) of {error, unknown_kind} -> 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 "module load name" "registry"
|
|
||||||
check 10 "new/0 returns []" "true"
|
|
||||||
check 11 "kinds/0 length" "7"
|
|
||||||
check 12 "kinds/0 includes activity_types" "true"
|
|
||||||
check 13 "register + lookup round-trip" "true"
|
|
||||||
check 14 "lookup empty -> not_found" "true"
|
|
||||||
check 15 "lookup bogus kind" "ok"
|
|
||||||
check 16 "register bogus kind" "ok"
|
|
||||||
check 17 "list empty kind -> []" "true"
|
|
||||||
check 18 "three registers, list returns 3" "3"
|
|
||||||
check 19 "re-register overrides entry" "true"
|
|
||||||
check 20 "re-register doesn't grow list" "1"
|
|
||||||
check 21 "different kinds independent" "true"
|
|
||||||
check 22 "list bogus kind" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/registry_pure.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/registry_server.sh — Step 5b acceptance test.
|
|
||||||
#
|
|
||||||
# Exercises the gen_server-wrapped registry. Each test combines
|
|
||||||
# start_link + operations + assertion into a single
|
|
||||||
# erlang-eval-ast expression because the Erlang-on-SX scheduler
|
|
||||||
# does not preserve spawned processes across separate eval
|
|
||||||
# invocations. 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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/registry.erl\")) :name)")
|
|
||||||
|
|
||||||
;; start_link returns a Pid
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"is_pid(registry:start_link())\") :name)")
|
|
||||||
|
|
||||||
;; register + lookup round-trip
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:start_link(), registry:register(activity_types, create, e1), registry:lookup(activity_types, create) =:= {ok, e1}\") :name)")
|
|
||||||
|
|
||||||
;; lookup unknown name returns not_found
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:start_link(), registry:lookup(activity_types, missing) =:= not_found\") :name)")
|
|
||||||
|
|
||||||
;; register returns the atom 'ok'
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:start_link(), registry:register(object_types, note, e_n) =:= ok\") :name)")
|
|
||||||
|
|
||||||
;; list returns all pairs in a kind
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(erlang-eval-ast \"registry:start_link(), registry:register(activity_types, a, 1), registry:register(activity_types, b, 2), registry:register(activity_types, c, 3), length(registry:list(activity_types))\")")
|
|
||||||
|
|
||||||
;; Re-register overrides without growing the list
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(erlang-eval-ast \"registry:start_link(), registry:register(activity_types, a, v1), registry:register(activity_types, a, v2), length(registry:list(activity_types))\")")
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:start_link(), registry:register(activity_types, a, v1), registry:register(activity_types, a, v2), registry:lookup(activity_types, a) =:= {ok, v2}\") :name)")
|
|
||||||
|
|
||||||
;; State persists across multiple calls in the same expression
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(erlang-eval-ast \"registry:start_link(), registry:register(activity_types, x, 1), registry:register(object_types, x, 2), {registry:lookup(activity_types, x), registry:lookup(object_types, x)} =:= {{ok, 1}, {ok, 2}}\")")
|
|
||||||
|
|
||||||
;; Unknown kind rejected via gen_server too
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:start_link(), case registry:lookup(bogus_kind, foo) of {error, unknown_kind} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Empty kind list returns []
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"registry:start_link(), registry:list(validators) =:= []\") :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 "gen_server loaded" "gen_server"
|
|
||||||
check 3 "registry module loaded" "registry"
|
|
||||||
check 10 "start_link returns Pid" "true"
|
|
||||||
check 11 "register + lookup round-trip" "true"
|
|
||||||
check 12 "lookup missing -> not_found" "true"
|
|
||||||
check 13 "register returns ok atom" "true"
|
|
||||||
check 14 "three registers, list = 3" "3"
|
|
||||||
check 15 "re-register doesn't grow list" "1"
|
|
||||||
check 16 "re-register overrides value" "true"
|
|
||||||
check 17 "different kinds independent" "true"
|
|
||||||
check 18 "lookup bogus kind" "ok"
|
|
||||||
check 19 "empty kind list = []" "true"
|
|
||||||
|
|
||||||
# 12 cases total (epoch 2 + 3 are setup, but counted for honesty)
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/registry_server.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/sandbox_eval.sh — Step 7d-pure test.
|
|
||||||
#
|
|
||||||
# Exercises sandbox:eval_pure/2 and eval_pure/3. Catches all
|
|
||||||
# three exception classes (throw / error / exit) and returns
|
|
||||||
# them tagged. Successful fold-shaped (Activity, State) calls
|
|
||||||
# pass through unchanged. 13 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/sandbox.erl\")) :name)")
|
|
||||||
|
|
||||||
;; eval_pure/2 normal return
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-eval-ast \"sandbox:eval_pure(fun (X) -> X + 1 end, 41) =:= {ok, 42}\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/2 throw caught
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(get (erlang-eval-ast \"case sandbox:eval_pure(fun (_) -> throw(boom) end, 1) of {error, {throw, boom}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/2 error caught
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(get (erlang-eval-ast \"case sandbox:eval_pure(fun (_) -> erlang:error(crash) end, 1) of {error, {error, crash}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/2 exit caught
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(get (erlang-eval-ast \"case sandbox:eval_pure(fun (_) -> erlang:exit(bye) end, 1) of {error, {exit, bye}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/2 carries the original argument through
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"sandbox:eval_pure(fun (X) -> X end, marker) =:= {ok, marker}\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/2 returning a tuple is wrapped in {ok, _}
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"sandbox:eval_pure(fun (_) -> {a, b} end, 0) =:= {ok, {a, b}}\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/3 normal return (Activity, State) shape
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(get (erlang-eval-ast \"sandbox:eval_pure(fun (A, S) -> S + A end, 10, 5) =:= {ok, 15}\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/3 throw caught
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(get (erlang-eval-ast \"case sandbox:eval_pure(fun (_, _) -> throw(stop) end, x, y) of {error, {throw, stop}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/3 error caught
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(get (erlang-eval-ast \"case sandbox:eval_pure(fun (_, _) -> erlang:error(badarith) end, 1, 2) of {error, {error, badarith}} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; eval_pure/3 fold-style fun: tag activities into state
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(get (erlang-eval-ast \"Fold = fun ({tag, T}, S) -> [T | S]; (_, S) -> S end, sandbox:eval_pure(Fold, {tag, foo}, []) =:= {ok, [foo]}\") :name)")
|
|
||||||
|
|
||||||
;; Successful eval_pure does not catch silently — distinguishes ok+nil from error
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"sandbox:eval_pure(fun (_) -> nil end, 0) =:= {ok, nil}\") :name)")
|
|
||||||
|
|
||||||
;; Tuple reason inside the caught exception is preserved
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"case sandbox:eval_pure(fun (_) -> throw({bad_input, {field, x}}) end, 0) of {error, {throw, {bad_input, {field, x}}}} -> ok; _ -> bad end\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 60 "$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 "module load name" "sandbox"
|
|
||||||
check 10 "eval_pure/2 normal return" "true"
|
|
||||||
check 11 "eval_pure/2 throw caught" "ok"
|
|
||||||
check 12 "eval_pure/2 error caught" "ok"
|
|
||||||
check 13 "eval_pure/2 exit caught" "ok"
|
|
||||||
check 14 "eval_pure/2 arg passthrough" "true"
|
|
||||||
check 15 "eval_pure/2 tuple wrapped in ok" "true"
|
|
||||||
check 16 "eval_pure/3 fold-shape success" "true"
|
|
||||||
check 17 "eval_pure/3 throw caught" "ok"
|
|
||||||
check 18 "eval_pure/3 error caught" "ok"
|
|
||||||
check 19 "eval_pure/3 tag-fold body" "true"
|
|
||||||
check 20 "ok+nil distinct from error" "true"
|
|
||||||
check 21 "tuple reason preserved" "ok"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/sandbox_eval.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/smoke_app_pure.sh — Step 9b-pure smoke test.
|
|
||||||
#
|
|
||||||
# Mirrors §Step 9b structurally without TCP/curl/JSON. A trigger
|
|
||||||
# projection (Erlang fun) matches Note activities tagged
|
|
||||||
# "smoketest", constructs a derived TestEcho activity carrying
|
|
||||||
# the Note's CID via :echoes, and captures it into projection
|
|
||||||
# state. Proves the reactive-application mechanism — match-then-
|
|
||||||
# derive — works end-to-end through nx_kernel's broadcast.
|
|
||||||
#
|
|
||||||
# Cascade publication (the trigger actually publishing the
|
|
||||||
# derived activity back through outbox) is sidestepped to avoid
|
|
||||||
# gen_server reentrancy; the projection state is the proof point.
|
|
||||||
# 12 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
|
|
||||||
|
|
||||||
# Shared prelude — KM/KS/AS, the Match function (Note +
|
|
||||||
# smoketest tag), the trigger fold body, and various activity
|
|
||||||
# proplists.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], Match = fun (Act) -> case envelope:get_field(type, Act) of {ok, note} -> case envelope:get_field(object, Act) of {ok, Obj} -> case envelope:get_field(tags, Obj) of {ok, Tags} -> lists:member(smoketest, Tags); _ -> false end; _ -> false end; _ -> false end end, TrigFold = fun (Act, {Captured, Count}) -> case Match(Act) of true -> {ok, Id} = envelope:get_field(id, Act), Derived = [{type, test_echo}, {object, [{echoes, Id}]}], {[Derived | Captured], Count + 1}; false -> {Captured, Count} end end, projection:start_link(trig, {[], 0}, TrigFold), nx_kernel:start_link(alice, KS, AS), nx_kernel:with_projections([trig]), MatchNote = [{type, note}, {object, [{content, hi}, {tags, [smoketest]}]}], NoMatchNote = [{type, note}, {object, [{content, plain}, {tags, [other]}]}],'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 8)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Initial: no triggers fired
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Matching Note fires the trigger once
|
|
||||||
(epoch 11)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish(MatchNote), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Non-matching Note does NOT fire trigger
|
|
||||||
(epoch 12)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish(NoMatchNote), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Mix: one match + one non-match -> trigger fires exactly once
|
|
||||||
(epoch 13)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish(MatchNote), nx_kernel:publish(NoMatchNote), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Trigger captures the derived TestEcho
|
|
||||||
(epoch 14)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(MatchNote), {[Derived], _} = projection:query(trig), envelope:get_field(type, Derived) =:= {ok, test_echo}\") :name)")
|
|
||||||
|
|
||||||
;; Derived TestEcho :echoes points at the Note's :id (CID)
|
|
||||||
(epoch 15)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(MatchNote), {[Derived], _} = projection:query(trig), {ok, Obj} = envelope:get_field(object, Derived), {ok, EchoesId} = envelope:get_field(echoes, Obj), [Logged] = log:entries(nx_kernel:log_state(nx_kernel:query())), {ok, LoggedId} = envelope:get_field(id, Logged), EchoesId =:= LoggedId\") :name)")
|
|
||||||
|
|
||||||
;; Two matching Notes -> trigger fires twice, captures both derived
|
|
||||||
(epoch 16)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish(MatchNote), MatchNote2 = [{type, note}, {object, [{content, hello}, {tags, [smoketest]}]}], nx_kernel:publish(MatchNote2), {Captured, Count} = projection:query(trig), {length(Captured), Count}\")")
|
|
||||||
|
|
||||||
;; Trigger ignores non-Note activities even if they have :tags
|
|
||||||
(epoch 17)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} OtherType = [{type, pin}, {object, [{tags, [smoketest]}, {path, p}, {cid, c}]}], nx_kernel:publish(OtherType), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Trigger ignores Note without :tags
|
|
||||||
(epoch 18)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} NoTag = [{type, note}, {object, [{content, hi}]}], nx_kernel:publish(NoTag), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Multiple tags including smoketest -> matches
|
|
||||||
(epoch 19)
|
|
||||||
(eval "(erlang-eval-ast \"${PRELUDE} Many = [{type, note}, {object, [{content, hi}, {tags, [smoketest, foo, bar]}]}], nx_kernel:publish(Many), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
|
|
||||||
;; Sig-failed publish doesn't reach the trigger
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(erlang-eval-ast \"OtherKM = <<9,9,9,9>>, BadKS = [{key_id,k1},{algorithm,ed25519},{value,OtherKM}], AS = [{public_keys,[[{id,k1},{created,0},{value,<<1,2,3,4>>}]]}], Match = fun (Act) -> case envelope:get_field(type, Act) of {ok, note} -> case envelope:get_field(object, Act) of {ok, Obj} -> case envelope:get_field(tags, Obj) of {ok, Tags} -> lists:member(smoketest, Tags); _ -> false end; _ -> false end; _ -> false end end, TrigFold = fun (Act, {Captured, Count}) -> case Match(Act) of true -> {ok, Id} = envelope:get_field(id, Act), Derived = [{type, test_echo}, {object, [{echoes, Id}]}], {[Derived | Captured], Count + 1}; false -> {Captured, Count} end end, projection:start_link(trig, {[], 0}, TrigFold), nx_kernel:start_link(alice, BadKS, AS), nx_kernel:with_projections([trig]), MatchNote = [{type, note}, {object, [{content, hi}, {tags, [smoketest]}]}], nx_kernel:publish(MatchNote), {_, Count} = projection:query(trig), Count\")")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 300 "$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 8 "nx_kernel module loaded" "nx_kernel"
|
|
||||||
check 10 "initial Count = 0" "0"
|
|
||||||
check 11 "Match fires once" "1"
|
|
||||||
check 12 "Non-match does NOT fire" "0"
|
|
||||||
check 13 "Mix: only match fires" "1"
|
|
||||||
check 14 "Derived type = test_echo" "true"
|
|
||||||
check 15 "Derived :echoes = Note's :id" "true"
|
|
||||||
check 16 "Two matches -> 2 derived, count 2" "(2 2)"
|
|
||||||
check 17 "Non-Note ignored" "0"
|
|
||||||
check 18 "Note without tags ignored" "0"
|
|
||||||
check 19 "Multi-tag includes smoketest" "1"
|
|
||||||
check 20 "Sig failure -> no trigger" "0"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/smoke_app_pure.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# next/tests/smoke_pin_pure.sh — Step 9a-pure smoke test.
|
|
||||||
#
|
|
||||||
# Mirrors plans/fed-sx-milestone-1.md §Step 9a but without TCP /
|
|
||||||
# curl / JSON. Exercises Pin-verb extensibility end-to-end:
|
|
||||||
# 1. define_registry fold projection registers DefineActivity
|
|
||||||
# 2. A pin-state projection (Erlang fun) folds Pin activities
|
|
||||||
# 3. Both projections wired into nx_kernel
|
|
||||||
# 4. Publish Create{DefineActivity{name: pin}} -> registry update
|
|
||||||
# 5. Publish Pin{path:..., cid:...} -> pin-state update
|
|
||||||
#
|
|
||||||
# Proves the meta-projection + verb-fold mechanism is wired
|
|
||||||
# correctly. The remaining Step 9a deliverable (curl smoke test)
|
|
||||||
# layers TCP on top — needs Step 8b-start. 14 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
|
|
||||||
|
|
||||||
# Shared prelude — starts kernel + two projections, wires them in,
|
|
||||||
# binds DefineAct and PinAct ready to publish.
|
|
||||||
PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], PinFold = fun (Act, S) -> case envelope:get_field(type, Act) of {ok, pin} -> case envelope:get_field(object, Act) of {ok, Obj} -> {ok, P} = envelope:get_field(path, Obj), {ok, C} = envelope:get_field(cid, Obj), [{P, C} | S]; _ -> S end; _ -> S end end, projection:start_link(define_reg, registry:new(), define_registry:fold_fn()), projection:start_link(pin_state, [], PinFold), nx_kernel:start_link(alice, KS, AS), nx_kernel:with_projections([define_reg, pin_state]), DefineAct = [{type, create}, {object, [{type, define_activity}, {name, pin}]}], PinAct = [{type, pin}, {object, [{path, docs_intro}, {cid, qm_cid_1}]}],'
|
|
||||||
|
|
||||||
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 "(er-load-gen-server!)")
|
|
||||||
(epoch 3)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
|
||||||
(epoch 4)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)")
|
|
||||||
(epoch 5)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
|
||||||
(epoch 6)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/registry.erl\")) :name)")
|
|
||||||
(epoch 7)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/projection.erl\")) :name)")
|
|
||||||
(epoch 8)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
|
||||||
(epoch 9)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/nx_kernel.erl\")) :name)")
|
|
||||||
(epoch 10)
|
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/define_registry.erl\")) :name)")
|
|
||||||
|
|
||||||
;; Initial state: pin_state empty
|
|
||||||
(epoch 20)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} projection:query(pin_state) =:= []\") :name)")
|
|
||||||
|
|
||||||
;; Initial state: pin NOT in registry
|
|
||||||
(epoch 21)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} registry:lookup(activity_types, pin, projection:query(define_reg)) =:= not_found\") :name)")
|
|
||||||
|
|
||||||
;; Step 1: Publish DefineActivity{pin}, then pin IS in the registry
|
|
||||||
(epoch 22)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(DefineAct), case registry:lookup(activity_types, pin, projection:query(define_reg)) of {ok, _} -> ok; _ -> bad end\") :name)")
|
|
||||||
|
|
||||||
;; Define activity does NOT advance pin_state
|
|
||||||
(epoch 23)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(DefineAct), projection:query(pin_state) =:= []\") :name)")
|
|
||||||
|
|
||||||
;; Step 2: Publish Pin activity, pin_state has the {path, cid}
|
|
||||||
(epoch 24)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(PinAct), projection:query(pin_state) =:= [{docs_intro, qm_cid_1}]\") :name)")
|
|
||||||
|
|
||||||
;; Pin activity does NOT add to the registry
|
|
||||||
(epoch 25)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(PinAct), length(registry:list(activity_types, projection:query(define_reg))) =:= 0\") :name)")
|
|
||||||
|
|
||||||
;; Both publishes interleaved — order independent
|
|
||||||
(epoch 26)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(DefineAct), nx_kernel:publish(PinAct), {projection:query(pin_state), case registry:lookup(activity_types, pin, projection:query(define_reg)) of {ok, _} -> registered; _ -> unregistered end} =:= {[{docs_intro, qm_cid_1}], registered}\") :name)")
|
|
||||||
|
|
||||||
;; Reverse order: publish Pin FIRST, then DefineActivity — Pin still folds
|
|
||||||
(epoch 27)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(PinAct), nx_kernel:publish(DefineAct), {projection:query(pin_state), case registry:lookup(activity_types, pin, projection:query(define_reg)) of {ok, _} -> registered; _ -> unregistered end} =:= {[{docs_intro, qm_cid_1}], registered}\") :name)")
|
|
||||||
|
|
||||||
;; Two Pins -> two entries in pin_state (newest-first)
|
|
||||||
(epoch 28)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} PinAct2 = [{type, pin}, {object, [{path, docs_arch}, {cid, qm_cid_2}]}], nx_kernel:publish(PinAct), nx_kernel:publish(PinAct2), projection:query(pin_state) =:= [{docs_arch, qm_cid_2}, {docs_intro, qm_cid_1}]\") :name)")
|
|
||||||
|
|
||||||
;; Log tip advances with each publish
|
|
||||||
(epoch 29)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:publish(DefineAct), nx_kernel:publish(PinAct), nx_kernel:log_tip() =:= 2\") :name)")
|
|
||||||
|
|
||||||
;; Multiple DefineActivity registrations (different names) accumulate
|
|
||||||
(epoch 30)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Foo = [{type, create}, {object, [{type, define_activity}, {name, foo}]}], nx_kernel:publish(DefineAct), nx_kernel:publish(Foo), length(registry:list(activity_types, projection:query(define_reg))) =:= 2\") :name)")
|
|
||||||
|
|
||||||
;; pin_state survives an empty-publish round (non-Pin doesn't disturb)
|
|
||||||
(epoch 31)
|
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} Other = [{type, create}, {object, [{type, note}, {content, hi}]}], nx_kernel:publish(PinAct), nx_kernel:publish(Other), projection:query(pin_state) =:= [{docs_intro, qm_cid_1}]\") :name)")
|
|
||||||
EPOCHS
|
|
||||||
|
|
||||||
OUTPUT=$(timeout 300 "$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 10 "define_registry loaded" "define_registry"
|
|
||||||
check 20 "initial pin_state is []" "true"
|
|
||||||
check 21 "pin not in registry initially" "true"
|
|
||||||
check 22 "DefineActivity registers pin" "ok"
|
|
||||||
check 23 "DefineActivity skips pin_state" "true"
|
|
||||||
check 24 "Pin advances pin_state" "true"
|
|
||||||
check 25 "Pin doesn't register a type" "true"
|
|
||||||
check 26 "both publishes: both states ok" "true"
|
|
||||||
check 27 "reverse order works too" "true"
|
|
||||||
check 28 "two Pins -> two entries" "true"
|
|
||||||
check 29 "log tip after two publishes" "true"
|
|
||||||
check 30 "two DefineActivities accumulate" "true"
|
|
||||||
check 31 "Note doesn't disturb pin_state" "true"
|
|
||||||
|
|
||||||
TOTAL=$((PASS+FAIL))
|
|
||||||
if [ $FAIL -eq 0 ]; then
|
|
||||||
echo "ok $PASS/$TOTAL next/tests/smoke_pin_pure.sh passed"
|
|
||||||
else
|
|
||||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
|
||||||
echo "$ERRORS"
|
|
||||||
fi
|
|
||||||
[ $FAIL -eq 0 ]
|
|
||||||
@@ -99,10 +99,6 @@ in isolation, and a clear acceptance check.
|
|||||||
|
|
||||||
## Step 1 — Repo skeleton + canonical CID
|
## Step 1 — Repo skeleton + canonical CID
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **1a** — `next/` directory skeleton, README, `.gitignore` for `data/`
|
|
||||||
- [x] **1b** — `next/kernel/nx_cid.erl` (from_sx/to_string/from_string/equals) + `next/tests/cid.sh` (13 cases). Module is `nx_cid` not `cid` — the `cid` BIF module would be shadowed by a user module of the same name; plan §Step 1's `cid.erl` is illustrative per briefing.
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -150,11 +146,6 @@ canonicalize_sx(V) -> ... % sorts dict keys, normalizes strings
|
|||||||
|
|
||||||
## Step 2 — Activity envelope + signature verify
|
## Step 2 — Activity envelope + signature verify
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **2a** — `next/kernel/envelope.erl` `validate_shape/1` + `get_field/2` (property-list envelope; Erlang maps `#{}` not supported in this port) + `next/tests/envelope_shape.sh` (15 cases)
|
|
||||||
- [x] **2b** — `canonical_bytes/1` over sig-stripped, key-sorted envelope (deterministic textual form via `cid:to_string` substrate; dag-cbor stand-in for v1) + `next/tests/envelope_canonical.sh` (8 cases)
|
|
||||||
- [x] **2c** — `verify_signature/2` against actor `public_keys`, time-aware key validity per design §9.6 (created ≤ published, optional supersession check) + `next/tests/envelope_sig.sh` (11 cases). Signature scheme is HMAC-shaped (`crypto:hash(sha256, KeyMaterial ++ canonical_bytes)`) — RSA/Ed25519 verify deferred to m2 (BIFs not yet wired).
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
```erlang
|
```erlang
|
||||||
@@ -195,13 +186,6 @@ verify_signature(Activity, ActorState) ->
|
|||||||
|
|
||||||
## Step 3 — JSONL log + sequence numbers
|
## Step 3 — JSONL log + sequence numbers
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **3a** — `log:open/2` + `log:append/2` + `log:tip/1` + `log:replay/3` + `log:entries/1` over an in-memory log state (per-actor seq; replay in append order; round-trip the stored activity). `next/tests/log_memory.sh` (12 cases).
|
|
||||||
- [ ] **3b** — *Parked behind substrate gap (see Blockers below).* Term codec + on-disk persistence: serializer/parser writing each activity as a JSONL-style line; restart-resumes-tip from the segment file.
|
|
||||||
- [ ] **3c** — Segment rotation at size threshold + gen_server-mediated concurrent appends.
|
|
||||||
|
|
||||||
**Blockers (Step 3b):** The Erlang port returns SX strings (an opaque OCaml-string type) from `atom_to_list/1` and `integer_to_list/1`, rejects them from `++`/list pattern matching, and does not register `binary_to_list`/`list_to_binary`. `$X` character literals decode to `nil` in `parse-number`. Net effect: there is no in-Erlang path from an arbitrary term to a byte sequence (or back) that doesn't go through a temp-file round-trip through the filesystem. Workaround paths: (a) add a `term_to_binary`/`binary_to_term` BIF in a separate substrate loop, (b) accept a filesystem-mediated SX-string→binary helper and live with the O(N) IO cost, (c) restrict the on-disk format to a binary-only encoding with a per-instance atom-id table for atoms (introduces an extra durability dependency). Decision to defer; revisit once a downstream Step (5–8) forces the issue or a substrate BIF arrives. In-memory log from 3a is sufficient to unblock Step 5+ which consume the API surface.
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
```erlang
|
```erlang
|
||||||
@@ -243,18 +227,6 @@ replay(LogState, InitAcc, Fun) -> ...
|
|||||||
|
|
||||||
## Step 4 — Genesis bundle
|
## Step 4 — Genesis bundle
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **4a** — Seed genesis SX file authoring: `next/genesis/manifest.sx` + `next/genesis/activity-types/create.sx`. Manifest uses bare parenthesised paths (data lists, not `(list ...)` calls — consumed by `parse`, not `eval`). `next/tests/genesis_parse.sh` (5 cases).
|
|
||||||
- [x] **4b-act** — Remaining activity-types: `update.sx` + `delete.sx`, manifest updated, parse tests (10 cases total in `genesis_parse.sh`)
|
|
||||||
- [x] **4b-obj** — Object-types: SXArtifact, Note, Tombstone, DefineActivity, DefineObject, DefineProjection, DefineValidator, DefineCodec, DefineSigSuite, Snapshot — 10 `DefineObject` files + manifest updated + 12 new parse tests
|
|
||||||
- [x] **4b-proj** — Projections: activity-log, by-type, by-actor, by-object, actor-state, define-registry, audience-graph — 7 `DefineProjection` files + manifest updated + 9 new parse tests
|
|
||||||
- [x] **4b-vld** — Validators: envelope-shape, signature, type-schema — 3 `DefineValidator` files + manifest updated + 5 new parse tests
|
|
||||||
- [x] **4b-cod** — Codecs (dag-cbor, raw, dag-json) + sig-suites (rsa-sha256-2018, ed25519-2020) + audience predicates (Public, Followers, Direct) — 8 SX files + manifest fully populated + 14 new parse tests
|
|
||||||
- [x] **4c** — `bootstrap:read_genesis/0,1` + `read_section/2` + `sections/0` + `section_subdir/1` + `ends_with_sx/1` in Erlang: walk seven hardcoded section subdirs, filter `.sx` files via byte-pattern suffix match, read each into a binary. Returns `{ok, [{Section, [{Name, Bytes}, ...]}, ...]}`. Skips SX parsing — the substrate has no in-Erlang binary→SX-term path (same gap as Step 3b); bundle CID over raw bytes is enough for Step 4d. `next/tests/bootstrap_read.sh` (15 cases).
|
|
||||||
- [x] **4d** — `bootstrap:build_genesis/1` + `verify_genesis/2` + `cidhash_path/1` + `write_cidhash/2` + `read_cidhash/1`: bundle CID via host `cid:to_string` over `{genesis_bundle, Sections}`; mismatch returns `{error, {cid_mismatch, Got, Expected}}`; `.cidhash` sibling file persists between runs. `next/tests/bootstrap_build.sh` (12 cases).
|
|
||||||
- [x] **4e** — `bootstrap:load_genesis/1` + `strip_sx_suffix/1`: bridges `read_genesis` output into `registry` entries. Section atom = registry kind; entry name = filename minus `.sx` (binary); entry value = raw file bytes (parsed forms replace these once an SX-parser bridge exists). `next/tests/bootstrap_load.sh` (15 cases).
|
|
||||||
- [x] **4f-consolidate** — `bootstrap:start/3(ActorId, KeySpec, ActorState)` — one-call bring-up: `registry:start_link/0` → `populate_registry/0` → `nx_kernel:start_link/3`. Returns the kernel Pid. `next/tests/bootstrap_start.sh` (10 cases).
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
Genesis bundle SX sources (per design §12.2). Each is a small SX file authored
|
Genesis bundle SX sources (per design §12.2). Each is a small SX file authored
|
||||||
@@ -338,12 +310,6 @@ created with a known stable CID.
|
|||||||
|
|
||||||
## Step 5 — Registry mechanism + bootstrap dispatch
|
## Step 5 — Registry mechanism + bootstrap dispatch
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **5a** — Pure-functional `next/kernel/registry.erl`: `new/0`, `kinds/0`, `register/4`, `lookup/3`, `list/2`. State is a property list keyed by kind atom; per-kind storage is a property list of `{Name, Entry}`. Unknown kinds rejected with `{error, unknown_kind}`. `next/tests/registry_pure.sh` (14 cases).
|
|
||||||
- [x] **5b** — gen_server wrapper around the pure registry: `start_link/0`, registered name `registry`, `register/3 lookup/2 list/1 stop/0` API delegating through `gen_server:call`. `next/tests/registry_server.sh` (12 cases). Port note: each test combines start_link + ops in a single expression because spawned processes don't survive across separate `erlang-eval-ast` invocations.
|
|
||||||
- [x] **5c-populate** — `bootstrap:populate_registry/0` walks `read_genesis` output and calls `registry:register/3` (the gen_server API) for each entry. Returns the total entries registered. `next/tests/bootstrap_populate.sh` (14 cases).
|
|
||||||
- [x] **5d-pure** — `next/kernel/define_registry.erl` — Erlang-fun stand-in for the genesis `define-registry.sx` projection fold. Routes `Create{Define*{...}}` activities through `registry:register/4` keyed by `define_kind/1` (7 atoms: define_activity → activity_types, …). `fold_fn/0` plugs into `projection:start_link/3`. Integration test verifies the full activity → projection → registry-lookup chain. `next/tests/define_registry_pure.sh` (16 cases).
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
Registries are gen_servers, one per kind, each holding the active version map:
|
Registries are gen_servers, one per kind, each holding the active version map:
|
||||||
@@ -386,16 +352,6 @@ projection fold maintains it.)
|
|||||||
|
|
||||||
## Step 6 — Validation pipeline + POST /activity
|
## Step 6 — Validation pipeline + POST /activity
|
||||||
|
|
||||||
**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] **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.
|
|
||||||
- [x] **6b-sig** — `pipeline:stage_signature/2` (direct call) + `stage_signature/1` (factory returning a context-bound stage fun). Not wired into default stage lists since ActorState isn't available at static-list build time; callers compose by `Stages = [..., pipeline:stage_signature(AS)]`. `next/tests/pipeline_signature.sh` (11 cases) covers direct + factory + composition + halt behaviour with stage_envelope.
|
|
||||||
- [x] **6c-replay** — `pipeline:stage_replay/2` (direct) + `stage_replay/1` (factory closed over LogState). Checks the log entries for an existing activity with the same `:id`. Returns `{error, replay}` on duplicate, `{error, no_id}` when missing. `next/tests/pipeline_replay.sh` (12 cases).
|
|
||||||
- [x] **6c-schema-pure** — `pipeline:stage_schema/2` (direct) + `stage_schema/1` (factory closed over a SchemaLookup callback). SchemaLookup is `fun(Type) -> {ok, SchemaFn} | not_found`; SchemaFn is `fun(Object) -> bool`. Open-world default: unknown type → ok; no :object skips the check. `next/tests/pipeline_schema.sh` (14 cases). SX-source eval bridge will plug into the same shape later.
|
|
||||||
- [x] **6d-cs** — `outbox:construct/4` (skeleton + CID-derived :id via `cid:to_string`) + `outbox:sign/2` (HMAC over canonical bytes, append :signature pair from KeySpec) + `cid_of/1` accessor. Verified end-to-end: construct→sign→envelope:verify_signature passes; wrong key material fails with bad_signature. `next/tests/outbox_construct.sh` (13 cases).
|
|
||||||
- [x] **6d-publish** — `outbox:publish/2(Request, Context)` orchestrates construct + sign + `pipeline:run_stages([envelope, signature, replay])` + `log:append`. Returns `{ok, [{cid, _}, {activity, _}], NewLog}` or `{error, Reason, LogState}` on stage halt. Replay catches duplicate publishes; bad key material surfaces `bad_signature`. `next/tests/outbox_publish.sh` (13 cases).
|
|
||||||
- [ ] **6e** — HTTP handler for POST /activity glue (depends on Step 8 http server)
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
```erlang
|
```erlang
|
||||||
@@ -456,12 +412,6 @@ publish(ActorId, ActivityRequest) ->
|
|||||||
|
|
||||||
## Step 7 — Projection scheduler
|
## Step 7 — Projection scheduler
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **7a** — Pure-functional `next/kernel/projection.erl`: `new/2,3`, `fold_activity/2`, `replay/2`, `name/1`, `state/1`, `fold_fn/1`. Projection record is `[{name, _}, {state, _}, {fold, fun}]`; fold body is an Erlang fun in v1 (SX-source eval bridge deferred). `next/tests/projection_pure.sh` (12 cases).
|
|
||||||
- [x] **7b** — gen_server-per-projection: `start_link/3(Name, InitialState, FoldFn)` + `async_fold/2(Name, Activity)` (cast) + `query/1(Name)` (call) + `stop/1`. Each projection registered under its own Name atom. `next/tests/projection_server.sh` (11 cases). Snapshot persistence deferred (needs SX-source eval + on-disk state).
|
|
||||||
- [x] **7c** — `outbox:publish` broadcast hook: after `log:append`, fans out the signed activity to every projection listed under `Context`'s `:projections` entry via `projection:async_fold`. Stage halts (replay, sig failure) skip broadcast. `next/tests/outbox_broadcast.sh` (14 cases).
|
|
||||||
- [x] **7d-pure** — `next/kernel/sandbox.erl` with `eval_pure/2` and `eval_pure/3` — try/catch wrappers over Erlang funs. Catches throw, error, exit; returns `{ok, Result}` on success, `{error, {Class, Reason}}` on exception. Gas/IO sandboxing lands with SX-source eval; API shape is stable. `next/tests/sandbox_eval.sh` (13 cases).
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
```erlang
|
```erlang
|
||||||
@@ -506,24 +456,6 @@ publish(ActorId, ActivityRequest) ->
|
|||||||
|
|
||||||
## Step 8 — HTTP server + endpoints
|
## Step 8 — HTTP server + endpoints
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **8a** — `http:listen/2` BIF wrapper in `lib/erlang/runtime.sx` (the briefing's allowed exception). Validates args, bridges Erlang handler funs to SX-callable lambdas via `er-of-sx`/`er-to-sx`, delegates to the native `http-listen` primitive in `bin/sx_server.ml`. Tests verify registration + arg validation (not the blocking listen loop). `next/tests/http_listen_bif.sh` (5 cases).
|
|
||||||
- [x] **8b-route** — `next/kernel/http_server.erl`: pure `route/1` dispatch + `ok_response/1`, `not_found_response/0`, `welcome_body/0`. GET / returns welcome; everything else returns 404 (graceful for missing fields). `next/tests/http_route.sh` (11 cases).
|
|
||||||
- [ ] **8b-start** — `start/1(Port)` spawns an Erlang process hosting `http:listen/2`, requires the dict↔proplist marshaling bridge in the BIF wrapper.
|
|
||||||
- [x] **8c-cap** — Route GET `/.well-known/sx-capabilities` (static doc: kernel/version/verbs lines). `next/tests/http_capabilities.sh` (8 cases). Other concrete routes follow.
|
|
||||||
- [x] **8c-actors-doc** — `match_prefix/2` byte-level path-prefix matcher + GET `/actors/{id}` route returning an `actor: <id>` stub body. `/actors/{id}/outbox` deferred (needs path-segment splitting). `next/tests/http_actors.sh` (13 cases).
|
|
||||||
- [x] **8c-art** — Route GET `/artifacts/{cid}` via `match_prefix`. Stub body echoes the cid (`artifact: <cid>\n`); real content store lookup deferred. `next/tests/http_artifacts.sh` (9 cases).
|
|
||||||
- [x] **8c-proj** — Routes GET `/projections` (list stub) + GET `/projections/{name}` (state stub) via `match_prefix`. Bare-path list endpoint dispatches before the prefix clause. `next/tests/http_projections.sh` (11 cases). Registry-backed implementation deferred.
|
|
||||||
- [x] **8c-post-auth** — `route/2(Req, Cfg)` adds POST `/activity` with bearer-token check. Cfg `:publish_token` is the expected token; missing / wrong / malformed Authorization all return 401. Authorized requests get a stub 200 ("published (stub)"). `next/tests/http_post_activity.sh` (13 cases).
|
|
||||||
- [x] **8c-post-publish-pure** — `next/kernel/nx_kernel.erl` — pure-functional kernel orchestrator. `new/3(ActorId, KeySpec, ActorState)` builds the runtime state; `publish/2(Request, State)` calls `outbox:publish` with a Context derived from state, advances log + next_published on success. `next/tests/nx_kernel_pure.sh` (12 cases).
|
|
||||||
- [x] **8c-post-publish-srv** — gen_server wrapper around nx_kernel: `start_link/3`, named-process `publish/1`, `query/0`, `log_tip/0`, `with_projections/1`, `stop/0`. `next/tests/nx_kernel_server.sh` (11 cases). HTTP layer integration follows.
|
|
||||||
- [x] **8c-post-publish-http** — POST `/activity` handler now calls `nx_kernel:publish/1` when the kernel process is registered; falls back to the existing stub when not. Success → 200 with `cid: <cid>\n` body via `cid_response/1`; sig/replay failures → 422 via `validation_failed_response/0`. `next/tests/http_publish.sh` (10 cases).
|
|
||||||
- [x] **8d-accept** — `accept_format/1` + `accept_format_from/1` parse the Accept header into `:activity_json | :json | :sx | :cbor | :text`. Priority: activity+json > json > sx > cbor; everything else falls to text. `next/tests/http_accept.sh` (13 cases).
|
|
||||||
- [x] **8d-dispatch-cap** — `capabilities_body_for/1` returns distinct stubs per format (json `{...}`, sx `(...)`, cbor `A1 64 caps 69 fed-sx-m1`); activity_json shares the json body. Route intercepts GET capabilities to thread the Accept format through `accept_format_from/1`. `next/tests/http_capabilities_format.sh` (13 cases).
|
|
||||||
- [x] **8d-content-type** — `content_type_for/1` maps format atoms to MIME-type binaries (text/plain, application/json, application/activity+json, application/sx, application/cbor). `ok_response/2(Body, Format)` builds a 200 response with the right Content-Type header. `next/tests/http_content_type.sh` (13 cases).
|
|
||||||
- [x] **8d-dispatch-post** — POST `/activity` now threads the Accept format through both kernel-present (`cid_response_for/2` → `{"cid":"<cid>"}` for json / `(cid "<cid>")` for sx / raw bytes for cbor) and kernel-absent (`post_activity_response_for/1` → `{"status":"stub"}` / `(status "stub")` / etc.) paths. `next/tests/http_post_format.sh` (13 cases) covers shape + Content-Type for both stub and publish paths.
|
|
||||||
- [x] **8d-dispatch-get** — `actor_doc_response_for/2`, `artifact_response_for/2`, `projection_response_for/2`, `projections_list_response_for/1`. `dispatch` refactored to `/3` to thread Format; route extracts Format once and passes it down. `next/tests/http_get_format.sh` (17 cases) covers per-format bodies + Content-Type + end-to-end GETs with Accept headers.
|
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
Core endpoints (per design §16.1):
|
Core endpoints (per design §16.1):
|
||||||
@@ -576,13 +508,6 @@ Auth on `POST /activity`: bearer token from env var `NEXT_PUBLISH_TOKEN`.
|
|||||||
|
|
||||||
## Step 9 — Smoke tests
|
## Step 9 — Smoke tests
|
||||||
|
|
||||||
**Sub-deliverables:**
|
|
||||||
- [x] **9-pre-fold** — In-process end-to-end test of the HTTP → publish → broadcast → projection-fold chain. Proves the full vertical works without a real TCP socket. `next/tests/http_publish_fold.sh` (10 cases). Step 9a/b proper need TCP (Step 8b-start).
|
|
||||||
- [x] **9a-pure** — In-process Pin smoke test mirroring the §Step 9a flow. Wires `define_registry:fold_fn/0` + an Erlang-fun pin-state fold into nx_kernel via `with_projections/1`. Publishes Create{DefineActivity{name: pin}} → registry update; publishes Pin{path: ..., cid: ...} → pin_state update. Order-independent; ignores Note + other types. `next/tests/smoke_pin_pure.sh` (13 cases).
|
|
||||||
- [ ] **9a-tcp** — Same flow under curl over Step 8b-start once TCP listening lands.
|
|
||||||
- [x] **9b-pure** — In-process reactive smoke test. A trigger projection (Erlang-fun fold) matches Note activities tagged `smoketest`, constructs a derived `TestEcho{echoes: <Note CID>}`, and captures it into projection state. Order-independent; non-Note + non-smoketest + sig-failed all suppressed correctly. `next/tests/smoke_app_pure.sh` (12 cases). Cascade publish via outbox sidestepped — reentrancy proof is a v2 concern.
|
|
||||||
- [ ] **9b-tcp** — Same flow under curl over Step 8b-start + cascade publish through outbox.
|
|
||||||
|
|
||||||
**The proof points.** Two end-to-end smoke tests demonstrate, between them, that
|
**The proof points.** Two end-to-end smoke tests demonstrate, between them, that
|
||||||
fed-sx is genuinely a substrate for distributed reactive applications expressed
|
fed-sx is genuinely a substrate for distributed reactive applications expressed
|
||||||
as data — not a system you extend by writing kernel code.
|
as data — not a system you extend by writing kernel code.
|
||||||
@@ -995,61 +920,3 @@ A few things still under-specified; resolve as work begins.
|
|||||||
60 seconds." Tunable per-projection later; v1 uses the default.
|
60 seconds." Tunable per-projection later; v1 uses the default.
|
||||||
5. **Genesis bundle format.** Dag-cbor map per §12.2; concrete schema needs
|
5. **Genesis bundle format.** Dag-cbor map per §12.2; concrete schema needs
|
||||||
one round of refinement once we author the actual definitions in step 4.
|
one round of refinement once we author the actual definitions in step 4.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Progress log
|
|
||||||
|
|
||||||
Newest first. One line per sub-deliverable commit. Erlang conformance gate
|
|
||||||
(`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry.
|
|
||||||
|
|
||||||
- **2026-05-28** — Step 4f-consolidate: `bootstrap:start/3(ActorId, KeySpec, ActorState)` brings up the full kernel substrate in one call — starts the registry gen_server, populates it from the canonical genesis bundle (31 entries across 7 kinds), then starts nx_kernel. Returns the kernel Pid (gen_server convention in this port returns raw Pid not `{ok, Pid}`). Tests verify whereis(nx_kernel), per-kind counts (3/10/7/3/3/2/3), registry lookup of a known entry (`create`), publish + log_tip advance. `next/tests/bootstrap_start.sh` 10/10. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 7d-pure: `next/kernel/sandbox.erl` — `eval_pure/2(Fun, Arg)` and `eval_pure/3(Fun, Activity, State)`. try/catch envelope returns `{ok, Result}` on success and `{error, {Class, Reason}}` for each of the three exception classes (throw, error, exit). The 3-arity variant matches the projection-fold shape so the scheduler can wrap fold bodies. Port note: this Erlang implementation catches by explicit class names rather than the open `Class:Reason` pattern — wrappers enumerate `throw:Reason / error:Reason / exit:Reason` explicitly. Real gas budget + IO denial + env-stripping lands with SX-source eval; the wrapper API doesn't change. `next/tests/sandbox_eval.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 9b-pure: **reactive application extensibility, proven end-to-end.** Mirrors §Step 9b structurally without TCP/curl/JSON. A trigger projection (Erlang-fun fold over `{Captured, Count}` state) matches Note activities whose `:object :tags` contains `smoketest`, constructs a derived `TestEcho` activity with `:object :echoes` pointing at the Note's `:id`, and captures it into projection state. Order-independent; non-Note + non-smoketest + Note-without-tags + sig-failed publishes all suppressed correctly. Multi-tag (e.g. `[smoketest, foo, bar]`) still matches. Cascade publish (the trigger actually publishing the derived activity back through outbox) is deferred — the gen_server reentrancy that introduces is a v2 concern; the projection-state capture is sufficient proof of the match-then-derive mechanism. `next/tests/smoke_app_pure.sh` 12/12. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 9a-pure: **the first verb-extensibility smoke test, proven end-to-end.** Mirrors §Step 9a structurally without TCP/curl/JSON. Two projections wired into `nx_kernel:with_projections([define_reg, pin_state])` — `define_reg` uses `define_registry:fold_fn/0` (Step 5d-pure), `pin_state` uses an Erlang-fun fold that records `{Path, Cid}` from Pin activities. Publish `Create{DefineActivity{name: pin}}` → registry update visible via `registry:lookup(activity_types, pin, projection:query(define_reg))`; publish `Pin{path: docs_intro, cid: qm_cid_1}` → `projection:query(pin_state) =:= [{docs_intro, qm_cid_1}]`. Order-independent (DefineActivity-then-Pin and Pin-then-DefineActivity both succeed); Note + non-Define types are pass-throughs in both projections. The TCP/curl variant (Step 9a-tcp) layers on Step 8b-start. `next/tests/smoke_pin_pure.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 5d-pure: `next/kernel/define_registry.erl` — the meta-projection fold body, in pure Erlang. State shape mirrors `registry:new()` exactly; `fold/2` dispatches Create{Define*} to `registry:register/4` keyed by `define_kind/1` (define_activity → activity_types, define_object → object_types, …). Non-Create + Create{non-Define} + Define{no :name} are all pass-throughs. Override re-registration preserves a single entry per name. `fold_fn/0` plugs the fold into `projection:start_link/3` — verified end-to-end: activity → projection async_fold → query state → registry:lookup returns the registered Object. The SX `define-registry.sx` body will replace this once an SX-source eval bridge exists; the Erlang shape proves the wiring is correct. `next/tests/define_registry_pure.sh` 16/16. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 6c-schema-pure: `pipeline:stage_schema/2` accepts (Activity, SchemaLookup) where SchemaLookup is a caller-supplied callback `fun(Type) -> {ok, SchemaFn} | not_found`. Open-world default — unregistered types resolve to ok so the pipeline doesn't block activities the kernel hasn't yet learned about (tightened to strict-world in milestone 2). Activities without `:object` skip the schema check. `stage_schema/1` returns a 1-arity stage fun closed over SchemaLookup for composition with run_stages. Halt order verified end-to-end: envelope-shape errors precede schema; envelope-ok + schema-fail surfaces `schema_mismatch`. The Erlang-fun shape is the substrate-friendly stand-in for the SX `:schema` bodies in genesis; same stage shape will dispatch through an SX-source eval bridge once it exists. `next/tests/pipeline_schema.sh` 14/14. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8d-dispatch-get: format-aware versions of every GET response builder. `actor_doc_response_for/2`, `artifact_response_for/2`, `projection_response_for/2`, `projections_list_response_for/1`. Each produces `{"key":"value"}` (json/activity_json), `(key "value")` (sx), raw payload bytes (cbor stub), or the existing text form. `dispatch` refactored to `/3` with a backward-compat `dispatch/2` wrapper. Route extracts Format via `accept_format_from/1` once at the top and threads it through dispatch. End-to-end GETs with `Accept: application/json` / `application/sx` verified for all three dynamic-prefix routes + the projections-list bare-path route. Step 8d effectively complete — format dispatch + Content-Type live on every non-static response. `next/tests/http_get_format.sh` 17/17. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8d-dispatch-post: `handle_post_activity` extracts the Accept format via `accept_format_from/1` and threads it into `publish_if_kernel/2`. Both success paths emit format-specific bodies: `cid_response_for/2` produces `{"cid":"<cid>"}\n` (json/activity_json), `(cid "<cid>")\n` (sx), raw CID bytes (cbor), or the existing text form; `post_activity_response_for/1` mirrors for the kernel-absent stub. Each response carries the matching Content-Type. End-to-end POSTs with `Accept: application/json` / `application/sx` verified through the full HTTP→nx_kernel→publish→cid_response_for chain. `next/tests/http_post_format.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8d-content-type: `content_type_for/1` maps format atoms to MIME-type binaries — text/plain (10b), application/json (16b), application/activity+json (25b), application/sx (14b), application/cbor (16b); unknown formats fall through to text/plain. `ok_response/2(Body, Format)` constructs a 200 response with `{headers, [{<<"content-type">>, MIME}]}`. Lowercase header key matches how the BIF wrapper normalises request headers. `ok_response/1` still produces the empty-headers shape — backward compat preserved. `next/tests/http_content_type.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8d-dispatch-cap: `capabilities_body_for/1` returns distinct byte sequences per format — text reuses the existing `capabilities_body/0`; json/activity_json share `{"caps":"fed-sx-m1"}`; sx returns `(caps "fed-sx-m1")`; cbor returns a minimal `A1 64 caps 69 fed-sx-m1` map. Route now intercepts GET `/.well-known/sx-capabilities` to pull the Accept format via `accept_format_from/1` and dispatch through `capabilities_body_for`. Unknown formats fall back to text. POST capabilities still 404 (only GET handled). `next/tests/http_capabilities_format.sh` 13/13 verifies all formats + the intercept + no-Accept default. Content-Type headers not yet set (8d-dispatch-rest covers headers + applying the same shape to actor/artifact/projection/cid responses). Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8d-accept: `accept_format/1` + `accept_format_from/1` parse the Accept header into a content-negotiation atom. Priority order via successive `match_prefix` checks: application/activity+json → `activity_json`; application/json → `json`; application/sx → `sx`; application/cbor → `cbor`; everything else (including nil / empty / non-binary) → `text`. Comma-separated lists with activity+json first still resolve to activity_json — leading-prefix match is sufficient for v1 envelopes. Step 8d split into 8d-accept (done) + 8d-dispatch (wire into response bodies). `next/tests/http_accept.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 5c-populate: `bootstrap:populate_registry/0` walks `read_genesis` output and calls `registry:register/3` (gen_server API) for each entry. Total return is 31 = 3 + 10 + 7 + 3 + 3 + 2 + 3 across the seven kinds, matching the manifest authored in Step 4. `next/tests/bootstrap_populate.sh` 14/14 verifies per-kind counts + lookups against known names (`activity_types/create`, `object_types/define-activity`, `validators/envelope-shape`). Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 9-pre-fold: in-process integration test proving the full POST → publish → broadcast → projection-fold chain. With `projection:start_link` + `nx_kernel:start_link` + `nx_kernel:with_projections([p_count, p_collect])`, three authorized POST `/activity` calls advance both projections to 3 — and the kernel's log to 3 entries — and the projection's collected activity carries the POST body as `:object`. Unauthorized or sig-failed POSTs leave projection state unchanged. Step 9a/b proper (curl-driven smoke tests) wait on Step 8b-start (TCP) + Define\* SX-source eval bridge, but the structural chain is already verified end-to-end. `next/tests/http_publish_fold.sh` 10/10. Erlang conformance 729/729. Step 9 split into 9-pre-fold (done) + 9a + 9b.
|
|
||||||
- **2026-05-28** — Step 8c-post-publish-http: POST `/activity` handler now bridges into `nx_kernel:publish/1` when the kernel gen_server is registered (`erlang:whereis(nx_kernel) =/= undefined`). On success the response carries the canonical CID via `cid_response/1`; on pipeline failure the response is 422 via `validation_failed_response/0`. When the kernel isn't registered, the handler falls through to the existing 200 stub — preserves backwards compatibility for the auth-only tests in `http_post_activity.sh`. Distinct POSTs produce distinct CIDs (next_published counter in nx_kernel state). Unauthorized POSTs never reach the kernel — log tip stays at 0. `next/tests/http_publish.sh` 10/10. The POST `/activity` → publish → fold loop is now functional end-to-end through the kernel. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-post-publish-srv: `nx_kernel.erl` extended with gen_server callbacks + named-process API. `start_link/3(ActorId, KeySpec, ActorState)` spawns the worker and registers under the literal `nx_kernel` atom; `publish/1(Request)` calls into `handle_call({publish, Request}, ...)` which delegates to the pure `publish/2` and reflects the new state back into the server. `query/0` returns the full state proplist; `log_tip/0` is a direct accessor; `with_projections/1` mutates the projections list. Same port quirks as Step 5b/7b documented (raw Pid return, no `?MODULE`, processes don't persist across separate `erlang-eval-ast` calls — tests inline start_link with operations). `next/tests/nx_kernel_server.sh` 11/11. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-post-publish-pure: `next/kernel/nx_kernel.erl` — pure-functional kernel orchestrator that wraps `outbox:publish/2` with a long-lived runtime state. `new/3(ActorId, KeySpec, ActorState)` initialises state with an empty log + monotonic `:next_published` counter. `publish/2(Request, State)` builds the publish Context from state, calls outbox:publish, and on success advances `:log` and increments `:next_published`. The counter solves the "same Request published twice" replay collision — each call gets a distinct `:published` timestamp, so the canonical-bytes CID differs and stage_replay doesn't halt. On failure (e.g. bad key), state is returned unchanged. Step 8c-post-publish split into pure (done) + srv (gen_server wrapper) sub-deliverables. `next/tests/nx_kernel_pure.sh` 12/12. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-post-auth: POST `/activity` route + bearer-token auth via new `route/2(Req, Cfg)` variant. Cfg's `:publish_token` is the expected bearer; mismatched / missing / malformed (no "Bearer " prefix) / empty-token Authorization all surface as 401 `unauthorized_response/0`. `route/1` is a backwards-compatible wrapper with empty Cfg — any POST `/activity` over `route/1` is 401 by design (no token configured). `Bearer ` prefix stripped via the same `match_prefix` helper used elsewhere. Real publish wiring deferred to `8c-post-publish` (needs the kernel orchestrator that holds logs / actor keys / projection list). `next/tests/http_post_activity.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-proj: routes GET `/projections` (list stub returning `projections: (empty)\n`) + GET `/projections/{name}` (state stub returning `projection: <name>\n`). Bare-path list clause dispatches before the prefix clause so `/projections` and `/projections/{name}` are distinguishable. All three dynamic-prefix routes (actors / artifacts / projections) compose cleanly — verified by a single combined-route test asserting all return 200 with distinct prefixes. Registry-backed implementation deferred — needs a running registry process at route time. `next/tests/http_projections.sh` 11/11. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-art: GET `/artifacts/{cid}` route added on top of `match_prefix`. Single GET dispatch clause now tries `actors_prefix` first, falls through to `artifacts_prefix` — no path collision (different leading bytes). Stub body echoes the CID with `artifact: ` prefix; real artifact-store lookup deferred to later (will key into the registry / genesis bundle). `next/tests/http_artifacts.sh` 9/9 covers happy path, empty-cid 404, POST 404, actor/artifact non-collision, static-route regression. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-actors-doc: `http_server` extended with `match_prefix/2` — pure byte-level prefix matcher built on Erlang binary pattern matching (`<<B, _/binary>>`-style head/tail walk). Empty prefix returns `{ok, FullPath}`; non-match returns `nomatch`; exact match returns `{ok, <<>>}`. Wired into a new GET `/actors/{id}` clause that extracts the id suffix and returns it as the body of `actor_doc_response/1` (stub: `actor: <id>\n`). Empty id falls into 404. `/actors/{id}/outbox` deferred to a later step (needs segment splitting beyond prefix). `next/tests/http_actors.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8c-cap: GET `/.well-known/sx-capabilities` route + `capabilities_body/0` + `capabilities_path/0` exposed for tests. Body is a small plain-text descriptor with `kernel: fed-sx-m1`, `version: 0.0.1`, `verbs: Create Update Delete` (hand-spelled as integer-segment binary; string-literal segments unusable in this port). `next/tests/http_capabilities.sh` 8/8 covers method+path matching, body content, the existing GET / regression-free. Step 8c split into cap (done) + actors / art / proj / post — the rest need path-prefix matching helpers since `{id}` and `{cid}` are dynamic. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8b-route: `next/kernel/http_server.erl` — pure `route/1` request→response dispatch. Request shape `[{method, Bin}, {path, Bin}, ...]`; response `[{status, N}, {headers, []}, {body, Bin}]`. GET / returns 200 with hand-spelled "fed-sx kernel m1" body; everything else returns 404 with "not found" body. Method/path binaries spelled byte-by-byte (string-literal segments would truncate). Split former 8b into 8b-route (done) + 8b-start (needs dict↔proplist marshaling bridge in the BIF wrapper before the spawned `http:listen` call gets useful request fields). `next/tests/http_route.sh` 11/11. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 8a: `http:listen/2` BIF wrapper added to `lib/erlang/runtime.sx` (the briefing's single allowed scope exception). The BIF takes `(Port, Handler)`, validates Port is an integer and Handler is an Erlang fun (else `badarg`), then builds an SX-callable bridge lambda that marshals request dict↔Erlang term via `er-of-sx`/`er-to-sx` and calls `er-apply-fun` on the handler. Delegates to the native `http-listen` primitive (registered in `bin/sx_server.ml`, native-only). Tests verify registration + arg validation paths (the blocking listen loop itself is not exercised — production callers spawn an Erlang process to host the call). `next/tests/http_listen_bif.sh` 5/5; Erlang conformance preserved at 729/729 despite the runtime.sx edit. Step 8 broken into 8a–8d on the plan.
|
|
||||||
- **2026-05-28** — Step 7c: `outbox:publish` now broadcasts the signed activity to every projection process named in `Context`'s `:projections` entry — fired immediately after `log:append`, via `projection:async_fold`. Missing/nil/empty list is a no-op (preserves the Step 6d-publish contract). Stage halts (replay duplicate, sig failure) suppress the broadcast — projection state stays at zero while the activity is rejected. `next/tests/outbox_broadcast.sh` 14/14 covers single + multi projection fan-out, three-publish accumulation, replay-skip, sig-skip, and the projection receiving the post-sign Signed envelope (not the pre-sign skeleton). Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 7b: `projection.erl` extended with gen_server callbacks + per-projection named-process API. `start_link/3(Name, InitialState, FoldFn)` spawns and registers under the supplied atom; `async_fold/2(Name, Activity)` casts a fold message; `query/1(Name)` synchronously returns the current state. Same port quirks as registry gen_server (Step 5b): raw Pid return, no `?MODULE` macro, processes don't survive between separate `erlang-eval-ast` calls — tests inline start_link with operations. Two named projections are independent. Snapshot persistence deferred to a later sub-step (needs SX-source eval + on-disk state). `next/tests/projection_server.sh` 11/11. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 7a: `next/kernel/projection.erl` — pure-functional projection driver. Record shape `[{name, _}, {state, _}, {fold, fun}]`; `fold_activity/2` advances state by one activity; `replay/2` folds a whole list (mirrors `log:entries/1` semantics); `new/2` defaults to the identity fold and `new/3` accepts a custom Erlang fun. Multiple projections share no state — independent record values. Step 7 split into 7a (done) + 7b (gen_server-per-projection) + 7c (broadcast hook from outbox) + 7d (sandbox eval, needs SX-source bridge). `next/tests/projection_pure.sh` 12/12. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 6d-publish: `outbox:publish/2(Request, Context)` orchestrates construct + sign + `pipeline:run_stages` + `log:append`. Stage list is `[stage_envelope, stage_signature(AS), stage_replay(LogState)]` — so a duplicate publish (same Request, same Published) halts at the replay stage and returns `{error, replay, LogState}` with the log unchanged; bad key material halts at `bad_signature`. Happy path returns `{ok, [{cid, Cid}, {activity, Signed}], NewLog}`. Projection-scheduler dispatch deferred to Step 7. `next/tests/outbox_publish.sh` 13/13 covers happy path, replay halt, sig halt, multi-publish progression, CID stability across fresh logs. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 6d-cs: `next/kernel/outbox.erl` — envelope construction + signing. `construct/4` takes `(Type, ActorId, Published, Object)`, builds the canonical key-sorted property list, and derives the activity `:id` from `cid:to_string({activity_envelope, Skeleton})`. `sign/2` extracts key_id/algorithm/key-material from a KeySpec proplist, computes the v1 HMAC over canonical bytes, and appends the `:signature` pair. `cid_of/1` is a convenience accessor. Round-trip end-to-end through `envelope:verify_signature/2` verified (correct key passes, wrong key returns bad_signature). Step 6d split into 6d-cs (done) + 6d-publish (orchestration). `next/tests/outbox_construct.sh` 13/13. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 6c-replay: `pipeline:stage_replay/2` (direct) + `stage_replay/1` (factory closed over LogState). Linear scan of `log:entries/1` checking for an existing entry with the same `:id`. Returns ok if new, `{error, replay}` on duplicate, `{error, no_id}` when the activity has no id field. Step 6c split into 6c-replay (done) + 6c-schema (deferred — blocked behind SX-source eval bridge for the activity-type :schema body). `next/tests/pipeline_replay.sh` 12/12 covers direct + factory + composition with stage_envelope. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 6b-sig: `pipeline:stage_signature/2` direct call + `stage_signature/1` factory returning a context-bound stage fun closed over ActorState. Not wired into the default `inbound_stages`/`outbound_stages` lists because actor state isn't a static-build-time value; callers prepend the factory result to a stage list (`Stages = [stage_envelope, pipeline:stage_signature(AS)]`). `next/tests/pipeline_signature.sh` 11/11 covers direct + factory + composition with stage_envelope (including halt ordering: bad envelope halts before sig; good envelope + bad sig surfaces sig error). Erlang conformance 729/729.
|
|
||||||
- **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 6a–6e 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 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 5a: `next/kernel/registry.erl` — pure-functional registry. State is `[{Kind, [{Name, Entry}, ...]}, ...]` keyed by the same seven section atoms as Step 4c (activity_types, object_types, projections, validators, codecs, sig_suites, audience). API: `new/0`, `kinds/0`, `register/4`, `lookup/3`, `list/2`. Unknown kinds rejected with `{error, unknown_kind}`; missing names return `not_found`; re-registering the same name overrides without growing the list. `next/tests/registry_pure.sh` 14/14. Step 5 broken into 5a–5d on the plan. Erlang conformance 729/729.
|
|
||||||
- **2026-05-28** — Step 4d: `bootstrap:build_genesis/1` + `verify_genesis/2` + `.cidhash` helpers in `next/kernel/bootstrap.erl`. Bundle CID delegated to host `cid:to_string` over `{genesis_bundle, Sections}` — deterministic, ~59 byte CIDv1 binary. `verify_genesis/2` returns `ok` on match, `{error, {cid_mismatch, Got, Expected}}` on drift. `write_cidhash`/`read_cidhash` persist the CID to a `.cidhash` sibling file (path hand-spelled `<<...,47,46,99,...>>` per the string-literal-in-binary substrate quirk). `next/tests/bootstrap_build.sh` 12/12. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4c: `next/kernel/bootstrap.erl` — Erlang module that enumerates the genesis bundle by walking seven hardcoded section subdirs via `file:list_dir/1`, filters `.sx` files via byte-pattern suffix match (`ends_with_sx/1`), reads each into a binary via `file:read_file/1`. Returns `{ok, [{Section, [{Name, Bytes}, ...]}]}`. Hits the same SX-parser substrate gap as Step 3b — kept the surface byte-only; parsing happens via SX-side helpers in later steps. Port gotchas: `fun name/arity` references unsupported (use anonymous fun wrappers); `<<"...">>` string-literal segments truncate to one byte (paths hand-spelled as integer-segment binaries). `next/tests/bootstrap_read.sh` 15/15. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4b-cod: bootstrap codecs + sig-suites + audience predicates complete. 3 `DefineCodec` files (dag-cbor + raw + dag-json, dag-cbor + dag-json deferring to host-codec primitive when wired), 2 `DefineSigSuite` files (rsa-sha256-2018 PEM-keyed, ed25519-2020 multibase-keyed, both :verify returning false as m2-deferred stand-in), 3 `DefineAudience` files (Public/Followers/Direct member-of predicates per design §16). Manifest now lists 26 bootstrap files across all eight sections; `next/tests/genesis_parse.sh` 50/50. Step 4b complete; remaining Step 4 is bundler code (4c–4e). Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4b-vld: bootstrap validators complete — 3 `DefineValidator` SX files (envelope-shape mirroring Step 2a, signature stub delegating to envelope:verify_signature/2 per design §9.6, type-schema looking up the object-type schema from define-registry). Manifest `:validators` populated; `next/tests/genesis_parse.sh` 36/36. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4b-proj: bootstrap projections complete — 7 `DefineProjection` SX files authored (activity-log identity, by-type/by-actor/by-object indexes, actor-state with key history fold, define-registry meta-fold over Create{Define*}, audience-graph stub). Manifest `:projections` populated; `next/tests/genesis_parse.sh` 31/31. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4b-obj: bootstrap object-types complete — 10 `DefineObject` SX files authored (SXArtifact, Note, Tombstone, DefineActivity, DefineObject, DefineProjection, DefineValidator, DefineCodec, DefineSigSuite, Snapshot). Each carries an SX `:schema` predicate. Manifest `:object-types` populated; `next/tests/genesis_parse.sh` 22/22. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4b-act: bootstrap activity-types complete — `update.sx` (Update verb, requires :object CID + :patch) + `delete.sx` (Delete verb, requires :object CID) authored as DefineActivity forms matching the Create shape. Manifest updated; `next/tests/genesis_parse.sh` 10/10. Step 4b broken into act/obj/proj/vld/cod sub-deliverables on the plan. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 4a: genesis bundle seeded. `next/genesis/manifest.sx` (GenesisManifest with eight section keys, only `:activity-types` populated for now) + `next/genesis/activity-types/create.sx` (DefineActivity{Create} with :schema/:semantics SX bodies). `next/tests/genesis_parse.sh` 5/5. Step 3b parked behind a substrate-level term-codec gap — Blockers note added under Step 3; in-memory log from 3a unblocks Step 5+ which only need the API surface. Erlang conformance 729/729.
|
|
||||||
- **2026-05-27** — Step 3a: `log:open/2 append/2 tip/1 replay/3 entries/1` over an in-memory state (per-actor seq, replay in append order, round-trip activities). `next/tests/log_memory.sh` 12/12. Pivoted from on-disk in this iteration: this port's `atom_to_list`/`integer_to_list` return SX strings rather than Erlang charlists, `binary_to_list` is unregistered, and `$X` char literals decode to nil — so a term codec needs a workaround. Captured as the Step 3b risk note in the plan. Erlang conformance 729/729.
|
|
||||||
- **2026-05-26** — Step 2c: `envelope:verify_signature/2` — time-aware key lookup over `public_keys` (created ≤ published < superseded_at), MAC recompute via `crypto:hash(sha256, KeyMaterial ++ canonical_bytes)`, compared against `signature.value`. Returns ok or one of `no_signature | no_key_id | no_published | no_keys | no_active_key | bad_signature`. `next/tests/envelope_sig.sh` 11/11 pass. Erlang conformance 729/729.
|
|
||||||
- **2026-05-26** — Step 2b: `envelope:canonical_bytes/1` — strip signature, insertion-sort property list by key, return host-CID-string as deterministic byte form (dag-cbor stand-in). `next/tests/envelope_canonical.sh` 8/8 pass. Erlang conformance 729/729 preserved.
|
|
||||||
- **2026-05-26** — Step 2a: `next/kernel/envelope.erl` `validate_shape/1` + `get_field/2` over property-list envelopes (Erlang `#{}` maps not supported in this port). `next/tests/envelope_shape.sh` 15/15 pass. Erlang conformance 729/729 preserved.
|
|
||||||
- **2026-05-26** — Step 1b: `next/kernel/nx_cid.erl` (from_sx/to_string/from_string/equals) — thin Erlang wrapper around the `cid:to_string/1` BIF. `next/tests/cid.sh` 13/13 pass. Module named `nx_cid` to avoid shadowing the `cid` BIF (user-module dispatch takes precedence over BIFs by module name). Erlang conformance 729/729 preserved.
|
|
||||||
- **2026-05-26** — Step 1a: `next/` skeleton created (kernel/, genesis/, tests/, data/), README, `.gitignore data/`. Erlang conformance 729/729 preserved.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,64 @@
|
|||||||
# Go-on-SX: Go on the CEK/VM
|
# Go-on-SX — Go as an SX guest language
|
||||||
|
|
||||||
Compile Go source to SX AST; the existing CEK evaluator runs it. The unique angle: Go's
|
Port Go to SX as the **first static-typed, bidirectional-checked guest** in
|
||||||
goroutines and channels map cleanly onto SX's IO suspension machinery (`perform`/`cek-resume`)
|
the rose-ash language family. Goal isn't a production Go compiler; it's to
|
||||||
— a goroutine is a `cek-step-loop` running in a cooperative scheduler, a channel send/receive
|
prove the substrate from a paradigm angle the existing eleven guests don't
|
||||||
is a `perform` that suspends until the other end is ready.
|
cover, and to chisel out the lib/guest kits that statically-typed guests N+1
|
||||||
|
and N+2 will need.
|
||||||
|
|
||||||
End-state goal: **core Go programs running**, including goroutines, channels, defer/panic/recover,
|
Reference:
|
||||||
interfaces, and structs. Not a full Go compiler — no generics, no CGo, no full stdlib — but
|
- `plans/lib-guest.md` — parent, chiselling discipline, two-language rule.
|
||||||
a faithful runtime for idiomatic Go concurrent programs.
|
- `plans/lib-guest-scheduler.md` — sister kit; Go's scheduler pairs with
|
||||||
|
Erlang's. Extraction gated on this loop reaching Phase 5.
|
||||||
|
- `plans/lib-guest-static-types-bidirectional.md` — sister kit; Go's
|
||||||
|
checker pairs with a TBD second consumer. Extraction gated on this loop
|
||||||
|
reaching Phase 3.
|
||||||
|
- `plans/erlang-on-sx.md` — reference implementation for paradigm-port:
|
||||||
|
process model, BIF registry, hot reload, VM bytecode opcodes.
|
||||||
|
|
||||||
## Ground rules
|
**Branch:** `loops/go` (loop-style workstream once kicked off). SX files via
|
||||||
|
`sx-tree` MCP only.
|
||||||
|
|
||||||
- **Scope:** only touch `lib/go/**` and `plans/go-on-sx.md`. Do **not** edit `spec/`,
|
## Thesis — why Go
|
||||||
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
|
||||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
Eleven guests already live in `lib/`: apl, common-lisp, datalog, erlang,
|
||||||
- **SX files:** use `sx-tree` MCP tools only.
|
forth, haskell, hyperscript, js, kernel, lua, minikanren, ocaml, prolog,
|
||||||
- **Architecture:** Go source → Go AST → SX AST. No standalone Go evaluator.
|
ruby, scheme, smalltalk, tcl. Every one is either **dynamically typed**
|
||||||
- **Concurrency model:** cooperative, not preemptive. Goroutines yield at channel ops and
|
(most) or **HM-inferred** (haskell, ocaml). None exercise:
|
||||||
`time.Sleep`. A round-robin scheduler in SX drives them.
|
|
||||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
1. **Bidirectional static type checking** — annotation-driven, locally-
|
||||||
|
inferred, the dominant paradigm of modern statically-typed languages.
|
||||||
|
2. **Anonymous-channel concurrency** — Go's `chan` and `select`. Erlang has
|
||||||
|
addressed processes + mailboxes; Go has anonymous values + structural
|
||||||
|
pairing. Two different vocabularies for the same underlying scheduler
|
||||||
|
machinery.
|
||||||
|
3. **Structural interfaces** — `io.Reader` is "anything with this method
|
||||||
|
signature", not a declared subtype relationship. Different from Haskell
|
||||||
|
typeclasses (nominal), different from Lua duck typing (no declaration).
|
||||||
|
|
||||||
|
These three together make Go an unusually high-value port for proving SX.
|
||||||
|
If SX can host Go cleanly, it can host the next decade of mainstream
|
||||||
|
statically-typed languages (Rust, TS, Swift, Kotlin, Scala 3, Hack) because
|
||||||
|
they share these three properties.
|
||||||
|
|
||||||
|
Like Erlang-on-SX validated the actor model on the substrate, Go-on-SX
|
||||||
|
validates the goroutine model + bidirectional types.
|
||||||
|
|
||||||
|
## Non-goals (deliberate)
|
||||||
|
|
||||||
|
Out of scope. Reject feature requests for these without further consideration:
|
||||||
|
|
||||||
|
- **`unsafe` package.** Memory mucking. Skip entirely.
|
||||||
|
- **CGo.** C interop. Out of scope at every level.
|
||||||
|
- **Full `reflect`.** Provide enough for `fmt.Println` to render values;
|
||||||
|
reject the rest.
|
||||||
|
- **Build tags, modules, vendoring.** Treat source as monolithic. One
|
||||||
|
package per file, no real import resolution.
|
||||||
|
- **Production performance.** Conformance tests pass; benchmarks don't.
|
||||||
|
- **Garbage collection tuning.** SX's GC is what you get.
|
||||||
|
- **Race detector, escape analysis, inlining.** Out of scope.
|
||||||
|
- **`os`, `net/http`, full stdlib.** Provide a deliberately small slice
|
||||||
|
(Phase 8 below).
|
||||||
|
|
||||||
## Architecture sketch
|
## Architecture sketch
|
||||||
|
|
||||||
@@ -26,113 +66,335 @@ a faithful runtime for idiomatic Go concurrent programs.
|
|||||||
Go source text
|
Go source text
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
lib/go/tokenizer.sx — Go tokens: keywords, idents, string/rune/number literals,
|
lib/go/lex.sx — tokens; ASI; literals; operators
|
||||||
│ operators, semicolon insertion rules
|
│ (consumes lib/guest/core/lex.sx)
|
||||||
▼
|
▼
|
||||||
lib/go/parser.sx — Go AST: package, import, var, const, type, func, struct,
|
lib/go/parse.sx — AST: package/import/var/const/type/func/struct/
|
||||||
│ interface, goroutine, channel ops, defer, select, for range
|
│ interface; expressions; statements
|
||||||
|
│ (consumes lib/guest/core/pratt.sx + ast.sx)
|
||||||
▼
|
▼
|
||||||
lib/go/transpile.sx — Go AST → SX AST
|
lib/go/types.sx — bidirectional type checker. Synth + check judgments;
|
||||||
│
|
│ structural interface satisfaction; pluggable subtype
|
||||||
|
│ (INDEPENDENT — no lib/guest/static-types-bidirectional
|
||||||
|
│ yet; this loop builds the first consumer)
|
||||||
▼
|
▼
|
||||||
lib/go/runtime.sx — goroutine scheduler, channel primitives, defer stack,
|
lib/go/eval.sx — tree-walk evaluator on CEK. Variables as mutable cells;
|
||||||
│ panic/recover, interface dispatch, slice/map ops
|
│ slices = (length, capacity, backing-vector); maps =
|
||||||
|
│ SX dict; defer stack per frame.
|
||||||
▼
|
▼
|
||||||
CEK / VM
|
lib/go/sched.sx — goroutine scheduler + channels + select
|
||||||
|
│ (INDEPENDENT — no lib/guest/scheduler yet; this loop
|
||||||
|
│ builds the first consumer)
|
||||||
|
▼
|
||||||
|
lib/go/std/ — minimal stdlib slice (fmt, strings, strconv, sync,
|
||||||
|
time, errors)
|
||||||
```
|
```
|
||||||
|
|
||||||
Key semantic mappings:
|
Semantic mappings (operational):
|
||||||
- `go fn()` → spawn new coroutine (SX coroutine primitive, Phase 4 of primitives)
|
- `go fn(args)` → `task-spawn` on the local scheduler.
|
||||||
- `ch <- v` (send) → `perform` that suspends until receiver ready; scheduler picks next goroutine
|
- `ch <- v` → `task-block` with predicate "receiver waiting on ch".
|
||||||
- `v := <-ch` (receive) → `perform` that suspends until sender ready
|
- `v := <-ch` → `task-block` with predicate "sender waiting on ch".
|
||||||
- `select { case ... }` → scheduler checks all channel readiness, picks first ready
|
- `select { case ... }` → `task-block` with predicate "any case ready".
|
||||||
- `defer fn()` → push onto a per-goroutine defer stack; run on return/panic
|
- `defer fn()` → push thunk onto per-frame defer stack; runs LIFO on
|
||||||
- `panic(v)` → `raise` the value; `recover()` catches it in deferred function
|
return or panic.
|
||||||
- `interface{}` → any SX value (duck typed)
|
- `panic(v)` → raise SX exception; deferred fns run while unwinding.
|
||||||
- `struct { ... }` → SX hash table with field names as keys
|
- `recover()` → CEK exception capture inside a deferred fn.
|
||||||
- `slice` → SX vector with length + capacity metadata
|
- `interface{T}` → type-check matches structurally against T's method
|
||||||
- `map[K]V` → SX mutable hash table (Phase 10 of primitives)
|
set; at runtime, the value carries its concrete-type metadata.
|
||||||
|
- `struct{...}` → SX dict + type tag; methods are functions in the type's
|
||||||
|
method table.
|
||||||
|
- `*T` (pointer) → mutable cell (Common Lisp port did the same).
|
||||||
|
- `[]T` (slice) → triple (length, capacity, backing-vector).
|
||||||
|
- `map[K]V` → SX dict; iteration order spec-undefined (v1 = sorted for
|
||||||
|
determinism — programs that depend on indeterminism fail loudly, which
|
||||||
|
is a feature not a bug).
|
||||||
|
|
||||||
## Roadmap
|
## Conformance scoreboard
|
||||||
|
|
||||||
### Phase 1 — tokenizer + parser
|
Following `lib/erlang/scoreboard.json` precedent. Add
|
||||||
- [ ] Tokenizer: keywords (`package`, `import`, `func`, `var`, `const`, `type`, `struct`,
|
`lib/go/scoreboard.json` on first iteration; populate as suites land.
|
||||||
`interface`, `go`, `chan`, `select`, `defer`, `return`, `if`, `else`, `for`, `range`,
|
Suites planned:
|
||||||
`switch`, `case`, `default`, `break`, `continue`, `goto`, `fallthrough`, `map`,
|
|
||||||
`make`, `new`, `nil`, `true`, `false`), automatic semicolon insertion, string literals
|
|
||||||
(interpreted + raw `` `...` ``), rune literals `'a'`, number literals (int, float, hex,
|
|
||||||
octal, binary, complex), operators, slices `[:]`
|
|
||||||
- [ ] Parser: package clause, imports, top-level `func`/`var`/`const`/`type`; function
|
|
||||||
bodies: short variable decl `:=`, assignments, `if`/`else`, `for`/`range`, `switch`,
|
|
||||||
`return`, struct literals, slice literals, map literals, composite literals, type
|
|
||||||
assertions `v.(T)`, method calls `v.Method(args)`, goroutine `go`, channel ops
|
|
||||||
`<-ch`, `ch <- v`, `defer`, `select`
|
|
||||||
- [ ] Tests in `lib/go/tests/parse.sx`
|
|
||||||
|
|
||||||
### Phase 2 — transpile: basic Go (no goroutines)
|
| Suite | Tests target | What it covers |
|
||||||
- [ ] `go-eval-ast` entry
|
|---|---|---|
|
||||||
- [ ] Arithmetic, string ops, comparison, boolean
|
| `lex` | 50+ | Keywords, operators, literals, ASI |
|
||||||
- [ ] Variables, short decl, assignment, multiple assignment
|
| `parse` | 80+ | All statement & expression shapes |
|
||||||
- [ ] `if`/`else if`/`else`
|
| `types` | 90+ | Synth, check, interface satisfaction, generics |
|
||||||
- [ ] `for` (C-style), `for range` over slice/map/string
|
| `eval` | 100+ | Tree-walk over typed AST |
|
||||||
- [ ] Functions: named + anonymous, multiple return values (SX multiple values, Phase 8)
|
| `runtime` | 60+ | Goroutines, channels, select, close |
|
||||||
- [ ] Structs → SX hash tables; field access `.field`; struct literals `T{f: v}`
|
| `stdlib` | 40+ | fmt, strings, strconv, sync, time, errors |
|
||||||
- [ ] Slices → SX vectors; `len`, `cap`, `append`, `copy`, slice expressions `s[a:b]`
|
| `e2e` | 10+ | Complete representative programs |
|
||||||
- [ ] Maps → SX hash tables; `make(map[K]V)`, `m[k]`, `m[k] = v`, `delete(m, k)`,
|
|
||||||
comma-ok `v, ok := m[k]`
|
|
||||||
- [ ] Pointers — modelled as single-element mutable vectors; `&x` creates wrapper, `*p` dereferences
|
|
||||||
- [ ] `fmt.Println`/`fmt.Printf`/`fmt.Sprintf` → SX IO perform (print)
|
|
||||||
- [ ] 40+ eval tests in `lib/go/tests/eval.sx`
|
|
||||||
|
|
||||||
### Phase 3 — defer / panic / recover
|
## Phasing — one feature per commit
|
||||||
- [ ] Defer stack per function frame — SX list of thunks, run LIFO on return
|
|
||||||
- [ ] `defer` statement pushes thunk; transpiler wraps function body in try/finally equivalent
|
|
||||||
- [ ] `panic(v)` → `raise` with Go panic wrapper
|
|
||||||
- [ ] `recover()` → catches panic value inside a deferred function; returns nil otherwise
|
|
||||||
- [ ] Panic propagation across call stack until recovered or fatal
|
|
||||||
- [ ] Tests: defer ordering, panic/recover, panic in goroutine without recover
|
|
||||||
|
|
||||||
### Phase 4 — goroutines + channels
|
Loop-style. Each phase: implement → test → commit → tick `[ ]` → append
|
||||||
- [ ] Coroutine-based goroutine type using SX coroutine primitive (Phase 4 of primitives)
|
Progress-log line → push `origin/loops/go`.
|
||||||
- [ ] Round-robin scheduler in `lib/go/runtime.sx`: maintains run queue, steps each
|
|
||||||
goroutine one turn at a time, suspends at channel ops
|
|
||||||
- [ ] Unbuffered channels: `make(chan T)` → rendezvous point; send suspends until receive
|
|
||||||
and vice versa. Implemented as a pair of waiting queues + `cek-resume`.
|
|
||||||
- [ ] Buffered channels: `make(chan T, n)` → circular buffer; send only blocks when full,
|
|
||||||
receive only blocks when empty
|
|
||||||
- [ ] `close(ch)` — mark channel closed; receivers drain then get zero value + `false`
|
|
||||||
- [ ] `select` — scheduler inspects all cases, picks a ready one (random if multiple),
|
|
||||||
blocks if none ready until at least one becomes ready
|
|
||||||
- [ ] `go fn(args)` — spawns new goroutine on run queue
|
|
||||||
- [ ] `time.Sleep(d)` — yields current goroutine, re-queues after d milliseconds
|
|
||||||
(simulated with IO perform timer)
|
|
||||||
- [ ] Tests: ping-pong, fan-out, fan-in, select with default, range over channel
|
|
||||||
|
|
||||||
### Phase 5 — interfaces
|
### Phase 1 — Tokenizer (`lib/go/lex.sx`) ⬜
|
||||||
- [ ] Interface type → SX dict `{:type "T" :methods {...}}` dispatch table
|
- [x] Scaffold + scoreboard + conformance runner (consumes lib/guest/lex.sx)
|
||||||
- [ ] `interface{}` / `any` → any SX value (already implicit)
|
- [x] Identifiers + 25 keywords
|
||||||
- [ ] Type assertion `v.(T)` → check `:type` field, panic if mismatch
|
- [x] Decimal integer literals
|
||||||
- [ ] Type switch `switch v.(type) { case T: ... }` → dispatches on `:type`
|
- [x] Interpreted string literals `"..."` with `\n \t \r \\ \" \'` escapes
|
||||||
- [ ] Method sets — structs implement interfaces implicitly if they have the right methods
|
- [x] Rune literals `'x'` (single char + simple escapes)
|
||||||
- [ ] Value vs pointer receivers — pointer receiver gets the mutable vector wrapper
|
- [x] Line + block comments (block w/ newline triggers ASI)
|
||||||
- [ ] Built-in interfaces: `error` (`Error() string`), `Stringer` (`String() string`)
|
- [x] Common operator/punct set incl. `:= <- ++ -- == != <= >= && || ...`
|
||||||
- [ ] Tests: interface satisfaction, type assertion, type switch, error interface
|
- [x] **Automatic semicolon insertion** (Go spec § Semicolons) — newline,
|
||||||
|
EOF, and block-comment-with-newline trigger `;` after
|
||||||
|
ident/int/string/rune/{break,continue,fallthrough,return}/{++,--,),],}}.
|
||||||
|
- [ ] Float / imaginary literals
|
||||||
|
- [ ] Raw string literals `` `...` ``
|
||||||
|
- [ ] Hex/octal/binary integer literals (0x… 0o… 0b…) + underscores
|
||||||
|
- [ ] Full operator set audit (47 distinct per Go spec)
|
||||||
|
- **Acceptance:** lex/ suite at 50+ tests. Current: 78/78.
|
||||||
|
|
||||||
### Phase 6 — standard library subset
|
### Phase 2 — Parser (`lib/go/parse.sx`) ⬜
|
||||||
- [ ] `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`, `Stringer` dispatch
|
- Consume `lib/guest/core/pratt.sx` + `lib/guest/core/ast.sx`. Chisel notes
|
||||||
- [ ] `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`, `TrimSpace`,
|
`consumes-pratt consumes-ast`.
|
||||||
`ToUpper`, `ToLower`, `Replace`, `Index`, `Count`, `Repeat`
|
- Grammar coverage:
|
||||||
- [ ] `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`, `FormatInt`
|
- Declarations: `package`, `import`, `var`, `const`, `type`, `func`
|
||||||
- [ ] `math` — full surface via SX math primitives (Phase 15)
|
- Types: basic, slice `[]T`, array `[N]T`, map `map[K]V`, chan `chan T`,
|
||||||
- [ ] `sort` — `sort.Slice`, `sort.Ints`, `sort.Strings`
|
func `func(...)...`, struct, interface, pointer `*T`
|
||||||
- [ ] `errors` — `errors.New`, `errors.Is`, `errors.As`
|
- Expressions: literals, identifier, call, index `[]`, slice `[a:b]`,
|
||||||
- [ ] `sync` — `sync.Mutex` (cooperative — just a boolean flag + goroutine queue),
|
type assertion `v.(T)`, operators
|
||||||
`sync.WaitGroup`, `sync.Once`
|
- Statements: `if`/`else`, `for` (C-style + range), `switch`, `select`,
|
||||||
- [ ] `io` — `io.Reader`/`io.Writer` interfaces; `io.ReadAll`; `strings.NewReader`
|
`return`, `defer`, `go`, `break`/`continue`, assign, short-decl `:=`,
|
||||||
|
send `ch <- v`, recv `<-ch`
|
||||||
|
- Output: SX-shaped AST per `lib/guest/core/ast.sx` conventions.
|
||||||
|
- Tests: round-trip parse of hello world, fibonacci, FizzBuzz, goroutine
|
||||||
|
ping-pong, struct + method.
|
||||||
|
- **Acceptance:** parse/ suite at 80+ tests.
|
||||||
|
|
||||||
### Phase 7 — full conformance target
|
### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ⬜
|
||||||
- [ ] Vendor a Go test suite or hand-build 100+ program tests in `lib/go/tests/programs/`
|
- **Independent implementation.** Do NOT use lib/guest/static-types-
|
||||||
- [ ] Drive scoreboard
|
bidirectional/ — that kit doesn't exist yet and depends on this work
|
||||||
|
for its design. See `plans/lib-guest-static-types-bidirectional.md`.
|
||||||
|
- Synth + check judgments. Context as a value (per-block scope).
|
||||||
|
- Coverage MVP: declared-type variables, function signatures (params +
|
||||||
|
returns), call type-checking, simple composite types (slice, map, chan
|
||||||
|
element), interface satisfaction (structural match against method sets),
|
||||||
|
short variable declaration `:=` (synth from RHS).
|
||||||
|
- **Untyped constants.** `42` has type `untyped int` until contextualised;
|
||||||
|
this is the canonical pitfall (see Gotchas below).
|
||||||
|
- Defer: generics (Phase 7), full conversion rules.
|
||||||
|
- Tests: positive (type-correct programs check) + negative (mismatched
|
||||||
|
types fail with informative errors carrying AST paths).
|
||||||
|
- **Acceptance:** types/ suite at 60+ tests. Chisel note `shapes-static-
|
||||||
|
types-bidirectional` — append a paragraph to the sister plan's design
|
||||||
|
diary describing what synth/check shape emerged.
|
||||||
|
|
||||||
|
### Phase 4 — Tree-walk evaluator (`lib/go/eval.sx`) ⬜
|
||||||
|
- AST-walking interpreter over CEK. Each Go statement maps to one step
|
||||||
|
function (precedent: `step-sf-if` etc. in spec/evaluator.sx).
|
||||||
|
- Variables: mutable cells. Pointer semantics: `&x` returns the cell,
|
||||||
|
`*p` dereferences.
|
||||||
|
- Slices: triple (length, capacity, backing-vector). `append` honours
|
||||||
|
capacity-grow per spec.
|
||||||
|
- Maps: SX dict + key-type metadata.
|
||||||
|
- Structs: SX dict + type tag. Methods looked up via type's method table.
|
||||||
|
- Functions: closures over enclosing scope; multiple return values.
|
||||||
|
- Channels: stub (Phase 5 wires them).
|
||||||
|
- Tests: arithmetic, control flow, recursion, closures, slices, maps,
|
||||||
|
structs, methods, pointer semantics, multiple-return.
|
||||||
|
- **Acceptance:** eval/ suite at 80+ tests. No concurrency yet.
|
||||||
|
|
||||||
|
### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ⬜
|
||||||
|
- **Independent implementation.** Do NOT use lib/guest/scheduler/ — that
|
||||||
|
kit doesn't exist yet and depends on this work for its design. See
|
||||||
|
`plans/lib-guest-scheduler.md`.
|
||||||
|
- `go expr` — spawn a goroutine; returns nothing.
|
||||||
|
- `chan T` — `make(chan T)` creates an unbuffered channel; `make(chan T,n)`
|
||||||
|
creates a buffered channel (Phase 5b — defer buffer to a sub-phase).
|
||||||
|
- `<-ch` — receive (blocks until sender ready).
|
||||||
|
- `ch <- v` — send (blocks until receiver ready for unbuffered, or buffer
|
||||||
|
has room for buffered).
|
||||||
|
- `select { case ... }` — non-deterministic multiplexing; `default` makes
|
||||||
|
it non-blocking.
|
||||||
|
- `close(ch)` — closes channel. Receive on closed → zero value + ok=false.
|
||||||
|
- Tests: ping-pong, fan-out/fan-in, work queue, select with default,
|
||||||
|
select with timeout (via a `time.After`-like stub), close semantics,
|
||||||
|
range over channel.
|
||||||
|
- **Acceptance:** runtime/ suite at 40+ tests. Chisel note `shapes-
|
||||||
|
scheduler` — append a paragraph to the sister plan's design diary
|
||||||
|
describing what task-spawn/block/wake/yield shape emerged.
|
||||||
|
|
||||||
|
### Phase 5b — Buffered channels + select fairness ⬜
|
||||||
|
- Buffered: send blocks only when buffer full; recv only when empty.
|
||||||
|
- `select` random case ordering (spec mandates pseudo-random; v1 uses a
|
||||||
|
fixed seed for determinism with a `runtime`-package knob to randomise).
|
||||||
|
- Tests: buffer-full blocking, buffer-empty blocking, select fairness
|
||||||
|
over many iterations.
|
||||||
|
- **Acceptance:** runtime/ +20 tests.
|
||||||
|
|
||||||
|
### Phase 6 — `defer` + panic/recover ⬜
|
||||||
|
- Defer stack per function frame; runs LIFO on return (normal or panic).
|
||||||
|
- `panic(v)` unwinds frames running deferreds; `recover()` inside a
|
||||||
|
deferred fn captures the panic value and stops unwinding.
|
||||||
|
- Goroutine panic propagation: a panicking goroutine that doesn't recover
|
||||||
|
crashes the whole program (honour Go spec, or document divergence).
|
||||||
|
- Tests: defer order (LIFO), defer + named-return mutation, panic/recover,
|
||||||
|
panic across goroutines, defer in a loop (push per iter, run on fn
|
||||||
|
return — common bug).
|
||||||
|
- **Acceptance:** eval/ +20 tests.
|
||||||
|
|
||||||
|
### Phase 7 — Generics (Go 1.18+) ⬜
|
||||||
|
- Type parameters with constraints (type sets: `interface{ int | float64
|
||||||
|
}`, `comparable`, `any`).
|
||||||
|
- Type inference at call sites — basic; the full Go inference algorithm
|
||||||
|
is notoriously complex. Implement enough for common cases; document
|
||||||
|
limitations in a Blockers section below.
|
||||||
|
- Tests: generic function (`func Map[T, U any](xs []T, f func(T) U) []U`),
|
||||||
|
generic data structure (linked list), constrained type param.
|
||||||
|
- **Acceptance:** types/ +30 tests.
|
||||||
|
|
||||||
|
### Phase 8 — Minimal stdlib (`lib/go/std/`) ⬜
|
||||||
|
- Implement just what's needed for representative programs:
|
||||||
|
- `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`,
|
||||||
|
`Stringer` dispatch. Verbs: `%d %s %v %t %f %T %+v`.
|
||||||
|
- `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`,
|
||||||
|
`TrimSpace`, `ToUpper`, `ToLower`, `Replace`, `Index`, `Count`,
|
||||||
|
`Repeat`, `NewReader`.
|
||||||
|
- `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`,
|
||||||
|
`FormatInt`.
|
||||||
|
- `errors` — `New`, `Is`, `As`, `Unwrap`.
|
||||||
|
- `sync` — `Mutex` (cooperative — flag + waiter queue), `WaitGroup`,
|
||||||
|
`Once`, `RWMutex`.
|
||||||
|
- `time` — `Now`, `Since`, `After` (channel-returning timer), `Sleep`,
|
||||||
|
`Duration`, `Time`.
|
||||||
|
- `io` — `Reader`/`Writer` interfaces; `ReadAll`; `Copy`.
|
||||||
|
- `sort` — `Slice`, `Ints`, `Strings`.
|
||||||
|
- Tests: round-trip Itoa/Atoi, fmt verb coverage, sync.WaitGroup with
|
||||||
|
goroutines, time.After in a select, sort.Slice with custom less fn.
|
||||||
|
- **Acceptance:** stdlib/ suite at 40+ tests.
|
||||||
|
|
||||||
|
### Phase 9 — End-to-end programs ⬜
|
||||||
|
- Complete programs from canonical sources (gopl.io, "concurrency
|
||||||
|
patterns" talk examples) running end-to-end:
|
||||||
|
- Concurrent prime sieve
|
||||||
|
- HTTP-ish ping-pong over stubbed transport
|
||||||
|
- Word frequency counter
|
||||||
|
- Pipeline (channel chain)
|
||||||
|
- Producer/consumer with sync.WaitGroup
|
||||||
|
- "Bounded parallelism" pattern (worker pool over a job channel)
|
||||||
|
- **Acceptance:** e2e/ suite at 10+ tests, all passing.
|
||||||
|
|
||||||
|
### Phase 10 — lib/guest extraction enabler ⬜
|
||||||
|
- Now that Go has lex+parse+types+eval+sched, sister plans are unblocked
|
||||||
|
on the Go side. This phase is **doc-only** in `loops/go`:
|
||||||
|
- Cross-reference `plans/lib-guest-scheduler.md` — mark its Phase 1
|
||||||
|
(Go scheduler independent) as complete from Go's side.
|
||||||
|
- Cross-reference `plans/lib-guest-static-types-bidirectional.md` —
|
||||||
|
mark its Phase 1 as complete from Go's side.
|
||||||
|
- Update the chiselling diary in each sister plan with the actual
|
||||||
|
Go-side surface that emerged.
|
||||||
|
- **Acceptance:** sister plans cross-referenced + diaries updated. No
|
||||||
|
new Go code.
|
||||||
|
|
||||||
|
### Phase 11 — VM bytecode opcodes (deferred, optional) ⬜
|
||||||
|
- Following Erlang-on-SX Phase 10 precedent: identify hot paths in the
|
||||||
|
tree-walk evaluator, define Go-specific bytecode opcodes, compile hot
|
||||||
|
fns through them. Substantial work; only justified if Go programs
|
||||||
|
exercise enough volume that performance starts mattering.
|
||||||
|
- **Acceptance:** TBD on demand.
|
||||||
|
|
||||||
|
## Ground rules (loop-style)
|
||||||
|
|
||||||
|
- **Scope:** only `lib/go/**` and this plan. Do not touch `spec/`,
|
||||||
|
`hosts/`, `shared/`, `lib/guest/**` (read-only consumer at this phase),
|
||||||
|
or other `lib/<lang>/`.
|
||||||
|
- **Consume `lib/guest/core/`** for lex/parse/ast/match/layout. Hand-
|
||||||
|
rolling defeats the chiselling goal.
|
||||||
|
- **Do NOT extract into `lib/guest/scheduler/` or `lib/guest/static-
|
||||||
|
types-bidirectional/` from this loop.** Those extractions are gated on
|
||||||
|
two consumers AND the discipline of writing each consumer
|
||||||
|
independently. Extraction is its own workstream after Go and the
|
||||||
|
second consumer both exist.
|
||||||
|
- **Substrate gaps** → Blockers entry with minimal repro. Don't fix the
|
||||||
|
substrate from this loop. Belongs to `sx-improvements.md`.
|
||||||
|
- **NEVER call `sx_build` without timeout awareness** — 600s watchdog.
|
||||||
|
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit.
|
||||||
|
- **Worktree:** branch `loops/go`, push `origin/loops/go`. Never `main`,
|
||||||
|
never `architecture`.
|
||||||
|
- **Commit granularity:** one feature per commit. Short factual messages:
|
||||||
|
`go: parse short-decl + 6 tests [consumes-pratt]`. Chisel note at end
|
||||||
|
in brackets.
|
||||||
|
- **Plan file:** update Progress log + tick boxes every commit.
|
||||||
|
- **If blocked** for two iterations on the same issue, add to Blockers
|
||||||
|
and move on. Phases 1-4 are sequential; Phases 5-8 are largely
|
||||||
|
independent once 4 lands.
|
||||||
|
|
||||||
|
## Chisel discipline (per parent lib-guest plan)
|
||||||
|
|
||||||
|
Every commit ends its message with a chisel note in brackets:
|
||||||
|
|
||||||
|
- `[consumes-X]` — used `lib/guest/X` kit.
|
||||||
|
- `[shapes-scheduler]` / `[shapes-static-types-bidirectional]` — revealed
|
||||||
|
something about what the sister lib-guest kits should look like. Add a
|
||||||
|
paragraph to the relevant sister plan's design diary.
|
||||||
|
- `[proposes-Y]` — revealed a gap in another existing kit. Blockers entry
|
||||||
|
in the kit's plan.
|
||||||
|
- `[nothing]` — pure Go work that didn't touch substrate or lib/guest
|
||||||
|
story. Acceptable; if it shows up twice in a row, stop and reflect.
|
||||||
|
|
||||||
|
## Go-specific gotchas
|
||||||
|
|
||||||
|
- **ASI (automatic semicolon insertion).** Newline becomes `;` after
|
||||||
|
identifier/literal/`)`/`]`/`}`. Build into the tokenizer; the Go spec's
|
||||||
|
"Semicolons" section is unusually precise — follow it literally.
|
||||||
|
- **Untyped constants.** `42` has type `untyped int` until used in a
|
||||||
|
context that forces a type. The canonical example: `var x float64 = 42
|
||||||
|
/ 7` — must compute as `untyped int / untyped int = 6` then convert to
|
||||||
|
`float64 = 6.0`. Wrong: float-coercing eagerly gives 6.0 prematurely.
|
||||||
|
Wrong: integer-truncating after coercion gives `5.something`. Test it.
|
||||||
|
- **Methods vs functions.** `func (r Receiver) Method()` is a method
|
||||||
|
bound to a type; `func Function(r Receiver)` is just a function.
|
||||||
|
Methods on pointer-receivers vs value-receivers have asymmetric
|
||||||
|
satisfaction in interfaces — pointer-receiver methods are NOT in the
|
||||||
|
value's method set for interface satisfaction.
|
||||||
|
- **Interface satisfaction is structural and silent.** Type satisfies an
|
||||||
|
interface if its method set contains all the interface's methods.
|
||||||
|
Lazy check: at every point a value flows into an interface-typed slot.
|
||||||
|
- **Channels are first-class values.** Pass them, store them, send them
|
||||||
|
through other channels. Each channel has identity.
|
||||||
|
- **`select` with `default`** = non-blocking. Without `default`, blocks
|
||||||
|
until a case is ready.
|
||||||
|
- **`nil` is typed.** `var x *int` makes x a `(*int)(nil)`. Comparison
|
||||||
|
`x == nil` works on typed nil; but `var i interface{} = (*int)(nil); i
|
||||||
|
== nil` is `false` — i holds a typed-nil-of-type-`*int`, not untyped
|
||||||
|
nil. The classic Go footgun. Test it.
|
||||||
|
- **Goroutine panic propagation.** A panicking goroutine that doesn't
|
||||||
|
recover crashes the whole program. Implement faithfully or document
|
||||||
|
divergence.
|
||||||
|
- **`defer` in a loop.** Each iteration pushes; they all run on function
|
||||||
|
return. Common bug; tests should cover.
|
||||||
|
- **Iteration order of maps.** Spec: unspecified. v1 = sorted by SX-
|
||||||
|
canonical key order for determinism; document that programs depending
|
||||||
|
on iteration order are not Go-conformant. Add a `runtime`-package knob
|
||||||
|
to enable randomisation later.
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
- No comments in `.sx` unless non-obvious. Cite Go spec sections inline
|
||||||
|
for non-obvious decisions (Go's spec is rigorous; citations work).
|
||||||
|
- No new planning docs — update this plan inline.
|
||||||
|
- One feature per iteration. Commit. Log. Push. Next.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. **Module/import model.** Go has packages and import paths. Probably
|
||||||
|
model "package" as one or more `.sx` files in a directory, no real
|
||||||
|
import resolution against a remote module graph. Decide in Phase 2.
|
||||||
|
2. **Goroutine identity.** Spec says goroutines have no identity; the
|
||||||
|
scheduler does internally. Expose to user code? No (not Go). Expose
|
||||||
|
for debugging? Yes via a `runtime`-package stub.
|
||||||
|
3. **Error handling: panic-as-exception vs explicit error returns.** Go
|
||||||
|
strongly prefers explicit errors. Stdlib stubs follow that: `strconv.
|
||||||
|
Atoi("x")` returns `(0, err)`, not panic.
|
||||||
|
4. **Memory model.** Go has a happens-before model for atomics + channel
|
||||||
|
ops. SX runtime is single-threaded under the scheduler — every channel
|
||||||
|
op is a synchronization point automatically. Don't model relaxed
|
||||||
|
memory; document the simplification.
|
||||||
|
5. **Iteration order of maps.** Already addressed in Gotchas; flagged
|
||||||
|
here as a known divergence from spec.
|
||||||
|
|
||||||
## Blockers
|
## Blockers
|
||||||
|
|
||||||
@@ -140,6 +402,16 @@ _(none yet)_
|
|||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
_Newest first._
|
_Newest first. Append one dated entry per commit._
|
||||||
|
|
||||||
_(awaiting phase 1)_
|
- 2026-05-26 — Phase 1 first slice: `lib/go/lex.sx` tokenizer consuming
|
||||||
|
`lib/guest/lex.sx` predicates. 25 keywords, ident/int/string/rune lits,
|
||||||
|
line+block comments, common operators, automatic semicolon insertion per
|
||||||
|
Go spec § Semicolons (newline / EOF / block-comment-with-newline triggers).
|
||||||
|
Scoreboard + conformance.sh wired. 78/78 tests. `[consumes-lex]`.
|
||||||
|
- 2026-05-26 — Plan rewritten to integrate the lib/guest framework
|
||||||
|
(chiselling discipline, sister plans for scheduler + bidirectional
|
||||||
|
types, type-checker phase added, conformance scoreboard model adopted).
|
||||||
|
Original 2026-04-26 draft preserved in git history. Loop not yet
|
||||||
|
kicked off; Phase 1 (tokenizer) is the first iteration when this loop
|
||||||
|
spins up.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user