feat(agent-office): Tarot Claude 파이프라인 + reroll 1회 (T5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
113
agent-office/tests/test_tarot_pipeline.py
Normal file
113
agent-office/tests/test_tarot_pipeline.py
Normal file
@@ -0,0 +1,113 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user