fed-sx-types Phase 3: /types/<cid> route + discovery_type_fetch
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
Wire format for serving + fetching type docs (plans/fed-sx-host-types.md
step 3).
http_server.erl:
- new type_doc Accept format + content type
(application/vnd.fed-sx.type-doc), distinct from actor-doc.
- GET /types/<cid> -> the cached TypeRecord term_codec-encoded, 404 if
not in the peer_types cache. Reads peer_types via a Cfg
{peer_types, peer_types} guard (hardcoded registered atom, mirroring
the actor-doc route's kernel guard).
discovery_type_fetch.erl — sibling of discovery_fetch. make_fetch_fn
produces the fun/2 peer_types:lookup_or_fetch calls: GET
<base>/types/<cid> with the type-doc Accept header, returning the RAW
bytes (peer_types owns the term_codec decode, so the wire format lives
in one place — the route encodes, the cache decodes). Cfg carries
type_url / type_url_fn for TypeCid -> base URL resolution.
Tests: next/tests/peer_types_route.sh (13, in-process route dispatch),
next/tests/discovery_type_fetch.sh (9, closure vs a python type-doc
stub, end-to-end through peer_types:lookup_or_fetch).
No regression: http_accept, http_actors, http_get_format,
discovery_fetch all still green. Conformance 771/771.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
118
next/kernel/discovery_type_fetch.erl
Normal file
118
next/kernel/discovery_type_fetch.erl
Normal file
@@ -0,0 +1,118 @@
|
||||
-module(discovery_type_fetch).
|
||||
-export([make_fetch_fn/0, make_fetch_fn/1,
|
||||
fetch/2,
|
||||
type_doc_url/2,
|
||||
resolve_type_url/2,
|
||||
accept_header/0]).
|
||||
|
||||
%% Live type-doc fetch for peer_types — host-type federation Step 3,
|
||||
%% the sibling of discovery_fetch.erl. peer_types:lookup_or_fetch/3
|
||||
%% calls a Cfg-supplied type_fetch_fn :: fun ((TypeCid, Cfg) -> {ok,
|
||||
%% Bytes} | {error, _}) on a cache miss; this module produces that
|
||||
%% closure for live federation. It GETs <base>/types/<cid> with an
|
||||
%% Accept header that asks for the type-doc format (http_server.erl
|
||||
%% Step 3) and returns the RAW response bytes — peer_types decodes
|
||||
%% them via term_codec into the TypeRecord. (This is the one shape
|
||||
%% difference from discovery_fetch, whose closure returns an already-
|
||||
%% decoded actor-state: there the cache stores the decoded AS, here
|
||||
%% peer_types owns the decode so the type-doc wire format lives in one
|
||||
%% place — the /types/ route encodes, peer_types decodes.)
|
||||
%%
|
||||
%% Cfg shape (parallels discovery_fetch's peer URL resolution):
|
||||
%% {type_url, [{TypeCid, BaseUrl}, ...]}
|
||||
%% {type_url_fn, fun ((TypeCid) -> {ok, BaseUrl} | not_found)}
|
||||
%%
|
||||
%% BaseUrl shape: <<"http://host:port">> (no trailing slash; this
|
||||
%% module appends the path). TypeCid is the type's CID bytes.
|
||||
%%
|
||||
%% Outcomes:
|
||||
%% 2xx -> {ok, Bytes}
|
||||
%% non-2xx -> {error, {status, N}}
|
||||
%% resolver miss -> {error, no_type_url}
|
||||
%% transport -> {error, Reason}
|
||||
|
||||
%% ── Accept header ────────────────────────────────────────────
|
||||
%% "application/vnd.fed-sx.type-doc" — same MIME http_server's
|
||||
%% content_type_for(type_doc) emits, so the Accept negotiation routes
|
||||
%% the served bytes to the term_codec-encoded TypeRecord arm.
|
||||
accept_header() ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
116,121,112,101,45,100,111,99>>.
|
||||
|
||||
%% ── public API ───────────────────────────────────────────────
|
||||
|
||||
%% make_fetch_fn/0 — the fun/2 peer_types:lookup_or_fetch calls. It
|
||||
%% reads the type-URL resolver out of the Cfg passed at call time, so
|
||||
%% the same Cfg threads through peer_types and this closure.
|
||||
make_fetch_fn() ->
|
||||
fun (TypeCid, Cfg) ->
|
||||
case resolve_type_url(TypeCid, Cfg) of
|
||||
{error, R} -> {error, R};
|
||||
{ok, BaseUrl} -> fetch(type_doc_url(BaseUrl, TypeCid), Cfg)
|
||||
end
|
||||
end.
|
||||
|
||||
%% make_fetch_fn/1 — variant that closes over a static Cfg for the
|
||||
%% resolver while still honouring the call-time Cfg for transport.
|
||||
%% Lets a caller bake the type_url map once and reuse the closure.
|
||||
make_fetch_fn(StaticCfg) ->
|
||||
fun (TypeCid, Cfg) ->
|
||||
case resolve_type_url(TypeCid, StaticCfg) of
|
||||
{error, R} -> {error, R};
|
||||
{ok, BaseUrl} -> fetch(type_doc_url(BaseUrl, TypeCid), Cfg)
|
||||
end
|
||||
end.
|
||||
|
||||
fetch(Url, _Cfg) ->
|
||||
AcceptKey = <<97,99,99,101,112,116>>, % "accept"
|
||||
Headers = [{AcceptKey, accept_header()}],
|
||||
try httpc:request(Url, get, Headers, <<>>) of
|
||||
{ok, Status, _H, Body} when Status >= 200, Status < 300 ->
|
||||
{ok, Body};
|
||||
{ok, Status, _H, _B} ->
|
||||
{error, {status, Status}};
|
||||
Other ->
|
||||
{error, {bad_response, Other}}
|
||||
catch
|
||||
error:Reason -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% type_doc_url/2 — <BaseUrl>/types/<cid>. TypeCid is the cid bytes,
|
||||
%% appended verbatim as the path segment (matches the "/types/" prefix
|
||||
%% http_server.erl registers).
|
||||
type_doc_url(BaseUrl, TypeCid) when is_binary(TypeCid) ->
|
||||
%% "/types/" — 7 bytes
|
||||
Prefix = <<47,116,121,112,101,115,47>>,
|
||||
<<BaseUrl/binary, Prefix/binary, TypeCid/binary>>.
|
||||
|
||||
%% resolve_type_url/2 — map a TypeCid to its serving node's base URL.
|
||||
%% type_url_fn (a 1-arity closure) takes precedence over the static
|
||||
%% type_url proplist; absent both -> {error, no_type_url}.
|
||||
resolve_type_url(TypeCid, Cfg) ->
|
||||
case field(type_url_fn, Cfg) of
|
||||
Fn when is_function(Fn, 1) ->
|
||||
case Fn(TypeCid) of
|
||||
{ok, BaseUrl} -> {ok, BaseUrl};
|
||||
_ -> {error, no_type_url}
|
||||
end;
|
||||
_ ->
|
||||
case field(type_url, Cfg) of
|
||||
nil -> {error, no_type_url};
|
||||
Map ->
|
||||
case find_keyed(TypeCid, Map) of
|
||||
{ok, BaseUrl} -> {ok, BaseUrl};
|
||||
_ -> {error, no_type_url}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% ── helpers ──────────────────────────────────────────────────
|
||||
|
||||
field(K, [{K, V} | _]) -> V;
|
||||
field(K, [_ | Rest]) -> field(K, Rest);
|
||||
field(_, []) -> nil.
|
||||
|
||||
find_keyed(_, []) -> {error, not_found};
|
||||
find_keyed(K, [{K, V} | _]) -> {ok, V};
|
||||
find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
||||
@@ -4,6 +4,7 @@
|
||||
welcome_body/0, capabilities_body/0,
|
||||
capabilities_path/0,
|
||||
match_prefix/2, actors_prefix/0, actor_doc_response/1,
|
||||
types_prefix/0, type_doc_response_for/2,
|
||||
artifacts_prefix/0, artifact_response/1,
|
||||
projections_list_path/0, projections_prefix/0,
|
||||
projections_list_response/0, projection_response/1,
|
||||
@@ -156,7 +157,12 @@ dispatch(<<71, 69, 84>>, Path, F, Cfg) ->
|
||||
{ok, Name} when byte_size(Name) > 0 ->
|
||||
projection_response_for(Name, F);
|
||||
_ ->
|
||||
not_found_response()
|
||||
case match_prefix(types_prefix(), Path) of
|
||||
{ok, Cid} when byte_size(Cid) > 0 ->
|
||||
type_doc_response_for(Cid, Cfg);
|
||||
_ ->
|
||||
not_found_response()
|
||||
end
|
||||
end
|
||||
end
|
||||
end;
|
||||
@@ -289,6 +295,10 @@ artifact_response(Cid) ->
|
||||
Body = <<Pre/binary, Cid/binary, 10>>,
|
||||
ok_response(Body).
|
||||
|
||||
%% "/types/" — 7 bytes: 47 116 121 112 101 115 47 (host-type fed Step 3)
|
||||
types_prefix() ->
|
||||
<<47,116,121,112,101,115,47>>.
|
||||
|
||||
%% "/projections" — 12 bytes (no trailing slash; the list endpoint)
|
||||
projections_list_path() ->
|
||||
<<47,112,114,111,106,101,99,116,105,111,110,115>>.
|
||||
@@ -488,9 +498,20 @@ actor_doc_prefix() ->
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
97,99,116,111,114,45,100,111,99>>.
|
||||
|
||||
%% "application/vnd.fed-sx.type-doc" — 31 bytes (host-type fed Step 3).
|
||||
%% Distinct from actor-doc: the body is a term_codec-encoded
|
||||
%% TypeRecord (peer_types cache entry), not a peer-actor-state.
|
||||
type_doc_prefix() ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
116,121,112,101,45,100,111,99>>.
|
||||
|
||||
accept_format(nil) -> text;
|
||||
accept_format(<<>>) -> text;
|
||||
accept_format(V) when is_binary(V) ->
|
||||
case match_prefix(type_doc_prefix(), V) of
|
||||
{ok, _} -> type_doc;
|
||||
_ ->
|
||||
case match_prefix(actor_doc_prefix(), V) of
|
||||
{ok, _} -> actor_doc;
|
||||
_ ->
|
||||
@@ -510,6 +531,7 @@ accept_format(V) when is_binary(V) ->
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end;
|
||||
accept_format(_) -> text.
|
||||
|
||||
@@ -586,6 +608,11 @@ content_type_for(actor_doc) ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
97,99,116,111,114,45,100,111,99>>;
|
||||
%% "application/vnd.fed-sx.type-doc" — 31 bytes (host-type fed Step 3).
|
||||
content_type_for(type_doc) ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
116,121,112,101,45,100,111,99>>;
|
||||
content_type_for(_) ->
|
||||
content_type_for(text).
|
||||
|
||||
@@ -714,6 +741,42 @@ kernel_actor_state(_Kernel, Id) ->
|
||||
_ -> nil
|
||||
end.
|
||||
|
||||
%% ── host-type fed Step 3: GET /types/<cid> ──────────────────────
|
||||
%%
|
||||
%% Serves a TypeRecord the node has cached (its own published types or
|
||||
%% types fetched from peers) so a federated peer running
|
||||
%% discovery_type_fetch can decode it directly into the shape
|
||||
%% peer_types + the object-schema pipeline stage consume. The wire
|
||||
%% body is term_codec:encode(TypeRecord) under the
|
||||
%% application/vnd.fed-sx.type-doc content type; a cache miss is a 404.
|
||||
%%
|
||||
%% Cid is the path segment after "/types/" (the type's CID bytes). Cfg
|
||||
%% carries `{peer_types, peer_types}` to opt the route into the cache —
|
||||
%% absent (or the gen_server down) short-circuits to 404, matching the
|
||||
%% kernel_actor_state guard for the actor-doc route. This port can't
|
||||
%% dispatch `Mod:Fun` on a variable module, so the registered
|
||||
%% `peer_types` atom is hardcoded; the Cfg field flags "no cache wired".
|
||||
|
||||
type_doc_response_for(Cid, Cfg) ->
|
||||
case type_record_for(Cfg, Cid) of
|
||||
nil -> not_found_response();
|
||||
TR -> ok_response(term_codec:encode(TR), type_doc)
|
||||
end.
|
||||
|
||||
type_record_for(Cfg, Cid) ->
|
||||
case field(peer_types, Cfg) of
|
||||
nil -> nil;
|
||||
_ ->
|
||||
case erlang:whereis(peer_types) of
|
||||
undefined -> nil;
|
||||
_ ->
|
||||
case peer_types:lookup(Cid) of
|
||||
{ok, TR} -> TR;
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% ── 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
|
||||
|
||||
176
next/tests/discovery_type_fetch.sh
Executable file
176
next/tests/discovery_type_fetch.sh
Executable file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
# next/tests/discovery_type_fetch.sh — host-type federation Phase 3.
|
||||
#
|
||||
# Client side of the type-doc wire: discovery_type_fetch builds the
|
||||
# fun/2 closure peer_types:lookup_or_fetch calls on a cache miss. It
|
||||
# GETs <base>/types/<cid> with the type-doc Accept header and returns
|
||||
# the RAW response bytes (peer_types decodes them via term_codec).
|
||||
# Exercised end-to-end against a background python http server that
|
||||
# serves hand-crafted term_codec bytes, so we test the wire — not just
|
||||
# an in-process call.
|
||||
|
||||
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=""
|
||||
|
||||
# ── live stub server ─────────────────────────────────────────
|
||||
# GET /types/bafy1 -> 200 with term_codec-encoded TypeRecord
|
||||
# TR = [{name, <<"Post">>}, {instance_type, <<"Note">>}]
|
||||
# GET anything else -> 404
|
||||
PORT=$(python3 -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1]);s.close()')
|
||||
SRVROOT=$(mktemp -d)
|
||||
PYSRV="$SRVROOT/srv.py"
|
||||
cat > "$PYSRV" <<'PY'
|
||||
import sys, http.server, socketserver
|
||||
|
||||
PORT = int(sys.argv[1])
|
||||
|
||||
# term_codec encoding (mirror of next/kernel/term_codec.erl).
|
||||
def enc_atom(s):
|
||||
b = s.encode()
|
||||
return f"a{len(b)}:".encode() + b
|
||||
def enc_bin(b):
|
||||
return f"b{len(b)}:".encode() + b
|
||||
def enc_tuple(items):
|
||||
return f"t{len(items)}:".encode() + b"".join(items)
|
||||
def enc_list(items):
|
||||
return f"l{len(items)}:".encode() + b"".join(items)
|
||||
|
||||
# [{name, <<"Post">>}, {instance_type, <<"Note">>}]
|
||||
TYPEDOC = enc_list([
|
||||
enc_tuple([enc_atom("name"), enc_bin(b"Post")]),
|
||||
enc_tuple([enc_atom("instance_type"), enc_bin(b"Note")]),
|
||||
])
|
||||
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/types/bafy1":
|
||||
self.send_response(200)
|
||||
self.send_header('content-type','application/vnd.fed-sx.type-doc')
|
||||
self.send_header('content-length', str(len(TYPEDOC)))
|
||||
self.end_headers()
|
||||
self.wfile.write(TYPEDOC)
|
||||
else:
|
||||
self.send_response(404); self.end_headers(); self.wfile.write(b'not found')
|
||||
def log_message(self, fmt, *args): pass
|
||||
|
||||
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
|
||||
srv.serve_forever()
|
||||
PY
|
||||
python3 "$PYSRV" "$PORT" >/dev/null 2>&1 &
|
||||
SRV_PID=$!
|
||||
TMPFILE=$(mktemp)
|
||||
trap "rm -rf $SRVROOT $TMPFILE; kill $SRV_PID 2>/dev/null || true" EXIT
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||||
if curl -fsS "http://127.0.0.1:$PORT/types/bafy1" >/dev/null 2>&1; then break; fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
bytes_of() { python3 -c "import sys; print(','.join(str(b) for b in sys.argv[1].encode()))" "$1"; }
|
||||
URL_BASE_BYTES=$(bytes_of "http://127.0.0.1:$PORT")
|
||||
|
||||
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/term_codec.erl\")) :name)")
|
||||
(epoch 4)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/peer_types.erl\")) :name)")
|
||||
(epoch 5)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/discovery_type_fetch.erl\")) :name)")
|
||||
|
||||
;; accept_header is the 31-byte type-doc MIME
|
||||
(epoch 10)
|
||||
(eval "(get (erlang-eval-ast \"byte_size(discovery_type_fetch:accept_header()) =:= 31\") :name)")
|
||||
|
||||
;; type_doc_url builds <base>/types/bafy1
|
||||
(epoch 11)
|
||||
(eval "(get (erlang-eval-ast \"U = discovery_type_fetch:type_doc_url(<<__URL_BASE__>>, <<98,97,102,121,49>>), U =:= <<__URL_BASE__,47,116,121,112,101,115,47,98,97,102,121,49>>\") :name)")
|
||||
|
||||
;; resolve_type_url via the static type_url proplist
|
||||
(epoch 12)
|
||||
(eval "(get (erlang-eval-ast \"discovery_type_fetch:resolve_type_url(<<98,97,102,121,49>>, [{type_url, [{<<98,97,102,121,49>>, <<__URL_BASE__>>}]}]) =:= {ok, <<__URL_BASE__>>}\") :name)")
|
||||
|
||||
;; fetch live -> {ok, Bytes} that decode to the TypeRecord
|
||||
(epoch 13)
|
||||
(eval "(get (erlang-eval-ast \"R = discovery_type_fetch:fetch(<<__URL_BASE__,47,116,121,112,101,115,47,98,97,102,121,49>>, []), case R of {ok, B} -> {ok, TR, _} = term_codec:decode(B), TR =:= [{name, <<80,111,115,116>>}, {instance_type, <<78,111,116,101>>}]; _ -> false end\") :name)")
|
||||
|
||||
;; closure from make_fetch_fn/0 dispatches and returns raw bytes
|
||||
(epoch 14)
|
||||
(eval "(get (erlang-eval-ast \"Fn = discovery_type_fetch:make_fetch_fn(), Cfg = [{type_url, [{<<98,97,102,121,49>>, <<__URL_BASE__>>}]}], case Fn(<<98,97,102,121,49>>, Cfg) of {ok, B} -> {ok, TR, _} = term_codec:decode(B), TR =:= [{name, <<80,111,115,116>>}, {instance_type, <<78,111,116,101>>}]; _ -> false end\") :name)")
|
||||
|
||||
;; closure with no resolver -> {error, no_type_url}
|
||||
(epoch 15)
|
||||
(eval "(get (erlang-eval-ast \"Fn = discovery_type_fetch:make_fetch_fn(), case Fn(<<98,97,102,121,49>>, []) of {error, no_type_url} -> true; _ -> false end\") :name)")
|
||||
|
||||
;; fetch on an unknown cid path -> {error, {status, 404}}
|
||||
(epoch 16)
|
||||
(eval "(get (erlang-eval-ast \"R = discovery_type_fetch:fetch(<<__URL_BASE__,47,116,121,112,101,115,47,122,122,122>>, []), case R of {error, {status, 404}} -> true; _ -> false end\") :name)")
|
||||
|
||||
;; end-to-end: peer_types:lookup_or_fetch uses the closure, decodes,
|
||||
;; and writes the TypeRecord into the cache
|
||||
(epoch 17)
|
||||
(eval "(get (erlang-eval-ast \"Fn = discovery_type_fetch:make_fetch_fn(), Cfg = [{type_fetch_fn, Fn}, {type_url, [{<<98,97,102,121,49>>, <<__URL_BASE__>>}]}], case peer_types:lookup_or_fetch(<<98,97,102,121,49>>, Cfg, peer_types:new()) of {ok, TR, S} -> TR =:= [{name, <<80,111,115,116>>}, {instance_type, <<78,111,116,101>>}] andalso peer_types:types(S) =:= [<<98,97,102,121,49>>]; _ -> false end\") :name)")
|
||||
EPOCHS
|
||||
|
||||
sed -i "s|__URL_BASE__|${URL_BASE_BYTES}|g" "$TMPFILE"
|
||||
|
||||
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 5 "discovery_type_fetch loaded" "discovery_type_fetch"
|
||||
check 10 "accept_header is 31-byte type-doc" "true"
|
||||
check 11 "type_doc_url builds /types/<cid>" "true"
|
||||
check 12 "resolve_type_url via type_url map" "true"
|
||||
check 13 "fetch live -> raw bytes decode to TR" "true"
|
||||
check 14 "closure -> raw bytes decode to TR" "true"
|
||||
check 15 "closure no resolver -> no_type_url" "true"
|
||||
check 16 "fetch 404 path -> {status, 404}" "true"
|
||||
check 17 "lookup_or_fetch caches fetched TR" "true"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL next/tests/discovery_type_fetch.sh passed"
|
||||
else
|
||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||
echo "$ERRORS"
|
||||
fi
|
||||
[ $FAIL -eq 0 ]
|
||||
140
next/tests/peer_types_route.sh
Executable file
140
next/tests/peer_types_route.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# next/tests/peer_types_route.sh — host-type federation Phase 3.
|
||||
#
|
||||
# Server side of the type-doc wire: http_server serves
|
||||
# GET /types/<cid> Accept: application/vnd.fed-sx.type-doc
|
||||
# as the term_codec-encoded TypeRecord pulled from the peer_types
|
||||
# cache; 404 if the cid isn't cached. Exercised via http_server:route
|
||||
# in-process (the established pattern — see http_actors.sh) so the
|
||||
# route resolution + content negotiation are tested without a live
|
||||
# socket. The peer_types gen_server holds the cache across epochs.
|
||||
|
||||
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
|
||||
|
||||
# TR is the served TypeRecord, Cid its key. AccV is the type-doc
|
||||
# Accept header value, CT the content-type key. Cfg opts the route
|
||||
# into the peer_types cache. ReqHit / ReqMiss / ReqEmpty / ReqPost
|
||||
# vary the request line.
|
||||
SETUP='TR = [{name, <<80,111,115,116>>}, {instance_type, <<78,111,116,101>>}], Cid = <<98,97,102,121,49>>, peer_types:start_link(), peer_types:put(Cid, TR), AcK = <<97,99,99,101,112,116>>, AcV = <<97,112,112,108,105,99,97,116,105,111,110,47,118,110,100,46,102,101,100,45,115,120,46,116,121,112,101,45,100,111,99>>, Hs = [{AcK, AcV}], Cfg = [{peer_types, peer_types}],'
|
||||
|
||||
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/term_codec.erl\")) :name)")
|
||||
(epoch 5)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/peer_types.erl\")) :name)")
|
||||
(epoch 6)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
|
||||
|
||||
;; ── negotiation + prefix primitives ────────────────────────
|
||||
;; Accept: type-doc negotiates to the type_doc format atom
|
||||
(epoch 10)
|
||||
(eval "(get (erlang-eval-ast \"http_server:accept_format(<<97,112,112,108,105,99,97,116,105,111,110,47,118,110,100,46,102,101,100,45,115,120,46,116,121,112,101,45,100,111,99>>) =:= type_doc\") :name)")
|
||||
;; type_doc content type is 31 bytes
|
||||
(epoch 11)
|
||||
(eval "(get (erlang-eval-ast \"byte_size(http_server:content_type_for(type_doc)) =:= 31\") :name)")
|
||||
;; types_prefix is "/types/" — 7 bytes
|
||||
(epoch 12)
|
||||
(eval "(get (erlang-eval-ast \"byte_size(http_server:types_prefix()) =:= 7\") :name)")
|
||||
|
||||
;; ── GET /types/<cid> ───────────────────────────────────────
|
||||
;; cache hit -> 200
|
||||
(epoch 20)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47,116,121,112,101,115,47,98,97,102,121,49>>}, {headers, Hs}], R = http_server:route(Req, Cfg), {ok, S} = envelope:get_field(status, R), S =:= 200\") :name)")
|
||||
;; body decodes back to the stored TypeRecord
|
||||
(epoch 21)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47,116,121,112,101,115,47,98,97,102,121,49>>}, {headers, Hs}], R = http_server:route(Req, Cfg), {ok, B} = envelope:get_field(body, R), {ok, DTR, _} = term_codec:decode(B), DTR =:= TR\") :name)")
|
||||
;; response carries the type-doc content type
|
||||
(epoch 22)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47,116,121,112,101,115,47,98,97,102,121,49>>}, {headers, Hs}], R = http_server:route(Req, Cfg), {ok, Hdrs} = envelope:get_field(headers, R), {_CTK, CTV} = hd(Hdrs), CTV =:= http_server:content_type_for(type_doc)\") :name)")
|
||||
;; type_doc_response_for/2 direct: known cid -> 200
|
||||
(epoch 23)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} R = http_server:type_doc_response_for(Cid, Cfg), {ok, S} = envelope:get_field(status, R), S =:= 200\") :name)")
|
||||
|
||||
;; ── misses + wrong method ──────────────────────────────────
|
||||
;; unknown cid -> 404
|
||||
(epoch 30)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47,116,121,112,101,115,47,122,122,122>>}, {headers, Hs}], R = http_server:route(Req, Cfg), {ok, S} = envelope:get_field(status, R), S =:= 404\") :name)")
|
||||
;; empty cid (GET /types/) -> 404
|
||||
(epoch 31)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47,116,121,112,101,115,47>>}, {headers, Hs}], R = http_server:route(Req, Cfg), {ok, S} = envelope:get_field(status, R), S =:= 404\") :name)")
|
||||
;; no peer_types cfg -> 404 even for a known cid
|
||||
(epoch 32)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47,116,121,112,101,115,47,98,97,102,121,49>>}, {headers, Hs}], R = http_server:route(Req, []), {ok, S} = envelope:get_field(status, R), S =:= 404\") :name)")
|
||||
;; POST /types/<cid> -> 404 (only GET serves type docs)
|
||||
(epoch 33)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<80,79,83,84>>}, {path, <<47,116,121,112,101,115,47,98,97,102,121,49>>}, {headers, Hs}], R = http_server:route(Req, Cfg), {ok, S} = envelope:get_field(status, R), S =:= 404\") :name)")
|
||||
;; existing routes intact: GET / still 200
|
||||
(epoch 34)
|
||||
(eval "(get (erlang-eval-ast \"${SETUP} Req = [{method, <<71,69,84>>}, {path, <<47>>}], R = http_server:route(Req, Cfg), {ok, S} = envelope:get_field(status, R), S =:= 200\") :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 6 "http_server module loaded" "http_server"
|
||||
check 10 "Accept type-doc -> type_doc" "true"
|
||||
check 11 "type_doc content type = 31 bytes" "true"
|
||||
check 12 "types_prefix = 7 bytes" "true"
|
||||
check 20 "GET /types/<cid> hit -> 200" "true"
|
||||
check 21 "body decodes to TypeRecord" "true"
|
||||
check 22 "response is type-doc content type" "true"
|
||||
check 23 "type_doc_response_for hit -> 200" "true"
|
||||
check 30 "unknown cid -> 404" "true"
|
||||
check 31 "empty cid -> 404" "true"
|
||||
check 32 "no peer_types cfg -> 404" "true"
|
||||
check 33 "POST /types/<cid> -> 404" "true"
|
||||
check 34 "existing GET / route intact" "true"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL next/tests/peer_types_route.sh passed"
|
||||
else
|
||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||
echo "$ERRORS"
|
||||
fi
|
||||
[ $FAIL -eq 0 ]
|
||||
Reference in New Issue
Block a user