Pass 4 / Finding F, commit 8 of 10 from BUILD_AZAZEL.md. The
public-half of property 9 (observable disposal). The Rambam in
Moreh Nevuchim 3:46 makes the load-bearing claim: "an unobserved
disposal is architecturally incomplete." The community's awareness
that errors are visibly carried away IS the implementation; the
route on the cliff serves no purpose without the witnesses on the
temple steps.
This endpoint is those witnesses. Anyone with HTTP access to
Shorashim can see WHAT was disposed (categorically) and HOW MANY
disposals occurred — turning the disposal channel from a private
operator action into a public, witnessed fact.
PRIVACY BOUNDARY (BUILD_AZAZEL.md §6 Q6):
The endpoint exposes:
- total disposal count (optionally filtered by ?since=ISO8601)
- categorical aggregate (avonot/peshaim/chatot counts across
recent disposals — what KIND of errors were disposed)
- up to 50 recent disposals as case_ref + shaliach +
triggering_call + disposed_at + categories_filled (which slots
of vidui were filled)
The endpoint does NOT expose:
- vidui ITEMS (the operator's specific descriptions of each
disposed thing)
- the disposed PAYLOAD (which doesn't exist in L3 anyway —
property 10)
- any handle for retrieving the disposed content (there isn't one)
The detailed vidui items are operator-internal, accessible via
future /kodesh/* endpoints. The split is deliberate: public is
WITNESS, operator is AUDITOR. The witness sees the fact;
the auditor reads the contents.
The privacy boundary statement is included in every response so
callers know what they're not seeing — they can reason about the
endpoint's silence rather than guess.
GRACEFUL DEGRADATION:
When VavPipeline.Repo is not running (dev/test without Postgres),
the endpoint returns total_disposals=0 with l3_status="unavailable"
rather than crashing. Mirrors the maybe_add_repo pattern from
MEMORY.md — Shorashim should respond cleanly even when L3 is down,
because /peah/* is the public-facing tier and must be more
resilient than internal tiers.
CHANGED:
vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex
+ GET /peah/azazel_log
Optional ?since=ISO8601 filter; returns total +
categorical_aggregate + recent[<=50] + l3_status +
privacy_boundary statement + disclosure block (kichush +
not_horaah from commit 5e00f75)
+ parse_since/1 helper (graceful: bad input → all_time)
+ format_since/1 helper
vav_pipeline/test/bigdei_kodesh_test.exs
+ 8 new tests covering:
- 200 OK when no disposals recorded (empty L3)
- response carries privacy_boundary statement
- response carries disclosure block
- since=invalid → all_time
- since=valid_iso8601 → echoed
- categorical_aggregate has all 3 slots even when empty
- [@l3] aggregate counts categorically across multiple disposals
- [@l3] PRIVACY BOUNDARY: response NEVER contains vidui item
strings (load-bearing — verifies a known secret_phrase
inserted into a vidui's chatot does NOT leak into the
response body)
Tests: vav_pipeline 361 passed, 0 failures, 35 excluded (was
353/33; +6 visible response-shape tests + 2 :l3-tagged tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
393 lines
13 KiB
Elixir
393 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"] =~ "Shorashim"
|
|
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
|
|
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
|
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
|
|
|
|
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
|
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
|
|
|
|
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 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 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
|