Files
bikoret/vav_pipeline/test/bigdei_kodesh_test.exs
T
Scott KarpandClaude Fable 5.1 4bffa01e0e tablet 55, third: roots the engine can read; abstentions count; an empty trace is no ground
Live after the second commit (vav :046010f0): a monetary case naming
bereishit 1:1 convened three judges and came back "accepted" — the
interpret seat said "resolved (5 roots, 0 verses)" with
reading(mitzvot_count(0),verse_count(0),grounded_in([],[])), the other
two seats said teiku, and one accept beat two abstentions (the reply
showed majority 1, minority 2). Three defects the tests could not see:

- The engine names roots by letter (bet_resh_aleph; json_bridge reads
  "bet-resh-aleph") and the pipeline sent it the Hebrew, which its
  verse facts never match. BereishitCycle.engine_root_name/1 builds
  the engine's name from the Shoresh Engine's letter names or, for the
  L1 fallback, from the Hebrew with finals folded; the router sends
  that. (The cycle's own Talmudic routing still sends the Hebrew —
  recorded, not changed here: it alters /peah/ask and needs its own
  verification.)
- judge.erl: "resolved" with no verse and no mitzvah is a reading
  coupled to nothing (avodah_zarah.pl Form 2); the judge abstains and
  says "resolved on an empty trace; nothing to stand on".
- panel.erl: the majority is the verdict with the most votes,
  abstentions included; an abstaining plurality or an accept/reject
  tie is teiku.

EUnit 57/0 (plurality, tie, empty trace). Elixir 632/0 (engine_root_name
from names, from Hebrew, nil on nothing; the no-engine case is an
abstaining majority of one, teiku).

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

597 lines
22 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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([])
# Tablet 55: a consultation the judges can be pointed at (judge.erl reads
# {M, F} from the sanhedrin env). Seat 2 (challenge) finds a kushya; the
# other seats read the verse as resolved.
def fake_consult(:challenge, _ref, _roots, _q, _ctx),
do: {:ok, %{status: "challenged", text: "", kushya: "but the verse says otherwise", verses: [{:devarim, 6, 4}]}}
def fake_consult(_step, _ref, _roots, _q, _ctx),
do: {:ok, %{status: "resolved", text: "the reading", verses: [{:bereishit, 1, 1}]}}
# A day's manna for the chatzer tests: a panel spends one unit of
# sanhedrin_panels_max (tablet 55), so the ledger must hold a provision.
defp write_provision(panels) do
VavPipeline.MannaLedger.reset()
on_exit(fn -> VavPipeline.MannaLedger.reset() end)
today = Date.utc_today() |> Date.to_iso8601()
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
provision = %{
"date" => today,
"provision" => %{"compute_units" => 50, "memory_mb" => 512, "api_calls_external" => 10, "storage_delta_mb" => 2, "sanhedrin_panels_max" => panels},
"type" => "regular",
"expires_at" => expires,
"written_by" => "bigdei_kodesh_test",
"written_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
path = VavPipeline.Manna.provision_file()
File.mkdir_p!(Path.dirname(path))
File.write!(path, Jason.encode!(provision))
on_exit(fn -> File.rm(path) end)
end
# ============================================================
# 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
write_provision(2)
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
write_provision(2)
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"]
# the hint names the panel: monetary → three judges (sanhedrin.hrl)
assert body["deliberation"]["panel_size"] == 3
# the Tamid's shape — votes and sources, not vote_count and torah_refs
assert Map.keys(body["deliberation"]["majority"]) |> Enum.sort() == ["position", "reasoning", "sources", "votes"]
assert is_list(body["deliberation"]["majority"]["sources"])
# the case was resolved before it was heard
assert is_map(body["grounding"]["scope"])
assert is_list(body["grounding"]["roots_consulted"])
# and the gate is still standing afterwards
assert Process.whereis(:halt_monitor) != nil
assert Process.whereis(:escalation) != nil
end
# Tablet 55: without a Talmudic Engine to consult, the judges abstain
# and the court says teiku — it does not accept on nothing.
test "POST /chatzer/sanhedrin with no engine to consult is teiku, not accepted" do
# The socket process may run in the test env; no engine answers behind it.
Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult})
write_provision(2)
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "bereishit 1:1 — what is created first?"}))
|> 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"] == "teiku"
assert body["deliberation"]["panel_size"] == 1
# the one judge abstained: an abstaining majority of one, no dissent
assert body["deliberation"]["majority"]["votes"] == 1
assert body["deliberation"]["majority"]["reasoning"] =~ "no consultation"
assert body["deliberation"]["minority"]["votes"] == 0
end
# Tablet 55: with a consultation behind the judges, the dialectic
# decides — interpret resolved, challenge finds a kushya, resolve
# resolved → 2 accept, 1 reject; the minority preserved with its verse.
test "POST /chatzer/sanhedrin monetary panel splits 2–1 from the dialectic" do
Application.put_env(:sanhedrin, :consult, {__MODULE__, :fake_consult})
write_provision(2)
try do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{
case: "devarim 6:4 — is the unity declared or commanded?",
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
d = Jason.decode!(conn.resp_body)["deliberation"]
assert d["panel_size"] == 3
assert d["outcome"] == "accepted"
assert d["majority"]["votes"] == 2
assert d["majority"]["position"] == "the reading"
assert d["majority"]["sources"] == ["bereishit 1:1"]
assert d["minority"]["votes"] == 1
assert d["minority"]["position"] == "but the verse says otherwise"
assert d["minority"]["sources"] == ["devarim 6:4"]
after
Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult})
end
end
# Devarim 17:9 — the decision is engraved in L3: an interpretation row
# with the panel and the votes, and the minority beside it.
@tag :l3
test "POST /chatzer/sanhedrin engraves the decision and the minority in L3" do
# The record is written from the request process (the router), so a
# checkout here covers it; the sandbox is shared for the sake of the
# panel's processes, which do not touch the Repo.
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, {:shared, self()})
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, :manual) end)
Application.put_env(:sanhedrin, :consult, {__MODULE__, :fake_consult})
write_provision(2)
try do
before = VavPipeline.L3.Store.count_interpretations()
before_min = VavPipeline.L3.Store.count_minority_opinions()
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "devarim 6:4", 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["grounding"]["recorded"]["l3"] == "recorded"
assert VavPipeline.L3.Store.count_interpretations() == before + 1
assert VavPipeline.L3.Store.count_minority_opinions() == before_min + 1
after
Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult})
end
end
# A panel is manna (Vayakhel §3.7): the day's sanhedrin_panels_max spent → 503.
test "POST /chatzer/sanhedrin with the day's panels spent returns 503 MANNA_DEPLETED" do
write_provision(1)
body = Jason.encode!(%{case: "bereishit 1:1"})
post = fn ->
conn(:post, "/chatzer/sanhedrin", body)
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
end
assert post.().status == 200
second = post.()
assert second.status == 503
assert Jason.decode!(second.resp_body)["error"]["code"] == "MANNA_DEPLETED"
end
test "POST /chatzer/sanhedrin with an unknown complexity_hint returns 422" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "x", complexity_hint: "cosmic"}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 422
assert Jason.decode!(conn.resp_body)["error"]["guidance"]["issue"] =~ "complexity_hint"
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