Headers X-Bikoret-Key / -Rebuke / -Verdict / -Bedikah; the `bikoret` object and the "— bikoret …" fringe on a gate reply; environment variables BIKORET_*; the image user and paths (/home/bikoret, /tmp/bikoret, /var/lib/bikoret/manna, /etc/bikoret/secrets, /opt/bikoret/spec); the tmp-bikoret pod volume; local databases bikoret_dev/_test. Kubernetes resource names, the shorashim_app role and the domain are unchanged (Tamid §1.1, "the names that still carry the old word"). Doorpost c963c4fe…. Elixir 604/0, L3 10/0, korban 295, Prolog bedikah 44/44. Gitea repo renamed heaviness510/bikoret (old URL redirects). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
399 lines
13 KiB
Elixir
399 lines
13 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
|
|
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
|