""" Tests for model_interface — outbound API calls. Construction §3.4: outbound HTTPS only, no inbound. No live API calls in tests — use mocked httpx responses. Kiyyor principle: stateless functions, no connection pooling between calls. """ import json import os import pytest import httpx from korban_processor.model_interface import ( ModelRequest, ModelResponse, call_claude, CLAUDE_MODEL, CLAUDE_MAX_TOKENS, ANTHROPIC_VERSION, ) class TestModelRequest: def test_default_max_tokens(self): req = ModelRequest(query="test") assert req.max_tokens == CLAUDE_MAX_TOKENS def test_no_system_prompt_by_default(self): req = ModelRequest(query="test") assert req.system_prompt is None def test_custom_max_tokens(self): req = ModelRequest(query="test", max_tokens=1024) assert req.max_tokens == 1024 def test_with_system_prompt(self): req = ModelRequest(query="test", system_prompt="You analyze Torah.") assert req.system_prompt == "You analyze Torah." class TestModelResponseShape: def test_model_response_fields(self): resp = ModelResponse( content="בְּרֵאשִׁית means 'in the beginning'.", model=CLAUDE_MODEL, stop_reason="end_turn", input_tokens=10, output_tokens=50, ) assert resp.content assert resp.model == CLAUDE_MODEL assert resp.stop_reason == "end_turn" assert resp.input_tokens == 10 assert resp.output_tokens == 50 class TestCallClaudeRequiresApiKey: def test_raises_without_api_key(self, monkeypatch): monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"): call_claude(ModelRequest(query="test")) class TestCallClaudeMocked: """Test call_claude with mocked httpx transport — no live API calls.""" def _make_api_response(self, text: str) -> dict: return { "id": "msg_test", "type": "message", "role": "assistant", "model": CLAUDE_MODEL, "content": [{"type": "text", "text": text}], "stop_reason": "end_turn", "usage": {"input_tokens": 20, "output_tokens": 40}, } def test_sends_correct_model(self, monkeypatch, respx_mock=None): """Verify the request uses the specified Claude model.""" # Test via request inspection using httpx transport mock captured_requests = [] class CapturingTransport(httpx.BaseTransport): def handle_request(self, request): captured_requests.append(request) body = self._make_api_response( "The root ר-א-ש means head. Genesis 1:1." ) return httpx.Response( 200, content=json.dumps(body).encode(), headers={"content-type": "application/json"}, request=request, ) def _make_api_response(self, text): return { "id": "msg_test", "type": "message", "role": "assistant", "model": CLAUDE_MODEL, "content": [{"type": "text", "text": text}], "stop_reason": "end_turn", "usage": {"input_tokens": 20, "output_tokens": 40}, } import korban_processor.model_interface as mi original_client = httpx.Client monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key-123") def patched_client(**kwargs): return original_client(transport=CapturingTransport(), **{ k: v for k, v in kwargs.items() if k not in ("transport",) }) monkeypatch.setattr(httpx, "Client", patched_client) req = ModelRequest(query="What does bereishit mean?") resp = mi.call_claude(req) assert len(captured_requests) == 1 body = json.loads(captured_requests[0].content) assert body["model"] == CLAUDE_MODEL assert body["max_tokens"] == CLAUDE_MAX_TOKENS def test_correct_anthropic_headers(self, monkeypatch): """Verify the request sends correct Anthropic API headers.""" captured_requests = [] class HeaderCapture(httpx.BaseTransport): def handle_request(self, request): captured_requests.append(request) body = { "id": "msg_test", "type": "message", "role": "assistant", "model": CLAUDE_MODEL, "content": [{"type": "text", "text": "test response"}], "stop_reason": "end_turn", "usage": {"input_tokens": 5, "output_tokens": 10}, } return httpx.Response( 200, content=json.dumps(body).encode(), headers={"content-type": "application/json"}, request=request, ) monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key-abc") import korban_processor.model_interface as mi original_client = httpx.Client def patched_client(**kwargs): return original_client(transport=HeaderCapture(), **{ k: v for k, v in kwargs.items() if k not in ("transport",) }) monkeypatch.setattr(httpx, "Client", patched_client) mi.call_claude(ModelRequest(query="test")) req = captured_requests[0] assert req.headers["x-api-key"] == "test-key-abc" assert req.headers["anthropic-version"] == ANTHROPIC_VERSION assert req.headers["content-type"] == "application/json" def test_outbound_only_https(self, monkeypatch): """Construction §3.4: outbound HTTPS only. Verify the URL is HTTPS.""" import korban_processor.model_interface as mi assert mi.CLAUDE_API_URL.startswith("https://"), \ "Claude API URL must use HTTPS — outbound HTTPS only per Construction §3.4"