"""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"