Skip to content

Commit 8a08e40

Browse files
committed
test: two more boot cases, and the printed reason is now readable
Case 5: TRINITY_PROFILE=regulated with TRINITY_MCP_AUTH_PROFILE set to personal and then to local, everything else as case 4. Neither starts, and the printed reason names regulated_requires_production_mcp_auth and the profile it refused. The variable goes through config/runtime.exs rather than Application.put_env, because that is the path an operator uses and the one that rejects a value outside the three. Case 6: case 4's configuration with case 1's broken key store. Does not start, and the printed reason is {:regulated_boot_refused, {:regulated_requires_receipts, {:key_file, :eisdir}}}. Case 1 keeps passing, so the same key store still does not stop a default node, which is the whole difference AC4 is about. This is also the first case to reach the error branch of the receipts check at an actual boot: cases 1 to 4 never did, so whether the reason survived Supervisor.stop/2 inside start/2 was untested until now. It does. Found while writing case 5: the report tail inspected the reason with limit: 3, which printed BOOT_REFUSED {:trinity, {{...}, ...}} and hid it completely. The earlier cases asserted the reason appeared somewhere in the captured output, and matched the crash report OTP writes to stderr rather than the line the node printed. Now inspected with limit: :infinity, and cases 5 and 6 assert against the BOOT_REFUSED line alone via refusal_line/1. Planted breaks, each reverted, lib/ left byte-identical: skipping the receipts gate entirely (verify_regulated_receipts) turned case 6 red and left case 1 green, which is the pair the profile exists to create. making AC1's check_mcp_auth/2 always pass turned case 5 red, and showed that :personal is still caught by AC2's check_embedded_as/2 with a different reason (regulated_refuses_personal_issuer). Case 5 catches that only because it asserts the specific reason rather than that a refusal happened. Signed-off-by: Ayla Croft <aylacroft@proton.me>
1 parent e0e96ed commit 8a08e40

1 file changed

Lines changed: 115 additions & 2 deletions

File tree

‎test/trinity/regulated_boot_node_test.exs‎

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,30 @@ defmodule Trinity.RegulatedBootNodeTest do
6464
end
6565

6666
# The tail of every script: start the application and say what happened.
67+
#
68+
# `limit: :infinity` is not decoration. This was `limit: 3`, which printed
69+
# `BOOT_REFUSED {:trinity, {{...}, ...}}` and hid the reason entirely, so an assertion that the
70+
# reason was named passed only because the same words appear in the crash report OTP writes to
71+
# stderr. A test for "it refused, and said why" has to read what the node said.
6772
@report """
6873
case Application.ensure_all_started(:trinity) do
69-
{:ok, _} -> IO.puts("BOOT_OK"); System.halt(0)
70-
{:error, reason} -> IO.puts("BOOT_REFUSED " <> inspect(reason, limit: 3)); System.halt(3)
74+
{:ok, _} ->
75+
IO.puts("BOOT_OK")
76+
System.halt(0)
77+
78+
{:error, reason} ->
79+
IO.puts("BOOT_REFUSED " <> inspect(reason, limit: :infinity, printable_limit: :infinity))
80+
System.halt(3)
7181
end
7282
"""
7383

84+
# The line the child printed, and nothing else: not the crash report, not a log line.
85+
defp refusal_line(out) do
86+
out
87+
|> String.split("\n")
88+
|> Enum.find("", &String.starts_with?(&1, "BOOT_REFUSED"))
89+
end
90+
7491
# A module that implements Trinity.Authority and is not Local. Defined in the child rather than
7592
# added to lib/, because AC5 says no adapter is supplied by this tree and none is invented.
7693
@fixture """
@@ -215,4 +232,100 @@ defmodule Trinity.RegulatedBootNodeTest do
215232
assert out =~ "BOOT_OK"
216233
end
217234
end
235+
236+
describe "5. regulated refuses an MCP authorization profile that is not production" do
237+
test "personal and local are both refused, where case 4's configuration is otherwise met" do
238+
for profile <- ["personal", "local"] do
239+
root = tmp!("regulated-mcp-#{profile}")
240+
241+
# No `Application.put_env` for :mcp_auth here, deliberately. The variable goes through
242+
# `config/runtime.exs`, which is the path an operator actually uses, and which raises on a
243+
# value that is not one of the three. MIX_ENV=dev, so that block runs (it is skipped in
244+
# :test, where the suite sets :mcp_auth itself).
245+
script = """
246+
#{@fixture}
247+
#{allowed_models()}
248+
#{@report}
249+
"""
250+
251+
{out, status} =
252+
boot(
253+
[
254+
{"XDG_DATA_HOME", root},
255+
{"TRINITY_PROFILE", "regulated"},
256+
{"TRINITY_MCP_AUTH_PROFILE", profile},
257+
{"TRINITY_AUTHORITY", "RegulatedBootFixture"},
258+
{"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"}
259+
],
260+
script
261+
)
262+
263+
assert status == 3,
264+
"""
265+
the node started on the #{profile} MCP authorization profile. Case 4 is the same \
266+
configuration with production, and it starts, so this is the only difference.
267+
268+
#{String.slice(out, -3000, 3000)}
269+
"""
270+
271+
assert out =~ "BOOT_REFUSED"
272+
273+
reason = refusal_line(out)
274+
275+
assert reason =~ "regulated_requires_production_mcp_auth",
276+
"it refused, but the printed reason is not the one under test:\n#{reason}"
277+
278+
# The reason names the profile that was refused, not just that one was.
279+
assert reason =~ ":#{profile}"
280+
end
281+
end
282+
end
283+
284+
describe "6. regulated refuses to keep running when receipts cannot be appended" do
285+
test "case 4's configuration with case 1's broken key store does not start" do
286+
root = tmp!("regulated-signer-down")
287+
288+
# Exactly case 4, plus the one break case 1 survives. Nothing else differs, so a refusal here
289+
# is attributable to the key store and to the profile, and to nothing else.
290+
script = """
291+
#{@fixture}
292+
#{production_mcp_auth()}
293+
#{allowed_models()}
294+
r = Application.get_env(:trinity, :receipts, [])
295+
Application.put_env(:trinity, :receipts, Keyword.put(r, :keys_dir, "#{broken_keys_dir(root)}"))
296+
#{@report}
297+
"""
298+
299+
{out, status} =
300+
boot(
301+
[
302+
{"XDG_DATA_HOME", root},
303+
{"TRINITY_PROFILE", "regulated"},
304+
{"TRINITY_AUTHORITY", "RegulatedBootFixture"},
305+
{"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"}
306+
],
307+
script
308+
)
309+
310+
assert status == 3,
311+
"""
312+
a regulated node started with the signer down. Case 1 proves the same key store does \
313+
not stop a :default node, and that difference is the whole point of AC4.
314+
315+
#{String.slice(out, -4000, 4000)}
316+
"""
317+
318+
assert out =~ "BOOT_REFUSED"
319+
320+
reason = refusal_line(out)
321+
322+
assert reason =~ "regulated_boot_refused",
323+
"it refused, but the printed reason is not the receipts check:\n#{reason}"
324+
325+
assert reason =~ "regulated_requires_receipts"
326+
327+
# The underlying signer failure is carried through rather than flattened into a bare atom.
328+
assert reason =~ "key_file"
329+
end
330+
end
218331
end

0 commit comments

Comments
 (0)