Files
bikoret/vav_pipeline/test/bigdei_kodesh_test.exs
T
Scott KarpandClaude Fable 5.1 7c40200dfc tablet 55: the row that was a label — the Sanhedrin's example request crashed its own gate
Verified live 2026-10-19: Tamid's own example for POST /chatzer/sanhedrin
(a case with `sides`) answered 500. The router passed the sides LIST as
the case context; halt_monitor:check_avodah_zarah/1 reads a map, maps:get
raised, and the halt monitor and escalation gen_servers died with the
caller's input (supervisor restarted them; health stayed 8/8). The router
then matched {:ok, decision} on the error and made a 500 of its own.

- sanhedrin/src/halt_monitor.erl: do_check_case refuses a non-map
  context as {halt, malformed_case, _} and stays standing. EUnit 49/0
  with the new halt_malformed_context_test.
- router.ex process_sanhedrin: sides validated at the kiyor (422 on a bad
  shape), carried as context %{sides: sides}; a Sanhedrin refusal answers
  422 SANHEDRIN_HALTED, a failure 503 SANHEDRIN_UNAVAILABLE. Two new
  router tests, one asserting the gate processes survive the example.
  Elixir 624/0.
- specs/broken_tablets/README.md: tablet 55, including what is NOT fixed
  — a panel of one accepting on zero references under a grounded-
  sounding sentence, the ignored complexity hint, no panel manna spent,
  no L3 record. The row in Tamid is still larger than the thing behind it.

Doorpost pin unchanged (no @spec_files touched; scripts/doorpost_check.py
passes).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 16:44:53 +00:00

436 lines
15 KiB
Elixir

defmodule VavPipeline.BigdeiKodeshTest do
@moduledoc """
Tests for the Bigdei Kodesh — the HTTP API layer.
Shemot 28:2 — V'asita vigdei kodesh l'Aharon achicha,
l'khavod ul'tifaret.
Four garments for four access tiers:
/peah/* — public, no auth
/courtyard/* — authenticated
/chatzer/* — full access
/kodesh/* — operator
"""
use ExUnit.Case, async: false
import Plug.Test
import Plug.Conn
alias VavPipeline.BigdeiKodesh.Router
@opts Router.init([])
# ============================================================
# Pe'ah — Public Endpoint (Machaneh Yisrael)
# ============================================================
describe "Pe'ah endpoint" do
test "GET /peah/ask without question returns usage" do
conn =
conn(:get, "/peah/ask")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert body["message"] =~ "Bikoret"
assert body["usage"] =~ "/peah/ask"
end
test "GET /peah/ask with question parameter processes it" do
conn =
conn(:get, "/peah/ask?question=What+is+bereishit")
|> Router.call(@opts)
# Should return 200 or 503 (if waiting for provision)
assert conn.status in [200, 503]
end
test "POST /peah/ask with valid question" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{question: "What is bereishit?"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status in [200, 503]
body = Jason.decode!(conn.resp_body)
if conn.status == 200 do
assert Map.has_key?(body, "answer") or Map.has_key?(body, "processing")
end
end
test "POST /peah/ask without question returns 422" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{not_question: "wrong field"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 422
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "KIYOR_VALIDATION_FAILED"
assert body["error"]["type"] == "purification_needed"
end
test "POST /peah/ask with empty question returns 422" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{question: ""}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 422
end
end
# ============================================================
# Pe'ah — Azazel disposal log (commit 8, public half of property 9)
# ============================================================
# Privacy boundary tests run WITHOUT a database (the empty L3 case
# exercises the response shape and the privacy guarantees). Tests
# that need actual disposal records to query are tagged :l3.
describe "GET /peah/azazel_log — public observability" do
# When L3 is up (BIKORET_L3=1) the endpoint queries the sandboxed
# Repo, so the test process must own a connection; without L3 the
# endpoint answers from an empty log and there is nothing to check out.
setup do
if Process.whereis(VavPipeline.Repo),
do: :ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
:ok
end
test "responds with 200 even when no disposals are recorded" do
conn =
conn(:get, "/peah/azazel_log")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert is_integer(body["total_disposals"])
assert is_list(body["recent"])
assert is_map(body["categorical_aggregate"])
end
test "response carries the privacy boundary statement" do
conn =
conn(:get, "/peah/azazel_log")
|> Router.call(@opts)
body = Jason.decode!(conn.resp_body)
# Property 9's privacy boundary must be visible to the caller —
# they need to know that the items they don't see are
# operator-internal, not missing.
assert body["privacy_boundary"] =~ "operator-internal"
assert body["privacy_boundary"] =~ "Property 9"
end
test "response carries the disclosure block (kichush + horaah)" do
conn =
conn(:get, "/peah/azazel_log")
|> Router.call(@opts)
body = Jason.decode!(conn.resp_body)
# Mirrors every other Torah-content response from commit 5e00f75
assert body["disclosure"]["origin"]
assert body["disclosure"]["not_horaah"]
assert body["disclosure"]["torah_basis"]
end
test "since=invalid returns all_time" do
conn =
conn(:get, "/peah/azazel_log?since=not-a-timestamp")
|> Router.call(@opts)
body = Jason.decode!(conn.resp_body)
assert body["since"] == "all_time"
end
test "since=valid_iso8601 is echoed in response" do
conn =
conn(:get, "/peah/azazel_log?since=2026-04-28T00:00:00Z")
|> Router.call(@opts)
body = Jason.decode!(conn.resp_body)
assert body["since"] == "2026-04-28T00:00:00Z"
end
test "categorical_aggregate has all three slots even when empty" do
conn =
conn(:get, "/peah/azazel_log")
|> Router.call(@opts)
body = Jason.decode!(conn.resp_body)
agg = body["categorical_aggregate"]
# All three slots present (the public sees the SHAPE of the
# vidui structure, not its contents)
assert Map.has_key?(agg, "avonot")
assert Map.has_key?(agg, "peshaim")
assert Map.has_key?(agg, "chatot")
end
@tag :l3
test "with disposals: aggregate counts categories accurately" do
base = %{
case_ref: "log-001",
vidui: %{
"avonot" => ["halt avodah_zarah: x"],
"peshaim" => [],
"chatot" => [],
"confessor" => "kohen",
"confessed_at" => "2026-04-28T12:00:00Z"
},
routing: "eretz_gezerah",
authorized_by: "kohen-gadol",
shaliach: "ish-itti",
disposed_at: ~U[2026-04-28 12:00:01Z]
}
{:ok, _} = VavPipeline.L3.Store.record_disposal(base)
{:ok, _} = VavPipeline.L3.Store.record_disposal(Map.put(base, :case_ref, "log-002"))
conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts)
body = Jason.decode!(conn.resp_body)
assert body["total_disposals"] == 2
assert body["categorical_aggregate"]["avonot"] == 2
end
@tag :l3
test "PRIVACY BOUNDARY: response NEVER contains vidui item strings" do
secret_phrase =
"kohen-confidential-content-that-must-never-leak-to-public-endpoint"
attrs = %{
case_ref: "privacy-001",
vidui: %{
"avonot" => [],
"peshaim" => [],
"chatot" => [secret_phrase],
"confessor" => "kohen",
"confessed_at" => "2026-04-28T12:00:00Z"
},
routing: "eretz_gezerah",
authorized_by: "kohen-gadol",
shaliach: "ish-itti",
disposed_at: ~U[2026-04-28 12:00:01Z]
}
{:ok, _} = VavPipeline.L3.Store.record_disposal(attrs)
conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts)
# The response body — as a string — must NOT contain the
# vidui item content. This is the load-bearing privacy
# guarantee; if the secret_phrase appears anywhere in the
# response, the public endpoint is leaking operator-internal
# state.
refute conn.resp_body =~ secret_phrase
end
end
# ============================================================
# Courtyard — Authenticated Endpoint
# ============================================================
describe "Courtyard endpoint" do
test "POST /courtyard/offering without auth returns 401" do
conn =
conn(:post, "/courtyard/offering", Jason.encode!(%{content: "test"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 401
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "UNAUTHORIZED"
end
test "POST /courtyard/offering with auth but no content returns 422" do
conn =
conn(:post, "/courtyard/offering", Jason.encode!(%{offering_type: "shelamim"}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer test-token")
|> Router.call(@opts)
assert conn.status == 422
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "KIYOR_VALIDATION_FAILED"
end
test "POST /courtyard/offering with auth and content processes" do
conn =
conn(:post, "/courtyard/offering", Jason.encode!(%{
content: "bereishit bara elohim",
offering_type: "shelamim"
}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer test-token")
|> Router.call(@opts)
assert conn.status in [200, 503]
if conn.status == 200 do
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "offering_id")
assert Map.has_key?(body, "offering_receipt")
assert body["offering_receipt"]["type"] == "shelamim"
end
end
end
# ============================================================
# Chatzer — Sanhedrin Endpoint
# ============================================================
describe "Chatzer endpoint" do
test "POST /chatzer/sanhedrin without auth returns 401" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "test case"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 401
end
test "POST /chatzer/sanhedrin without case returns 422" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{not_case: "wrong field"}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 422
end
test "POST /chatzer/sanhedrin with valid case deliberates" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{
case: "Is cooking on Shabbat permitted for pikuach nefesh?"
}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "case_ref")
assert Map.has_key?(body, "deliberation")
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
assert body["note"] =~ "not a binding ruling"
end
# Tablet 55: the Tamid's own example — a case WITH sides — returned
# 500 and took the halt monitor and escalation down with it.
test "POST /chatzer/sanhedrin with sides (the Tamid's example) deliberates" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{
case: "A borrower repaid before one witness; the lender denies receipt. Who bears the burden?",
sides: [
%{position: "the borrower is believed", evidence: ["the money was handed over before a witness"]},
%{position: "the lender is believed", evidence: ["one witness does not establish payment"]}
],
complexity_hint: "monetary"
}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
# and the gate is still standing afterwards
assert Process.whereis(:halt_monitor) != nil
assert Process.whereis(:escalation) != nil
end
test "POST /chatzer/sanhedrin with malformed sides returns 422, not 500" do
for bad <- ["not a list", [%{"no_position" => 1}], [%{"position" => "p", "evidence" => "not a list"}]] do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "x", sides: bad}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 422, "expected 422 for sides=#{inspect(bad)}, got #{conn.status}"
assert Jason.decode!(conn.resp_body)["error"]["code"] == "KIYOR_VALIDATION_FAILED"
end
end
end
# ============================================================
# Kodesh — Operator Endpoint
# ============================================================
describe "Kodesh endpoint" do
test "GET /kodesh/health returns health check" do
conn =
conn(:get, "/kodesh/health")
|> Router.call(@opts)
assert conn.status in [200, 503]
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "overall")
assert Map.has_key?(body, "checks")
assert body["overall"] in ["healthy", "degraded", "critical", "shabbat"]
# Required health checks
assert Map.has_key?(body["checks"], "l1_hash")
assert Map.has_key?(body["checks"], "halt_monitor_alive")
assert Map.has_key?(body["checks"], "esh_tamid")
end
test "GET /kodesh/status returns system status" do
conn =
conn(:get, "/kodesh/status")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "cloud_state")
assert Map.has_key?(body, "components")
assert Map.has_key?(body, "halt_status")
assert Map.has_key?(body, "signal_count")
end
end
# ============================================================
# Error handling — Chatat protocol
# ============================================================
describe "error handling" do
test "unknown endpoint returns 404" do
conn =
conn(:get, "/unknown/path")
|> Router.call(@opts)
assert conn.status == 404
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "NOT_FOUND"
end
test "all errors follow chatat format" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 422
body = Jason.decode!(conn.resp_body)
# Chatat format: error.code, error.type, error.message, error.guidance
assert Map.has_key?(body["error"], "code")
assert Map.has_key?(body["error"], "type")
assert Map.has_key?(body["error"], "message")
assert Map.has_key?(body["error"], "guidance")
end
test "all responses are JSON" do
conn =
conn(:get, "/kodesh/health")
|> Router.call(@opts)
content_type = get_resp_header(conn, "content-type")
assert Enum.any?(content_type, &String.contains?(&1, "application/json"))
end
end
end