feat(agent-office): Tarot 응답 스키마 검증 (T4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 00:13:38 +09:00
parent d4302acb6a
commit f79dc87d75
2 changed files with 111 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
"""Tarot 응답 스키마 검증 — 누락·빈 필드 reroll 트리거."""
VALID_CONFIDENCE = {"high", "medium", "low"}
def validate_interpretation(parsed: dict, spread_type: str) -> tuple[bool, str]:
if not isinstance(parsed, dict):
return False, "응답이 dict가 아님"
for k in ("summary", "cards", "interactions", "advice", "confidence"):
if k not in parsed:
return False, f"필수 필드 누락: {k}"
if parsed.get("confidence") not in VALID_CONFIDENCE:
return False, f"confidence 값 비정상: {parsed.get('confidence')}"
cards = parsed.get("cards")
if not isinstance(cards, list) or not cards:
return False, "cards가 빈 배열"
for i, c in enumerate(cards):
if not isinstance(c, dict):
return False, f"cards[{i}] dict 아님"
for k in ("position", "card", "reversed", "interpretation", "advice", "evidence"):
if k not in c:
return False, f"cards[{i}].{k} 누락"
ev = c["evidence"]
if not isinstance(ev, dict):
return False, f"cards[{i}].evidence dict 아님"
for k in ("card_meaning_used", "position_logic", "category_lens"):
if k not in ev:
return False, f"cards[{i}].evidence.{k} 누락"
if not isinstance(ev[k], str) or not ev[k].strip():
return False, f"cards[{i}].evidence.{k} 빈 문자열"
interactions = parsed.get("interactions")
if not isinstance(interactions, list):
return False, "interactions가 list 아님"
if spread_type == "three_card" and len(interactions) == 0:
return False, "three_card는 interactions 1개 이상 필요"
return True, ""

View File

@@ -0,0 +1,75 @@
import pytest
from app.tarot.schema import validate_interpretation
def _valid_three():
return {
"summary": "S",
"cards": [
{"position": "과거", "card": "the-fool", "reversed": False,
"interpretation": "...", "advice": "a",
"evidence": {"card_meaning_used": "키워드", "position_logic": "p", "category_lens": "c"}},
{"position": "현재", "card": "the-lovers", "reversed": True,
"interpretation": "...", "advice": "a",
"evidence": {"card_meaning_used": "키워드", "position_logic": "p", "category_lens": "c"}},
{"position": "미래", "card": "ten-of-cups", "reversed": False,
"interpretation": "...", "advice": "a",
"evidence": {"card_meaning_used": "키워드", "position_logic": "p", "category_lens": "c"}},
],
"interactions": [{"type": "synergy", "between": ["the-fool", "ten-of-cups"], "explanation": "..."}],
"advice": "A",
"warning": None,
"confidence": "medium",
}
def test_valid_three_card_passes():
ok, msg = validate_interpretation(_valid_three(), "three_card")
assert ok, msg
def test_missing_evidence_fails():
bad = _valid_three()
del bad["cards"][0]["evidence"]
ok, msg = validate_interpretation(bad, "three_card")
assert not ok
assert "evidence" in msg
def test_empty_card_meaning_used_fails():
bad = _valid_three()
bad["cards"][0]["evidence"]["card_meaning_used"] = ""
ok, msg = validate_interpretation(bad, "three_card")
assert not ok
assert "card_meaning_used" in msg
def test_three_card_requires_interactions():
bad = _valid_three()
bad["interactions"] = []
ok, msg = validate_interpretation(bad, "three_card")
assert not ok
assert "interactions" in msg
def test_one_card_accepts_empty_interactions():
one = {
"summary": "S",
"cards": [{"position": "오늘", "card": "the-fool", "reversed": False,
"interpretation": "...", "advice": "a",
"evidence": {"card_meaning_used": "k", "position_logic": "p", "category_lens": "c"}}],
"interactions": [],
"advice": "A",
"warning": None,
"confidence": "high",
}
ok, msg = validate_interpretation(one, "one_card")
assert ok, msg
def test_invalid_confidence_fails():
bad = _valid_three()
bad["confidence"] = "very high"
ok, msg = validate_interpretation(bad, "three_card")
assert not ok