fed-sx-m1: Step 8d-content-type — content_type_for/1 + ok_response/2 + 13 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 24s

This commit is contained in:
2026-05-28 15:04:46 +00:00
parent 3c945b9104
commit 1aaede4272
3 changed files with 157 additions and 2 deletions

View File

@@ -11,7 +11,8 @@
validation_failed_response/0,
cid_response/1,
accept_format/1, accept_format_from/1,
capabilities_body_for/1]).
capabilities_body_for/1,
content_type_for/1, ok_response/2]).
%% HTTP request router per design §16.1.
%%
@@ -389,3 +390,36 @@ capabilities_body_for(cbor) ->
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}].

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

@@ -0,0 +1,119 @@
#!/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 ]

View File

@@ -519,7 +519,8 @@ publish(ActorId, ActivityRequest) ->
- [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).
- [ ] **8d-dispatch-rest** — Same treatment for `actor_doc_response`, `artifact_response`, `projection_response`, `cid_response` (plus Content-Type headers).
- [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).
- [ ] **8d-dispatch-rest** — Wire `ok_response/2` + format dispatch into `actor_doc_response`, `artifact_response`, `projection_response`, `cid_response`.
**Deliverables:**
@@ -998,6 +999,7 @@ A few things still under-specified; resolve as work begins.
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 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.