Files
bikoret/vav_pipeline/test/bigdei_kodesh_test.exs
T
Scott KarpandClaude Fable 5.1 046010f0ed tablet 55, second: the judge consults — the Sanhedrin no longer rolls dice
judge.erl chose one of six canned positions by a hash over the question,
its own pid and the clock; a panel of one "accepted" on zero roots and
zero references under "Torah references ground this interpretation" —
the calf, cast by the court itself. Architecture §3.2 says a judge
"consults the specification (L0/L1)"; Construction §3.2 says the
pipeline hands the case roots and torah_refs. Neither happened.

Sanhedrin (Erlang):
- judge.erl: seat N performs step N of the dialectic — interpret,
  challenge, resolve, cycling — through a consult {M, F} in the app env.
  resolved → accept with the verses; challenged (a kushya stands) →
  reject; halt → reject; teiku → abstain. No consult, an error, or a
  consult that raises → abstain, and the reasoning says so; the judge
  stands. judge:start_link/2 takes the seat; panel.erl seats its judges.
- EUnit 54/0: seats cycle; no consult → abstain; resolved → accept with
  #torah_ref{}; a raising consult → abstain and alive; a panel of three
  with a kushya at seat 2 → accepted 2–1, minority preserved with verse.

Pipeline (Elixir):
- VavPipeline.SanhedrinConsult: what a judge asks — TalmudicEngineSocket
  interpret/challenge/resolve over the case's roots, direct (the client's
  stub fallbacks are NOT used: a court that accepts on a stub is the
  calf). Engine absent → {:error, _} → the judge abstains. Registered in
  the sanhedrin env at application start.
- router process_sanhedrin: the case is heard before it is submitted —
  ScopeResolver → Shema context (cloud honoured) → tohu → vayomer
  (Shoresh Engine over the verse) → roots; torah_refs = the scope;
  complexity_hint names the panel (422 on an unknown one); one unit of
  sanhedrin_panels_max per case (503 MANNA_DEPLETED when spent); the
  decision engraved in L3 as an interpretation (panel_size, votes
  for/against, processing_component "sanhedrin", coupling root→verse)
  with the minority beside it (grounding_type achorai); the reply in
  the Tamid's shape (position, reasoning, sources, votes) plus
  grounding {scope, roots_consulted, recorded}.
- Tests: Elixir 628/0 (router 30, incl. monetary → panel 3; no engine →
  teiku with 0 votes, never accepted; fake consult → 2–1 with sources;
  manna → 503; unknown hint → 422); L3 test: interpretation + minority
  rows land.

Pre-existing and untouched: with --include l3 the gate and hakhel
tests fail 17 times on main as well (the gate's local upstream double
refuses connections under the L3 flag) — a separate finding.

Doorpost pin unchanged (no @spec_files touched).

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

595 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
assert body["deliberation"]["majority"]["votes"] == 0
assert body["deliberation"]["minority"]["reasoning"] =~ "no consultation"
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