Files
web-page-backend/saju-lab/tests/test_compatibility.py
gahusb f4f518fc80 feat(saju-lab): compatibility.py — 두 사주 궁합 점수 + breakdown
- saju-web/app/compatibility/result/page.tsx의 calculateCompatibility() 1:1 매핑
- 알고리즘: base 50 ± (일간 오행 same/produce/overcome) ± (일지 6합/3합/충), clamp [0,100]
- breakdown: day_master_element + branch_interaction (delta + relation/flags + description)
- 17 unit tests passed (헬퍼 9개 + 통합 8개), 438/438 total

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:14:47 +09:00

175 lines
6.3 KiB
Python

"""compatibility.py — 두 사주 궁합 점수 + breakdown 검증.
saju-web/app/compatibility/result/page.tsx의 calculateCompatibility() 로직과 1:1 매핑.
알고리즘:
score = 50 (base)
+ (same: +10 / produce|produced: +25 / overcome|overcomed: -10)
+ (six_harmony: +20)
+ (three_harmony: +15)
+ (conflict: -20)
clamp(0, 100)
"""
from app.calculator.core import calculate_saju
from app.calculator.compatibility import (
calculate_compatibility,
_get_element_relation,
_get_branch_relation,
)
# ─── 단위 헬퍼 테스트 ─────────────────────────────────────────────
def test_element_relation_same():
assert _get_element_relation("", "") == "same"
def test_element_relation_produce():
# 木 → 火 (목생화): el1 produces el2
assert _get_element_relation("", "") == "produce"
def test_element_relation_produced():
# 火 ← 木: el1 is produced by el2
assert _get_element_relation("", "") == "produced"
def test_element_relation_overcome():
# 金 → 木 (금극목): el1 overcomes el2
assert _get_element_relation("", "") == "overcome"
def test_element_relation_overcomed():
# 木 ← 金: el1 is overcomed by el2
assert _get_element_relation("", "") == "overcomed"
def test_branch_six_harmony():
# 寅亥 6합
rel = _get_branch_relation("", "")
assert rel["six_harmony"] is True
assert rel["conflict"] is False
def test_branch_three_harmony():
# 亥卯未 3합 — 亥+卯
rel = _get_branch_relation("", "")
assert rel["three_harmony"] is True
def test_branch_conflict():
# 子午 충
rel = _get_branch_relation("", "")
assert rel["conflict"] is True
assert rel["six_harmony"] is False
def test_branch_same_not_three_harmony():
# 같은 지지는 3합 아님 (서로 다른 지지여야 합)
rel = _get_branch_relation("", "")
assert rel["three_harmony"] is False
assert rel["six_harmony"] is False
assert rel["conflict"] is False
# ─── 통합: calculate_compatibility ────────────────────────────────
def test_score_range_0_to_100():
"""모든 결과가 0~100 범위 + breakdown 키 존재."""
a = calculate_saju(1990, 5, 15, 14, "male")
b = calculate_saju(1992, 8, 8, 18, "female")
result = calculate_compatibility(a, b)
assert 0 <= result["score"] <= 100
assert "breakdown" in result
bd = result["breakdown"]
assert "day_master_element" in bd
assert "branch_interaction" in bd
assert "base" in bd
assert bd["base"] == 50
def test_same_saju_same_element_no_branch_interaction():
"""1990-5-15 male+female (둘 다 辛未/金) — same element, 같은 일지(중립) → 50+10+0 = 60."""
a = calculate_saju(1990, 5, 15, 14, "male")
b = calculate_saju(1990, 5, 15, 14, "female")
result = calculate_compatibility(a, b)
assert result["score"] == 60
assert result["breakdown"]["day_master_element"]["relation"] == "same"
assert result["breakdown"]["day_master_element"]["score"] == 10
assert result["breakdown"]["branch_interaction"]["score"] == 0
def test_six_harmony_plus_produce_high_score():
"""1985-1-1 (辛卯/金) + 1989-7-10 (壬戌/水)
→ 金生水 (produce) +25, 卯戌 6합 +20 → 50+25+20 = 95.
"""
a = calculate_saju(1985, 1, 1, 12, "male")
b = calculate_saju(1989, 7, 10, 12, "male")
result = calculate_compatibility(a, b)
assert result["score"] == 95
bd = result["breakdown"]
assert bd["day_master_element"]["relation"] == "produce"
assert bd["day_master_element"]["score"] == 25
assert bd["branch_interaction"]["flags"]["six_harmony"] is True
assert bd["branch_interaction"]["score"] == 20
def test_conflict_plus_overcome_low_score():
"""1988-12-1 (辛巳/金) + 1992-3-21 (丁亥/火)
→ 火→金 (overcomed) -10, 巳亥 충 -20 → 50-10-20 = 20.
"""
a = calculate_saju(1988, 12, 1, 8, "male")
b = calculate_saju(1992, 3, 21, 12, "female")
result = calculate_compatibility(a, b)
assert result["score"] == 20
bd = result["breakdown"]
assert bd["day_master_element"]["relation"] in ("overcome", "overcomed")
assert bd["day_master_element"]["score"] == -10
assert bd["branch_interaction"]["flags"]["conflict"] is True
assert bd["branch_interaction"]["score"] == -20
def test_three_harmony_plus_overcome():
"""1990-5-15 (辛未/金) + 1992-3-21 (丁亥/火)
→ 火→金 (overcomed) -10, 亥未 3합 +15 → 50-10+15 = 55.
"""
a = calculate_saju(1990, 5, 15, 14, "male")
b = calculate_saju(1992, 3, 21, 12, "female")
result = calculate_compatibility(a, b)
assert result["score"] == 55
bd = result["breakdown"]
assert bd["day_master_element"]["score"] == -10
assert bd["branch_interaction"]["flags"]["three_harmony"] is True
assert bd["branch_interaction"]["score"] == 15
def test_score_clamp_at_zero():
"""극단 케이스: 상극(-10) + 충(-20) = 50-30 = 20. clamp 동작 검증을 위한 sanity."""
a = calculate_saju(1988, 12, 1, 8, "male")
b = calculate_saju(1992, 3, 21, 12, "female")
result = calculate_compatibility(a, b)
assert result["score"] >= 0
assert result["score"] <= 100
def test_breakdown_descriptions_non_empty():
"""breakdown의 description 필드가 비어있지 않음."""
a = calculate_saju(1990, 5, 15, 14, "male")
b = calculate_saju(1995, 8, 20, 12, "female")
result = calculate_compatibility(a, b)
bd = result["breakdown"]
assert isinstance(bd["day_master_element"]["description"], str)
assert len(bd["day_master_element"]["description"]) > 0
assert isinstance(bd["branch_interaction"]["description"], str)
assert len(bd["branch_interaction"]["description"]) > 0
def test_symmetry_score_only():
"""A vs B와 B vs A의 score는 동일 (saju-web과 동일하게 대칭).
note: relation 태그(produce vs produced)는 비대칭일 수 있으나
delta 점수는 동일하므로 최종 score는 항상 같다.
"""
a = calculate_saju(1985, 1, 1, 12, "male")
b = calculate_saju(1989, 7, 10, 12, "male")
r_ab = calculate_compatibility(a, b)
r_ba = calculate_compatibility(b, a)
assert r_ab["score"] == r_ba["score"]