114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
import json
|
|
import pytest
|
|
import respx
|
|
from httpx import Response
|
|
|
|
from app.tarot import pipeline as p
|
|
from app.models import TarotInterpretRequest
|
|
|
|
|
|
def _valid_response_text():
|
|
return json.dumps({
|
|
"summary": "S",
|
|
"cards": [
|
|
{"position": "과거", "card": "the-fool", "reversed": False,
|
|
"interpretation": "i", "advice": "a",
|
|
"evidence": {"card_meaning_used": "k", "position_logic": "p", "category_lens": "c"}},
|
|
{"position": "현재", "card": "the-lovers", "reversed": True,
|
|
"interpretation": "i", "advice": "a",
|
|
"evidence": {"card_meaning_used": "k", "position_logic": "p", "category_lens": "c"}},
|
|
{"position": "미래", "card": "ten-of-cups", "reversed": False,
|
|
"interpretation": "i", "advice": "a",
|
|
"evidence": {"card_meaning_used": "k", "position_logic": "p", "category_lens": "c"}},
|
|
],
|
|
"interactions": [{"type": "synergy", "between": ["the-fool", "ten-of-cups"], "explanation": "."}],
|
|
"advice": "A", "warning": None, "confidence": "medium",
|
|
})
|
|
|
|
|
|
def _claude_resp(text, in_tok=100, out_tok=200):
|
|
return {
|
|
"content": [{"type": "text", "text": text}],
|
|
"usage": {"input_tokens": in_tok, "output_tokens": out_tok},
|
|
}
|
|
|
|
|
|
def _req():
|
|
return TarotInterpretRequest(
|
|
spread_type="three_card",
|
|
category="연애",
|
|
question="Q",
|
|
cards=[
|
|
{"position": "과거", "card_id": "the-fool", "reversed": False},
|
|
{"position": "현재", "card_id": "the-lovers", "reversed": True},
|
|
{"position": "미래", "card_id": "ten-of-cups", "reversed": False},
|
|
],
|
|
cards_reference="REFERENCE",
|
|
context_meta={"major_minor_ratio": "2:1"},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interpret_happy_path(monkeypatch):
|
|
monkeypatch.setattr(p, "ANTHROPIC_API_KEY", "sk-test")
|
|
with respx.mock(base_url="https://api.anthropic.com") as mock:
|
|
mock.post("/v1/messages").mock(return_value=Response(200, json=_claude_resp(_valid_response_text())))
|
|
out = await p.interpret(_req())
|
|
assert out["interpretation_json"]["confidence"] == "medium"
|
|
assert out["tokens_in"] == 100
|
|
assert out["tokens_out"] == 200
|
|
assert out["reroll_count"] == 0
|
|
assert out["cost_usd"] > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interpret_codeblock_strip(monkeypatch):
|
|
monkeypatch.setattr(p, "ANTHROPIC_API_KEY", "sk-test")
|
|
wrapped = "```json\n" + _valid_response_text() + "\n```"
|
|
with respx.mock(base_url="https://api.anthropic.com") as mock:
|
|
mock.post("/v1/messages").mock(return_value=Response(200, json=_claude_resp(wrapped)))
|
|
out = await p.interpret(_req())
|
|
assert out["interpretation_json"]["summary"] == "S"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interpret_reroll_on_validation_fail(monkeypatch):
|
|
monkeypatch.setattr(p, "ANTHROPIC_API_KEY", "sk-test")
|
|
bad = json.loads(_valid_response_text())
|
|
bad["cards"][0]["evidence"]["card_meaning_used"] = ""
|
|
bad_text = json.dumps(bad)
|
|
with respx.mock(base_url="https://api.anthropic.com") as mock:
|
|
route = mock.post("/v1/messages")
|
|
route.side_effect = [
|
|
Response(200, json=_claude_resp(bad_text)),
|
|
Response(200, json=_claude_resp(_valid_response_text())),
|
|
]
|
|
out = await p.interpret(_req())
|
|
assert out["reroll_count"] == 1
|
|
assert out["interpretation_json"]["cards"][0]["evidence"]["card_meaning_used"] == "k"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interpret_raises_when_both_attempts_fail(monkeypatch):
|
|
monkeypatch.setattr(p, "ANTHROPIC_API_KEY", "sk-test")
|
|
bad = json.loads(_valid_response_text())
|
|
bad["cards"][0]["evidence"]["card_meaning_used"] = ""
|
|
bad_text = json.dumps(bad)
|
|
with respx.mock(base_url="https://api.anthropic.com") as mock:
|
|
mock.post("/v1/messages").mock(return_value=Response(200, json=_claude_resp(bad_text)))
|
|
with pytest.raises(p.TarotError):
|
|
await p.interpret(_req())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interpret_raises_when_api_key_missing(monkeypatch):
|
|
monkeypatch.setattr(p, "ANTHROPIC_API_KEY", "")
|
|
with pytest.raises(p.TarotError):
|
|
await p.interpret(_req())
|
|
|
|
|
|
def test_calc_cost():
|
|
assert p.calc_cost(1_000_000, 0) == pytest.approx(3.0)
|
|
assert p.calc_cost(0, 1_000_000) == pytest.approx(15.0)
|
|
assert p.calc_cost(500_000, 500_000) == pytest.approx(9.0)
|