Files
bikoret/korban_processor/test/test_external_offering.py
T
Scott KarpandClaude Opus 4.7 1a27a283ae azazel commit 9 — Korban→Azazel intake routing + Chatzotzrot ack (properties 1 + 6)
Pass 4 / Finding F, commit 9 of 10 from BUILD_AZAZEL.md. Closes the
last two property gaps: 1 (parity provisioning, end-to-end) and 6
(out-of-band acknowledgment via Chatzotzrot). The Azazel Processor
is now functionally complete; commit 10 is the closure-and-review
pass with no further code.

PROPERTY 1 — PARITY PROVISIONING, two layers:

LAYER 1 (import-time): external_offering imports azazel_processor
at module load. If the import fails, external_offering refuses to
load with a Torah-anchored ImportError. The two-goat architecture is
SYMMETRIC OR IT IS BROKEN — operators cannot run the processing
channel without the disposal channel being importable. The strongest
formulation: if you cannot dispose, you cannot process.

LAYER 2 (runtime): when zerikah produces no vital content,
consult_external_model now routes the korban to disposal rather than
returning an empty ProcessedKorban with the rejection in `discarded`.
The empty-vital case is named (vidui), routed (eretz_gezerah),
recorded (L3 azazel_records), and acknowledged (chatzotzrot :moadim).

PROPERTY 6 — OUT-OF-BAND ACKNOWLEDGMENT:

The Python orchestrator drops the AzazelRecord JSON in the pending-l3
directory. The Elixir AzazelIngest GenServer reads it, persists to
L3 via record_disposal/1, AND fires Chatzotzrot.signal(:moadim, ...)
with a categorical summary (case_ref + shaliach + categories_filled).
The chatzotzrot is the side channel — distinct from the disposal
channel itself. The orchestrator does NOT know whether the signal
was successfully transmitted; the disposal completes regardless.

The :moadim signal type was the right choice: Yom Kippur completion
semantics. This disposal cycle is closed; the public fact (property
9) is now witnessable via /peah/azazel_log.

PRIVACY BOUNDARY ALSO APPLIES TO THE CHATZOTZROT:

The signal's `reason` field carries a CATEGORICAL summary, not the
vidui items. A test verifies that a known secret string in a vidui
chatot does NOT leak into the chatzotzrot signal — the operator-side
event channel is held to the same privacy boundary as the public
endpoint.

CHANGED:

  korban_processor/src/korban_processor/external_offering.py
    + Import azazel_processor at module load (raises ImportError
      with Torah-anchored explanation if azazel_processor is not
      importable — property 1, layer 1)
    + DEFAULT_AZAZEL_PENDING_L3_DIR
    + ExternalConsultation.disposal_record: Optional[AzazelRecord]
        Non-None iff the consultation was routed to disposal
    + _content_addressable_case_ref(content, source_query)
        SHA-256-based stable identifier for audit
    + _make_pending_l3_recorder(pending_dir)
        Returns a recorder callable that drops the JSON file
        atomically (write to .tmp, then os.replace)
    + _route_to_azazel(...) — internal helper running the full
        protocol envelope:
        - opens authorize_disposal context
        - activates a fresh IshItti shaliach
        - builds vidui via confess_empty_vital
        - dispatches via dispatch_for_disposal
        - tevilah's the shaliach
    + consult_external_model:
        - new keyword args azazel_kohen_gadol, azazel_shaliach_id,
          azazel_pending_l3_dir
        - branch on `if not vital.strip()`: route to disposal,
          attach record to ExternalConsultation

  vav_pipeline/lib/vav_pipeline/azazel_ingest.ex
    + ingest_file/2 fires Chatzotzrot.signal(:moadim, ...) after
      successful L3 write, BEFORE shredding the source file. So the
      ack comes BEFORE the in-flight artifact disappears — recovery
      is possible if either side crashes.
    + Process.whereis(Chatzotzrot)-guarded so tests without the full
      supervision tree still work.
    + chatzotzrot_summary/1 builds the categorical summary
      (case_ref + shaliach + categories_filled). Privacy boundary:
      no vidui items, only counts/category names.

TESTS:

  korban_processor/test/test_external_offering.py             +4 tests
    - empty-vital response routes to azazel:
      - processed.vital_content == ""
      - disposal_record is an AzazelRecord with kohen + shaliach
      - pending-l3 file dropped with correct shape
    - processable response does NOT create disposal_record (the
      negative case — vital content goes through normally)
    - PRIVACY: pending-l3 file does not contain the model's
      response text (verified with a known secret_phrase)
    - case_ref is content-addressable: same content + same
      source_query → same case_ref (audit can recompute)

  vav_pipeline/test/azazel_ingest_test.exs                    +2 tests
    - successful ingest fires Chatzotzrot :moadim signal
      (subscribe :all, observe — :moadim routes to :all per
      chatzotzrot.ex routing table)
    - PRIVACY: chatzotzrot signal does NOT include vidui item
      content (known secret_phrase verified absent from the signal)

Tests:
  - korban_processor: 207 passed, 1 skipped (was 203/1; +4)
  - azazel_processor: 125 passed (unchanged)
  - vav_pipeline:     363 passed, 0 failures, 35 excluded
                      (was 361/35; +2 azazel_ingest chatzotzrot tests)

Property coverage after commit 9:
  All 12 properties of azazel_engineering_reading.md §14 defended
  by code:
    1  parity provisioning      ✓ (import-time + runtime)
    2  blind routing / goral     ✓
    3  enumerative commit        ✓
    4  role-separated operator   ✓
    5  provisioned infrastructure ✓
    6  out-of-band acknowledgment ✓
    7  authorized routing        ✓
    8  protocol-restricted access ✓
    9  observable disposal       ✓ (operator-internal + public)
    10 hard termination          ✓
    11 classification discipline ✓
    12 unmanaged destination     ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:36:16 -04:00

317 lines
12 KiB
Python

"""
Tests for external_offering — the orchestrated outbound consultation flow.
Architecture §4 Step 22.
These tests use a mock model caller so no real API requests are made.
A separate live integration test (skipped by default) can run against
Anthropic's API when ANTHROPIC_API_KEY is set and the SHORASHIM_LIVE_API
env var is true.
"""
import os
import pytest
from korban_processor.external_offering import (
ExternalConsultation,
consult_external_model,
)
from korban_processor.korban import OfferingType, Provenance
from korban_processor.model_interface import ModelRequest, ModelResponse
def _fake_caller(response_text: str):
"""Build a mock caller that returns a fixed response."""
def caller(request: ModelRequest) -> ModelResponse:
return ModelResponse(
content=response_text,
model="mock-claude",
stop_reason="end_turn",
input_tokens=10,
output_tokens=20,
)
return caller
# ============================================================
# Basic flow
# ============================================================
def test_consult_external_model_returns_full_consultation():
"""A consultation captures request, raw response, processed korban,
and routing — the full chain is auditable."""
fake_response = (
"Bereishit 1:1 begins with the root bara. The shoresh ב-ר-א means "
"to create from nothing. This is foundational."
)
result = consult_external_model(
query="Explain bereishit 1:1",
source_query="user_courtyard_request_42",
caller=_fake_caller(fake_response),
)
assert isinstance(result, ExternalConsultation)
assert result.request.query == "Explain bereishit 1:1"
assert result.raw_response.content == fake_response
assert result.raw_response.model == "mock-claude"
assert result.processed.provenance is Provenance.CLAUDE
assert result.routing.destination == "talmudic_engine" # default OLAH
def test_raw_korban_is_preserved_on_processed_output():
"""Eish zarah Form 3 (gilui arayot — severed coupling): the raw
korban must be preserved on the processed output. The floor between
input and output is never severed."""
result = consult_external_model(
query="any",
source_query="trigger_query",
caller=_fake_caller(
"The root k-d-sh appears in Vayikra. Holiness is separation."
),
)
raw = result.processed.raw_korban
assert raw is not None, (
"raw_korban must be preserved on ProcessedKorban — "
"severing input from output is gilui arayot"
)
assert raw.source_query == "trigger_query"
assert raw.provenance is Provenance.CLAUDE
def test_provenance_is_recorded_on_korban():
"""Eish zarah Form 1 (avodah zarah — external as Torah): provenance
is never concealed. Every external response is tagged with its source."""
result = consult_external_model(
query="any",
source_query="any",
provenance=Provenance.CLAUDE,
caller=_fake_caller("test response with bereishit reference"),
)
assert result.processed.provenance is Provenance.CLAUDE
assert result.processed.raw_korban.provenance is Provenance.CLAUDE
# ============================================================
# Offering type → routing destination
# ============================================================
@pytest.mark.parametrize("offering_type,destination", [
(OfferingType.OLAH, "talmudic_engine"),
(OfferingType.SHELAMIM, "talmudic_engine"),
(OfferingType.MINCHAH, "talmudic_engine"),
(OfferingType.CHATAT, "error_correction"),
])
def test_offering_type_drives_routing(offering_type, destination):
"""Each offering type routes to its specified destination."""
result = consult_external_model(
query="any",
source_query="any",
offering_type=offering_type,
caller=_fake_caller(
"The shoresh r-tz-h means will. Vayikra 1:3 uses lirtzono — "
"for his acceptance."
),
)
assert result.routing.destination == destination
# ============================================================
# Refusal to silently proxy unknown providers
# ============================================================
def test_unknown_provenance_without_caller_raises():
"""We don't silently route MISTRAL/GPT/etc through Claude. A caller
must be supplied explicitly. This prevents accidental misrouting and
keeps provenance honest."""
with pytest.raises(NotImplementedError) as excinfo:
consult_external_model(
query="any",
source_query="any",
provenance=Provenance.GPT,
)
assert "GPT" in str(excinfo.value).upper() or "gpt" in str(excinfo.value)
def test_explicit_caller_overrides_provider_check():
"""If the caller supplies a callable for a non-Claude provider, the flow
proceeds — they took responsibility for the wiring."""
result = consult_external_model(
query="any",
source_query="any",
provenance=Provenance.GPT,
caller=_fake_caller("test response with shoresh sh-l-m"),
)
assert result.processed.provenance is Provenance.GPT
# ============================================================
# System prompt is forwarded
# ============================================================
def test_system_prompt_is_forwarded():
"""A system prompt sent in becomes part of the request record."""
result = consult_external_model(
query="any",
source_query="any",
system_prompt="You are a Torah scholar. Cite verse references.",
caller=_fake_caller("Bereishit 1:1 is the opening verse."),
)
assert result.request.system_prompt == "You are a Torah scholar. Cite verse references."
# ============================================================
# Empty-vital → Azazel disposal routing (commit 9, properties 1 + 6)
# ============================================================
# Property 1 at runtime: an empty-vital korban is routed to disposal,
# not silently dropped. The disposal record IS the permanent
# acknowledgment that the system received this input and could not
# metabolize it.
import json
import pathlib
import tempfile
from azazel_processor import AzazelRecord
def _empty_vital_response():
"""A model response that contains no extractable Torah content
(no Torah refs, no Hebrew, no insight markers, no roots).
zerikah will return empty vital for this."""
return "ok thanks. let me know if you want me to do anything else."
def test_empty_vital_korban_is_routed_to_azazel():
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
pending = pathlib.Path(d)
result = consult_external_model(
query="anything",
source_query="empty_vital_test_001",
caller=_fake_caller(_empty_vital_response()),
azazel_kohen_gadol="kohen-gadol-test",
azazel_shaliach_id="ish-itti-test",
azazel_pending_l3_dir=pending,
)
# The processed korban is empty (zerikah returned no vital)
assert result.processed.vital_content == ""
# The disposal_record field is now populated
assert result.disposal_record is not None
assert isinstance(result.disposal_record, AzazelRecord)
assert result.disposal_record.shaliach == "ish-itti-test"
assert result.disposal_record.authorized_by == "kohen-gadol-test"
assert result.disposal_record.triggering_call == (
"external_offering.consult_external_model"
)
# A pending-l3 file was dropped for AzazelIngest to pick up
files = list(pending.glob("*.json"))
assert len(files) == 1
body = json.loads(files[0].read_text())
assert body["case_ref"] == result.disposal_record.case_ref
assert body["routing"] == "eretz_gezerah"
assert body["shaliach"] == "ish-itti-test"
# The vidui is on the pending file (operator-internal channel)
assert body["vidui"]["confessor"] == "kohen-gadol-test"
assert body["vidui"]["chatot"] # non-empty
def test_processable_response_does_not_create_disposal_record():
"""The companion to the above: an offering with extractable Torah
content goes through the processing channel and the disposal_record
field stays None."""
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
result = consult_external_model(
query="explain bereishit 1:1",
source_query="processable_test_001",
caller=_fake_caller(
"Bereishit 1:1 — the shoresh ב-ר-א means to create. The verse "
"indicates beginnings."
),
azazel_pending_l3_dir=pathlib.Path(d),
)
assert result.disposal_record is None
assert result.processed.vital_content != ""
# No file was dropped — nothing for AzazelIngest to pick up
assert list(pathlib.Path(d).glob("*.json")) == []
def test_pending_l3_file_does_not_contain_payload():
"""Property 10 follow-through: the file the orchestrator drops
for the Elixir bridge contains the vidui (what was disposed) but
not the disposed thing itself. The model's actual response text
must not appear in the pending-l3 file."""
secret_phrase = "this-string-must-not-leak-to-the-pending-l3-file"
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
pending = pathlib.Path(d)
consult_external_model(
query="any",
source_query="leak_check_001",
caller=_fake_caller(secret_phrase), # response is just the secret
azazel_pending_l3_dir=pending,
)
files = list(pending.glob("*.json"))
assert len(files) == 1
body_text = files[0].read_text()
assert secret_phrase not in body_text
def test_case_ref_is_content_addressable():
"""Audit can verify the routing was for a specific input by
recomputing the case_ref. Property 7's audit-trail half — the
routing decision is owned and traceable."""
response = _empty_vital_response()
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
pending = pathlib.Path(d)
result1 = consult_external_model(
query="any",
source_query="audit_test_001",
caller=_fake_caller(response),
azazel_pending_l3_dir=pending,
)
# Same content + same source_query → same case_ref
with tempfile.TemporaryDirectory(prefix="azazel-pending-") as d:
pending = pathlib.Path(d)
result2 = consult_external_model(
query="different-query", # query is not in the case_ref
source_query="audit_test_001",
caller=_fake_caller(response),
azazel_pending_l3_dir=pending,
)
assert result1.disposal_record.case_ref == result2.disposal_record.case_ref
# ============================================================
# Live API integration test — opt-in only
# ============================================================
@pytest.mark.skipif(
not os.environ.get("SHORASHIM_LIVE_API"),
reason="Set SHORASHIM_LIVE_API=1 to run a real API call against Anthropic",
)
def test_live_anthropic_call():
"""Real API call. Costs money. Run only when explicitly enabled."""
if not os.environ.get("ANTHROPIC_API_KEY"):
pytest.skip("ANTHROPIC_API_KEY not set")
result = consult_external_model(
query="What is the shoresh of bereishit?",
source_query="live_integration_test",
offering_type=OfferingType.OLAH,
)
assert result.raw_response.content
assert result.processed.raw_korban is not None
assert result.processed.provenance is Provenance.CLAUDE
assert result.raw_response.model.startswith("claude")