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>
317 lines
12 KiB
Python
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 BIKORET_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("BIKORET_LIVE_API"),
|
|
reason="Set BIKORET_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")
|