Files
ExperionCrawler/mcp-server/verifier/test_validators.py

155 lines
7.5 KiB
Python

"""Phase B Verifier MVP — 단위 테스트 (최소 10케이스)."""
from __future__ import annotations
import json, pytest
from verifier import validators
# ── 헬퍼 ──
def _noop_conn():
return None # _load_tag_set 내부에서 호출되지 않도록 monkeypatch로 대체
# ═══════════════════════════════════════════════════════════════════
# R1 — tag-existence
# ═══════════════════════════════════════════════════════════════════
def test_R1_valid_tag_format_pass(monkeypatch):
"""PASS: ficq-6113.pv → 올바른 형식 + DB 존재."""
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"ficq-6113", "p-6102"})
assert validators.validate_tag("ficq-6113.pv", _noop_conn) is None
def test_R1_valid_tag_no_suffix_pass(monkeypatch):
"""PASS: p-6102 → suffix 없어도 OK."""
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"ficq-6113", "p-6102"})
assert validators.validate_tag("p-6102", _noop_conn) is None
def test_R1_tags_dot_suffix_pass(monkeypatch):
"""PASS: vp-6117a.pv → 문자 포함 태그 번호."""
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"vp-6117a", "vp-6117b"})
assert validators.validate_tag("vp-6117a.pv", _noop_conn) is None
def test_R1_invalid_tag_format_fail():
"""FAIL: raw_material_input → 형식 위반 (대문자 + 밑줄)."""
err = validators.validate_tag("raw_material_input", _noop_conn)
assert err is not None
assert err.code == "invalid_tag_format"
assert "R1" in err.rule
def test_R1_unknown_tag_fail(monkeypatch):
"""FAIL: RM-6101 → 형식은 맞지만 DB에 없음."""
monkeypatch.setattr(validators, "_load_tag_set", lambda gc: {"ficq-6113", "p-6102"})
err = validators.validate_tag("RM-6101", _noop_conn)
assert err is not None
assert err.code == "tag_not_found"
assert "suggested" in err.extra
def test_R1_none_or_empty_pass():
"""PASS: None/빈 문자열 → 검증 스킵."""
assert validators.validate_tag(None, _noop_conn) is None
assert validators.validate_tag("", _noop_conn) is None
# ═══════════════════════════════════════════════════════════════════
# R2 — area-format
# ═══════════════════════════════════════════════════════════════════
def test_R2_valid_area_pass():
"""PASS: P6, P6-1, P10, UTIL."""
for a in ("P6", "P6-1", "P10", "UTIL", "PACKING"):
assert validators.validate_area(a) is None, f"area={a} expected PASS"
def test_R2_invalid_area_format_fail():
"""FAIL: 6-1 → P prefix 없음."""
err = validators.validate_area("6-1")
assert err is not None
assert err.code == "invalid_area_format"
def test_R2_unknown_area_fail():
"""FAIL: P7 → 존재하지 않는 area."""
err = validators.validate_area("P7")
assert err is not None
assert err.code == "unknown_area"
def test_R2_empty_or_none_pass():
"""PASS: None/빈 문자열 → 선택 필터."""
assert validators.validate_area(None) is None
assert validators.validate_area("") is None
def test_R2_sub_area_pass():
"""PASS: P6-2 → 형식+base 모두 유효."""
assert validators.validate_area("P6-2") is None
# ═══════════════════════════════════════════════════════════════════
# R4 — trace_connections 보강
# ═══════════════════════════════════════════════════════════════════
def test_R4_valid_direction_pass():
"""PASS: upstream, downstream."""
assert validators.validate_direction("upstream") is None
assert validators.validate_direction("downstream") is None
assert validators.validate_direction(None) is None
def test_R4_invalid_direction_fail():
"""FAIL: sideways → 허용되지 않은 방향."""
err = validators.validate_direction("sideways")
assert err is not None
assert err.code == "invalid_direction"
def test_R4_valid_max_depth_pass():
"""PASS: 1, 50, 20."""
for n in (1, 50, 20, None):
assert validators.validate_max_depth(n) is None, f"max_depth={n} expected PASS"
def test_R4_invalid_max_depth_out_of_range():
"""FAIL: 0, 51 → 범위 초과."""
err0 = validators.validate_max_depth(0)
assert err0 is not None and err0.code == "max_depth_out_of_range"
err51 = validators.validate_max_depth(51)
assert err51 is not None and err51.code == "max_depth_out_of_range"
def test_R4_invalid_max_depth_not_int():
"""FAIL: 'abc' → 정수 아님."""
err = validators.validate_max_depth("abc")
assert err is not None and err.code == "invalid_max_depth"
# ═══════════════════════════════════════════════════════════════════
# R3 / R5 stub
# ═══════════════════════════════════════════════════════════════════
def test_R3_stub_returns_none():
"""Phase B.2 전까지는 항상 None."""
assert validators.validate_response_text("anything") is None
# ═══════════════════════════════════════════════════════════════════
# VerifierError 직렬화
# ═══════════════════════════════════════════════════════════════════
def test_verifier_error_to_dict():
err = validators.VerifierError("R1", "tag_not_found", "hint msg", suggested=["a", "b"])
d = err.to_dict()
assert d["verifier_error"] == "R1.tag_not_found"
assert d["hint"] == "hint msg"
assert d["suggested"] == ["a", "b"]
# ═══════════════════════════════════════════════════════════════════
# log_rejection — 파일 적재
# ═══════════════════════════════════════════════════════════════════
def test_log_rejection_writes_jsonl(tmp_path, monkeypatch):
monkeypatch.setattr(validators, "_LOG_DIR", tmp_path)
err = validators.VerifierError("R2", "invalid_area_format", "bad area", valid_areas=["P6"])
validators.log_rejection("find_tags", {"area": "6-1"}, err)
files = list(tmp_path.iterdir())
assert len(files) == 1
content = files[0].read_text(encoding="utf-8")
rec = json.loads(content)
assert rec["tool"] == "find_tags"
assert rec["params"] == {"area": "6-1"}
assert rec["verifier_error"]["verifier_error"] == "R2.invalid_area_format"