"""39 케이스 커버리지 갭 분석 단위테스트.""" from __future__ import annotations from dataclasses import dataclass from app.engine.case_coverage import analyze_coverage @dataclass class _Case: case_id: str subgroup: str actor: str detectable_internal: bool _CASES = [ _Case("A1", "A-1", "저자(가해)", True), _Case("A2", "A-1", "저자(가해)", True), _Case("C1", "C", "플랫폼", False), # 탐지 범위 밖 ] def test_no_data_all_detectable_need_data(): rep = analyze_coverage(_CASES, None) assert rep["detectable_cases"] == 2 assert rep["need_data_cases"] == 2 assert rep["covered_cases"] == 0 assert rep["coverage_ratio"] == 0.0 def test_with_samples_marks_covered(): rep = analyze_coverage(_CASES, {"A1": 10}) assert rep["covered_cases"] == 1 assert rep["need_data_cases"] == 1 assert rep["coverage_ratio"] == 0.5 def test_out_of_scope_excluded_from_request(): rep = analyze_coverage(_CASES, {"A1": 5, "A2": 5}) ids = {x["case_id"] for x in rep["data_request_list"]} assert "C1" not in ids assert rep["need_data_cases"] == 0 # --- 태그 → 케이스 도달 범위 회귀 (v1.3) --------------------------------- from pathlib import Path from app.engine.taxonomy import load_taxonomy ROOT = Path(__file__).resolve().parents[1] #: _assign_tags 가 실제로 낼 수 있는 주 태그 조합 전부. #: 이 목록이 곧 케이스 도달 범위의 상한이므로 조합이 바뀌면 여기도 바뀌어야 한다. _EMITTED_PRIMARY = [ ["reproduction", "citation_missing"], ["reproduction", "derivative_work"], ["derivative_work", "citation_missing"], ["derivative_work", "substandard_derivative"], ["reproduction"], ] def _reachable(): tax = load_taxonomy(ROOT / "data/taxonomy") return {c.case_id for combo in _EMITTED_PRIMARY for c in tax.find_cases(combo)} def test_reachable_cases_cover_most_technical_detection_cases(): """v1.3 ● 19건 중 16건이 태그만으로 후보에 오른다. 남은 B3·C1·D1 은 게재 사실·편집 개입·성명표시처럼 텍스트에서 알 수 없는 사실이 필요해 LegalContext 없이는 도달할 수 없다. 이 경계를 고정한다. """ tax = load_taxonomy(ROOT / "data/taxonomy") detectable = {c.case_id for c in tax.cases if c.detectable_internal} reachable = _reachable() assert detectable - reachable == {"B3", "C1", "D1"} assert len(reachable & detectable) == 16 def test_no_tie_group_is_silently_collapsed(): """동점군이 2건 이상이면 전부 반환된다 — 하나만 남으면 나머지가 사장된다.""" tax = load_taxonomy(ROOT / "data/taxonomy") assert len(tax.find_cases(["reproduction", "citation_missing"])) == 7 assert len(tax.find_cases(["derivative_work", "substandard_derivative"])) == 2