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

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:
2026-06-06 13:47:00 +00:00
parent 238a1fbea0
commit 0b8772ec69
3 changed files with 365 additions and 19 deletions

View File

@@ -16,7 +16,11 @@
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]).
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.
%%
@@ -94,11 +98,11 @@ dispatch(<<71, 69, 84>>,
%% 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}
%% GET /actors/{id}[/sub] 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);
{ok, Rest} when byte_size(Rest) > 0 ->
actor_get(Rest, F);
_ ->
case match_prefix(artifacts_prefix(), Path) of
{ok, Cid} when byte_size(Cid) > 0 ->
@@ -112,9 +116,71 @@ dispatch(<<71, 69, 84>>, Path, F) ->
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(_, _, _) ->
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.
%% f e d - s x _ k e r n e l _ m 1 \n
welcome_body() ->
@@ -538,6 +604,105 @@ actor_doc_response_for(Id, cbor) ->
actor_doc_response_for(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_for(Cid, text) ->