diff --git a/agent-office/app/tarot/schema.py b/agent-office/app/tarot/schema.py new file mode 100644 index 0000000..4f13331 --- /dev/null +++ b/agent-office/app/tarot/schema.py @@ -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, "" diff --git a/agent-office/tests/test_tarot_schema.py b/agent-office/tests/test_tarot_schema.py new file mode 100644 index 0000000..fadef30 --- /dev/null +++ b/agent-office/tests/test_tarot_schema.py @@ -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