fed-sx-m2: Step 4a — per-actor HTTP sub-paths + 17 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Per design §16.1 each actor has /outbox /inbox /followers /following sub-paths. New split_first_slash/1 helper lets the GET /actors/... dispatch arm fan out on the sub-segment: GET /actors/<id> actor doc (M1 — unchanged) GET /actors/<id>/outbox outbox stub (4a) GET /actors/<id>/inbox inbox stub (4a) GET /actors/<id>/followers follower stub (4a) GET /actors/<id>/following following stub (4a) POST /actors/<id>/inbox 202 Accepted stub (4a; Step 5 real) Four new content-negotiated response functions mirror the existing actor_doc_response_for/2 shape (text / json / activity_json / sx variants): actor_outbox_response_for/2 actor_inbox_get_response_for/2 actor_followers_response_for/2 actor_following_response_for/2 POST returns 202 via new accepted_response/1 + actor_inbox_post_response/0. Unknown sub-paths under /actors/<id>/ return 404. Bare /actors/<id> preserves the M1 actor-doc arm so http_route + http_post_format regression suites stay green. 4b-4e (token map, route/3 kernel access, per-actor outbox listing from log entries, real inbox pipeline) layer on top of this dispatch in subsequent iterations. 17/17 in next/tests/http_multi_actor.sh covering: - split_first_slash sanity (no slash / id+sub / trailing slash) - all four GET sub-paths return 200 with stub bodies - POST inbox returns 202 + 'accepted' - unknown sub-paths return 404 (GET and POST) - empty /actors/ returns 404 - body carries the actor id - content negotiation: outbox JSON, inbox SX, followers JSON Conformance 761/761. 120/120 across 10 Step-4-adjacent suites (http_route, http_publish, http_post_format, http_marshal, http_publish_fold, http_listen_bif, http_server_start, nx_kernel_multi, actor_state_pure, bootstrap_start).
This commit is contained in:
@@ -16,7 +16,11 @@
|
|||||||
content_type_for/1, ok_response/2,
|
content_type_for/1, ok_response/2,
|
||||||
cid_response_for/2, post_activity_response_for/1,
|
cid_response_for/2, post_activity_response_for/1,
|
||||||
actor_doc_response_for/2, artifact_response_for/2,
|
actor_doc_response_for/2, artifact_response_for/2,
|
||||||
projection_response_for/2, projections_list_response_for/1]).
|
projection_response_for/2, projections_list_response_for/1,
|
||||||
|
actor_outbox_response_for/2, actor_inbox_get_response_for/2,
|
||||||
|
actor_followers_response_for/2, actor_following_response_for/2,
|
||||||
|
actor_inbox_post_response/0, accepted_response/1,
|
||||||
|
split_first_slash/1]).
|
||||||
|
|
||||||
%% HTTP request router per design §16.1.
|
%% HTTP request router per design §16.1.
|
||||||
%%
|
%%
|
||||||
@@ -94,11 +98,11 @@ dispatch(<<71, 69, 84>>,
|
|||||||
%% prefix clause because the bare path has no trailing slash.
|
%% 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) ->
|
dispatch(<<71, 69, 84>>, <<47,112,114,111,106,101,99,116,105,111,110,115>>, F) ->
|
||||||
projections_list_response_for(F);
|
projections_list_response_for(F);
|
||||||
%% GET /actors/{id} or /artifacts/{cid} or /projections/{name}
|
%% GET /actors/{id}[/sub] or /artifacts/{cid} or /projections/{name}
|
||||||
dispatch(<<71, 69, 84>>, Path, F) ->
|
dispatch(<<71, 69, 84>>, Path, F) ->
|
||||||
case match_prefix(actors_prefix(), Path) of
|
case match_prefix(actors_prefix(), Path) of
|
||||||
{ok, Id} when byte_size(Id) > 0 ->
|
{ok, Rest} when byte_size(Rest) > 0 ->
|
||||||
actor_doc_response_for(Id, F);
|
actor_get(Rest, F);
|
||||||
_ ->
|
_ ->
|
||||||
case match_prefix(artifacts_prefix(), Path) of
|
case match_prefix(artifacts_prefix(), Path) of
|
||||||
{ok, Cid} when byte_size(Cid) > 0 ->
|
{ok, Cid} when byte_size(Cid) > 0 ->
|
||||||
@@ -112,9 +116,71 @@ dispatch(<<71, 69, 84>>, Path, F) ->
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
|
%% POST /actors/{id}/inbox — peer-side delivery (Step 4a returns
|
||||||
|
%% 202 Accepted stub; Step 5 lands the real ingestion pipeline).
|
||||||
|
dispatch(<<80, 79, 83, 84>>, Path, _F) ->
|
||||||
|
case match_prefix(actors_prefix(), Path) of
|
||||||
|
{ok, Rest} when byte_size(Rest) > 0 ->
|
||||||
|
actor_post(Rest);
|
||||||
|
_ ->
|
||||||
|
not_found_response()
|
||||||
|
end;
|
||||||
dispatch(_, _, _) ->
|
dispatch(_, _, _) ->
|
||||||
not_found_response().
|
not_found_response().
|
||||||
|
|
||||||
|
%% actor_get/2 — Rest is the part after "/actors/". If it has no
|
||||||
|
%% inner slash, it's the bare actor doc. Otherwise dispatch on the
|
||||||
|
%% sub-segment.
|
||||||
|
|
||||||
|
actor_get(Rest, F) ->
|
||||||
|
case split_first_slash(Rest) of
|
||||||
|
{Id, <<>>} -> actor_doc_response_for(Id, F);
|
||||||
|
{Id, Sub} -> actor_subresource_get(Id, Sub, F);
|
||||||
|
Id -> actor_doc_response_for(Id, F)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 111 117 116 98 111 120 = "outbox"
|
||||||
|
actor_subresource_get(Id, <<111,117,116,98,111,120>>, F) ->
|
||||||
|
actor_outbox_response_for(Id, F);
|
||||||
|
%% 105 110 98 111 120 = "inbox"
|
||||||
|
actor_subresource_get(Id, <<105,110,98,111,120>>, F) ->
|
||||||
|
actor_inbox_get_response_for(Id, F);
|
||||||
|
%% 102 111 108 108 111 119 101 114 115 = "followers"
|
||||||
|
actor_subresource_get(Id, <<102,111,108,108,111,119,101,114,115>>, F) ->
|
||||||
|
actor_followers_response_for(Id, F);
|
||||||
|
%% 102 111 108 108 111 119 105 110 103 = "following"
|
||||||
|
actor_subresource_get(Id, <<102,111,108,108,111,119,105,110,103>>, F) ->
|
||||||
|
actor_following_response_for(Id, F);
|
||||||
|
actor_subresource_get(_, _, _) ->
|
||||||
|
not_found_response().
|
||||||
|
|
||||||
|
actor_post(Rest) ->
|
||||||
|
case split_first_slash(Rest) of
|
||||||
|
{_Id, <<105,110,98,111,120>>} ->
|
||||||
|
actor_inbox_post_response();
|
||||||
|
_ ->
|
||||||
|
not_found_response()
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% split_first_slash/1 — split a binary on the first slash. Returns
|
||||||
|
%% {Before, After} where After omits the slash itself. If no slash
|
||||||
|
%% is present, returns just Before. 47 = "/".
|
||||||
|
%%
|
||||||
|
%% <<"alice">> -> <<"alice">>
|
||||||
|
%% <<"alice/">> -> {<<"alice">>, <<>>}
|
||||||
|
%% <<"alice/inbox">> -> {<<"alice">>, <<"inbox">>}
|
||||||
|
%% <<"alice/inbox/x">> -> {<<"alice">>, <<"inbox/x">>}
|
||||||
|
|
||||||
|
split_first_slash(Bin) ->
|
||||||
|
split_first_slash(Bin, <<>>).
|
||||||
|
|
||||||
|
split_first_slash(<<>>, Acc) ->
|
||||||
|
Acc;
|
||||||
|
split_first_slash(<<47, Rest/binary>>, Acc) ->
|
||||||
|
{Acc, Rest};
|
||||||
|
split_first_slash(<<B, Rest/binary>>, Acc) ->
|
||||||
|
split_first_slash(Rest, <<Acc/binary, B>>).
|
||||||
|
|
||||||
%% "fed-sx kernel m1\n" — 17 bytes, hand-spelled.
|
%% "fed-sx kernel m1\n" — 17 bytes, hand-spelled.
|
||||||
%% f e d - s x _ k e r n e l _ m 1 \n
|
%% f e d - s x _ k e r n e l _ m 1 \n
|
||||||
welcome_body() ->
|
welcome_body() ->
|
||||||
@@ -538,6 +604,105 @@ actor_doc_response_for(Id, cbor) ->
|
|||||||
actor_doc_response_for(Id, _) ->
|
actor_doc_response_for(Id, _) ->
|
||||||
actor_doc_response(Id).
|
actor_doc_response(Id).
|
||||||
|
|
||||||
|
%% ── Step 4a: per-actor sub-resource stubs ──────────────────────
|
||||||
|
%% Per design §16.1 each actor has /outbox /inbox /followers
|
||||||
|
%% /following routes. v1 returns text-stub bodies so route resolution
|
||||||
|
%% can be tested end-to-end; real serialisation of per-actor outbox
|
||||||
|
%% listings (Step 4d) + follower-graph projection bodies (Step 6+)
|
||||||
|
%% layer on top of these dispatch arms.
|
||||||
|
|
||||||
|
%% "outbox: " — 8 bytes
|
||||||
|
actor_outbox_response_for(Id, text) ->
|
||||||
|
Pre = <<111,117,116,98,111,120,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>);
|
||||||
|
actor_outbox_response_for(Id, json) ->
|
||||||
|
Pre = <<123,34,111,117,116,98,111,120,34,58,34>>, % '{"outbox":"'
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, json);
|
||||||
|
actor_outbox_response_for(Id, activity_json) ->
|
||||||
|
Pre = <<123,34,111,117,116,98,111,120,34,58,34>>,
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, activity_json);
|
||||||
|
actor_outbox_response_for(Id, sx) ->
|
||||||
|
Pre = <<40,111,117,116,98,111,120,32,34>>, % '(outbox "'
|
||||||
|
Suf = <<34,41,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, sx);
|
||||||
|
actor_outbox_response_for(Id, _) ->
|
||||||
|
Pre = <<111,117,116,98,111,120,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>).
|
||||||
|
|
||||||
|
%% "inbox: " — 7 bytes
|
||||||
|
actor_inbox_get_response_for(Id, text) ->
|
||||||
|
Pre = <<105,110,98,111,120,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>);
|
||||||
|
actor_inbox_get_response_for(Id, json) ->
|
||||||
|
Pre = <<123,34,105,110,98,111,120,34,58,34>>, % '{"inbox":"'
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, json);
|
||||||
|
actor_inbox_get_response_for(Id, activity_json) ->
|
||||||
|
Pre = <<123,34,105,110,98,111,120,34,58,34>>,
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, activity_json);
|
||||||
|
actor_inbox_get_response_for(Id, sx) ->
|
||||||
|
Pre = <<40,105,110,98,111,120,32,34>>, % '(inbox "'
|
||||||
|
Suf = <<34,41,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, sx);
|
||||||
|
actor_inbox_get_response_for(Id, _) ->
|
||||||
|
Pre = <<105,110,98,111,120,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>).
|
||||||
|
|
||||||
|
%% "followers: " — 11 bytes
|
||||||
|
actor_followers_response_for(Id, text) ->
|
||||||
|
Pre = <<102,111,108,108,111,119,101,114,115,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>);
|
||||||
|
actor_followers_response_for(Id, json) ->
|
||||||
|
Pre = <<123,34,102,111,108,108,111,119,101,114,115,34,58,34>>,
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, json);
|
||||||
|
actor_followers_response_for(Id, activity_json) ->
|
||||||
|
Pre = <<123,34,102,111,108,108,111,119,101,114,115,34,58,34>>,
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, activity_json);
|
||||||
|
actor_followers_response_for(Id, sx) ->
|
||||||
|
Pre = <<40,102,111,108,108,111,119,101,114,115,32,34>>,
|
||||||
|
Suf = <<34,41,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, sx);
|
||||||
|
actor_followers_response_for(Id, _) ->
|
||||||
|
Pre = <<102,111,108,108,111,119,101,114,115,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>).
|
||||||
|
|
||||||
|
%% "following: " — 11 bytes
|
||||||
|
actor_following_response_for(Id, text) ->
|
||||||
|
Pre = <<102,111,108,108,111,119,105,110,103,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>);
|
||||||
|
actor_following_response_for(Id, json) ->
|
||||||
|
Pre = <<123,34,102,111,108,108,111,119,105,110,103,34,58,34>>,
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, json);
|
||||||
|
actor_following_response_for(Id, activity_json) ->
|
||||||
|
Pre = <<123,34,102,111,108,108,111,119,105,110,103,34,58,34>>,
|
||||||
|
Suf = <<34,125,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, activity_json);
|
||||||
|
actor_following_response_for(Id, sx) ->
|
||||||
|
Pre = <<40,102,111,108,108,111,119,105,110,103,32,34>>,
|
||||||
|
Suf = <<34,41,10>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, Suf/binary>>, sx);
|
||||||
|
actor_following_response_for(Id, _) ->
|
||||||
|
Pre = <<102,111,108,108,111,119,105,110,103,58,32>>,
|
||||||
|
ok_response(<<Pre/binary, Id/binary, 10>>).
|
||||||
|
|
||||||
|
%% POST /actors/<id>/inbox stub — 202 Accepted with body "accepted\n".
|
||||||
|
%% Real ingestion pipeline (sig verify + envelope:get_field + log
|
||||||
|
%% append on the receiving actor's inbox bucket) lands in Step 5.
|
||||||
|
|
||||||
|
actor_inbox_post_response() ->
|
||||||
|
%% "accepted\n" — 9 bytes
|
||||||
|
Body = <<97,99,99,101,112,116,101,100,10>>,
|
||||||
|
accepted_response(Body).
|
||||||
|
|
||||||
|
accepted_response(Body) ->
|
||||||
|
[{status, 202}, {headers, []}, {body, Body}].
|
||||||
|
|
||||||
%% artifact_response — text body `artifact: <cid>\n`.
|
%% artifact_response — text body `artifact: <cid>\n`.
|
||||||
|
|
||||||
artifact_response_for(Cid, text) ->
|
artifact_response_for(Cid, text) ->
|
||||||
|
|||||||
151
next/tests/http_multi_actor.sh
Executable file
151
next/tests/http_multi_actor.sh
Executable file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# next/tests/http_multi_actor.sh — m2 Step 4 tests (4a: per-actor
|
||||||
|
# URL sub-paths).
|
||||||
|
#
|
||||||
|
# Per design §16.1 each actor has:
|
||||||
|
# GET /actors/<id> actor doc (M1)
|
||||||
|
# GET /actors/<id>/outbox outbox listing (4a: stub)
|
||||||
|
# GET /actors/<id>/inbox inbox listing (4a: stub)
|
||||||
|
# GET /actors/<id>/followers follower list (4a: stub)
|
||||||
|
# GET /actors/<id>/following following list (4a: stub)
|
||||||
|
# POST /actors/<id>/inbox peer delivery (4a: 202 stub; Step 5 real)
|
||||||
|
#
|
||||||
|
# 4b-4e wire the routes to per-actor kernel state + token map.
|
||||||
|
|
||||||
|
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)")
|
||||||
|
|
||||||
|
;; split_first_slash sanity
|
||||||
|
(epoch 10)
|
||||||
|
(eval "(get (erlang-eval-ast \"http_server:split_first_slash(<<97,108,105,99,101>>) =:= <<97,108,105,99,101>>\") :name)")
|
||||||
|
(epoch 11)
|
||||||
|
(eval "(get (erlang-eval-ast \"http_server:split_first_slash(<<97,108,105,99,101,47,105,110,98,111,120>>) =:= {<<97,108,105,99,101>>, <<105,110,98,111,120>>}\") :name)")
|
||||||
|
(epoch 12)
|
||||||
|
(eval "(get (erlang-eval-ast \"http_server:split_first_slash(<<97,108,105,99,101,47>>) =:= {<<97,108,105,99,101>>, <<>>}\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/alice returns actor doc (regression check — M1 path)
|
||||||
|
(epoch 20)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<97,99,116,111,114,58>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/alice/outbox returns outbox stub
|
||||||
|
(epoch 21)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<111,117,116,98,111,120,58>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/alice/inbox returns inbox stub
|
||||||
|
(epoch 22)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,105,110,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<105,110,98,111,120,58>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/alice/followers returns followers stub
|
||||||
|
(epoch 23)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,101,114,115>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<102,111,108,108,111,119,101,114,115,58>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/alice/following returns following stub
|
||||||
|
(epoch 24)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,105,110,103>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 200}, _, {body, B}] -> http_server:match_prefix(<<102,111,108,108,111,119,105,110,103,58>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; POST /actors/alice/inbox returns 202 with "accepted"
|
||||||
|
(epoch 25)
|
||||||
|
(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,47,105,110,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 202}, _, {body, B}] -> http_server:match_prefix(<<97,99,99,101,112,116,101,100>>, B) =/= nomatch; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/alice/unknown returns 404
|
||||||
|
(epoch 26)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,117,110,107,110,111,119,110>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 404}, _, _] -> true; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; POST /actors/alice/unknown returns 404
|
||||||
|
(epoch 27)
|
||||||
|
(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,47,117,110,107,110,111,119,110>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 404}, _, _] -> true; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/ (no id) returns 404 (existing behaviour preserved)
|
||||||
|
(epoch 28)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), case R of [{status, 404}, _, _] -> true; _ -> false end\") :name)")
|
||||||
|
|
||||||
|
;; GET /actors/bob/outbox carries bob's id in the stub body
|
||||||
|
(epoch 29)
|
||||||
|
(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,98,111,98,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req), [{status, 200}, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,98,111,98>>, B) =/= nomatch\") :name)")
|
||||||
|
|
||||||
|
;; Accept: application/json on /actors/alice/outbox -> JSON stub
|
||||||
|
(epoch 30)
|
||||||
|
(eval "(get (erlang-eval-ast \"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, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<123,34,111,117,116,98,111,120,34>>, B) =/= nomatch\") :name)")
|
||||||
|
|
||||||
|
;; Accept: application/sx on /actors/alice/inbox -> SX stub
|
||||||
|
(epoch 31)
|
||||||
|
(eval "(get (erlang-eval-ast \"AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>, Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,105,110,98,111,120>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<40,105,110,98,111,120,32>>, B) =/= nomatch\") :name)")
|
||||||
|
|
||||||
|
;; Accept: application/json on /actors/alice/followers -> JSON stub
|
||||||
|
(epoch 32)
|
||||||
|
(eval "(get (erlang-eval-ast \"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, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,102,111,108,108,111,119,101,114,115>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(Req), [_, _, {body, B}] = R, http_server:match_prefix(<<123,34,102,111,108,108,111,119,101,114,115,34>>, B) =/= nomatch\") :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 "http_server loaded" "http_server"
|
||||||
|
check 10 "split sans slash returns bare" "true"
|
||||||
|
check 11 "split id/sub returns {id, sub}" "true"
|
||||||
|
check 12 "split id/ returns {id, <<>>}" "true"
|
||||||
|
check 20 "GET /actors/<id> regression" "true"
|
||||||
|
check 21 "GET /actors/<id>/outbox stub" "true"
|
||||||
|
check 22 "GET /actors/<id>/inbox stub" "true"
|
||||||
|
check 23 "GET /actors/<id>/followers stub" "true"
|
||||||
|
check 24 "GET /actors/<id>/following stub" "true"
|
||||||
|
check 25 "POST /actors/<id>/inbox -> 202" "true"
|
||||||
|
check 26 "GET /actors/<id>/<bad> -> 404" "true"
|
||||||
|
check 27 "POST /actors/<id>/<bad> -> 404" "true"
|
||||||
|
check 28 "GET /actors/ (empty) -> 404" "true"
|
||||||
|
check 29 "outbox body carries actor id" "true"
|
||||||
|
check 30 "outbox JSON content negotiation" "true"
|
||||||
|
check 31 "inbox SX content negotiation" "true"
|
||||||
|
check 32 "followers JSON content negotiation" "true"
|
||||||
|
|
||||||
|
TOTAL=$((PASS+FAIL))
|
||||||
|
if [ $FAIL -eq 0 ]; then
|
||||||
|
echo "ok $PASS/$TOTAL next/tests/http_multi_actor.sh passed"
|
||||||
|
else
|
||||||
|
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||||
|
echo "$ERRORS"
|
||||||
|
fi
|
||||||
|
[ $FAIL -eq 0 ]
|
||||||
@@ -260,21 +260,35 @@ token; the token now maps to an `:actor_id` rather than a fixed `alice`.
|
|||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
|
|
||||||
- New route prefixes: `/actors/<id>/inbox`, `/actors/<id>/followers`,
|
- [x] **4a** — Per-actor URL routing. New `split_first_slash/1`
|
||||||
`/actors/<id>/following`.
|
helper splits the `/actors/` suffix into `{Id, SubPath}`.
|
||||||
- `http_server:route/3` (Cfg → Cfg+Kernel) so handlers can look up
|
GET dispatch routes `outbox` / `inbox` / `followers` / `following`
|
||||||
actor state.
|
sub-paths to four new content-negotiated response functions
|
||||||
- Cfg's `:publish_token` becomes `:tokens => #{Token => ActorId}` map.
|
(`actor_outbox_response_for/2`, `actor_inbox_get_response_for/2`,
|
||||||
- `cid_response_for/2` already format-aware; per-actor outbox listing
|
`actor_followers_response_for/2`,
|
||||||
uses the same machinery.
|
`actor_following_response_for/2`) — text / json / activity_json /
|
||||||
|
sx variants per existing format pattern. POST dispatch routes
|
||||||
**Tests:**
|
`inbox` to a 202 Accepted stub (`actor_inbox_post_response/0` +
|
||||||
|
`accepted_response/1`). Unknown sub-paths under `/actors/<id>/`
|
||||||
- GET /actors/alice → 200 with actor doc.
|
return 404. Bare `/actors/<id>` keeps the M1 `actor_doc_response_for`
|
||||||
- GET /actors/unknown → 404.
|
arm. 17 cases in `http_multi_actor.sh`.
|
||||||
- POST /activity with alice's token publishes to alice.
|
- [ ] **4b** — Token → ActorId map. Cfg's `:publish_token` becomes
|
||||||
- POST /activity with bob's token publishes to bob.
|
`:tokens` (proplist `[{Token, ActorId}, ...]`). POST /activity
|
||||||
- Two actors' outboxes are independent.
|
resolves the bearer token to an actor id and routes through
|
||||||
|
`nx_kernel:publish_to/2` instead of `publish/1`. Multi-token
|
||||||
|
Cfg keeps the M1 `:publish_token` single-token field as a
|
||||||
|
back-compat alias for one token mapping to `alice`.
|
||||||
|
- [ ] **4c** — `http_server:route/3(Req, Cfg, Kernel)` — the Cfg
|
||||||
|
carries opaque `:kernel` reference (or accepts the registered
|
||||||
|
`nx_kernel` atom) so per-actor handlers can call
|
||||||
|
`nx_kernel:state_for/1`, `actor_log_state/2`, projections etc.
|
||||||
|
- [ ] **4d** — Per-actor outbox listing reads from the named
|
||||||
|
bucket's log entries via `nx_kernel:actor_log_state/2`, content-
|
||||||
|
negotiates as today (text / json / sx). `?page=N` pagination
|
||||||
|
layered on top using `log:replay/3`.
|
||||||
|
- [ ] **4e** — POST /actors/<id>/inbox stays a 202 stub for 4a-4d.
|
||||||
|
Step 5 lands the real ingestion pipeline (sig verify + inbox-
|
||||||
|
bucket append + projection broadcast).
|
||||||
|
|
||||||
**Acceptance:** `bash next/tests/http_multi_actor.sh` passes 14+ cases.
|
**Acceptance:** `bash next/tests/http_multi_actor.sh` passes 14+ cases.
|
||||||
|
|
||||||
@@ -717,6 +731,22 @@ proceed.
|
|||||||
|
|
||||||
Newest first.
|
Newest first.
|
||||||
|
|
||||||
|
- **2026-06-06** — Step 4a: per-actor HTTP sub-paths. New
|
||||||
|
`split_first_slash/1` helper lets GET / POST `/actors/<id>/...`
|
||||||
|
paths route on the sub-segment (`outbox`, `inbox`, `followers`,
|
||||||
|
`following`). Four new content-negotiated response stubs
|
||||||
|
(`actor_outbox_response_for/2`, `actor_inbox_get_response_for/2`,
|
||||||
|
`actor_followers_response_for/2`, `actor_following_response_for/2`)
|
||||||
|
with text / json / activity_json / sx variants, mirroring the
|
||||||
|
existing `actor_doc_response_for/2` shape. POST
|
||||||
|
`/actors/<id>/inbox` returns a 202 Accepted stub
|
||||||
|
(`actor_inbox_post_response/0` + `accepted_response/1`); real
|
||||||
|
ingestion pipeline lands in Step 5. Unknown sub-paths return
|
||||||
|
404. Bare `/actors/<id>` keeps the M1 actor-doc arm intact —
|
||||||
|
`http_route` and `http_post_format` regression suites unchanged
|
||||||
|
(10/10 each). 17/17 in `http_multi_actor.sh`. Conformance
|
||||||
|
761/761 preserved. 120/120 across 10 Step-4-adjacent suites.
|
||||||
|
|
||||||
- **2026-06-06** — Step 3 (closes Step 3): key rotation via Update.
|
- **2026-06-06** — Step 3 (closes Step 3): key rotation via Update.
|
||||||
`actor_state.erl` `fold_update` routes patches through
|
`actor_state.erl` `fold_update` routes patches through
|
||||||
`apply_patch/3` which special-cases `{add_publicKey, KeyProplist}`
|
`apply_patch/3` which special-cases `{add_publicKey, KeyProplist}`
|
||||||
|
|||||||
Reference in New Issue
Block a user