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>
595 lines
22 KiB
Elixir
595 lines
22 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([])
|
||
|
||
# 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
|