""" 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")