Step 13: Korban Processor complete — 7 modules, 181 tests, all passing
- semichah: provenance tagging (6 sources, rejects empty/anonymous) - shechitah: strip preamble, self-reference, hedging, markdown, sycophancy - zerikah: extract Torah refs, Hebrew text, root analysis, insight markers - haktarah: transform prose to Prolog propositions (verse/3, root/2, claim/2, connection/3) - router: stateless routing — removed module-level _routing_log (kiyyor principle) - model_interface: outbound Claude API via httpx (Construction §3.4, HTTPS only) - hebrew_parser: nikkud/teamim stripping, consonant extraction, gematria, root identification All 5 BUILD_KORBAN.md verification tests pass. AGENTS.md: added structure_not_content.md to spec table, added Principle 6. build_log.md: recorded session, broken tablets #18-20, design decisions. Ka'asher tzivah Hashem et Moshe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f8ff56f74
commit
33f8b03583
16 files changed
+1816
-31
No files matched your search
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
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"
|
||||
Reference in new issue
Block a user