Files
hyungi 8a3bea6b31 feat(safety): C-1 후속 — version_status decorate + facets 집계
검색 엔드포인트 wrapper decoration(run_search 코어 무접촉·ranking 무관):
- version_status: 법령 결과(material_type=law)에 legal_meta.version_status 부착
  (decorate_version_status, law 무결과 시 query skip). SearchResult.version_status 신설.
- facets=true: top-K 결과 분류 축(material_type/jurisdiction/version_status) 분포 라벨
  (compute_facets). 미요청=None(byte 불변). SearchResponse.facets 신설.
- result_decorate.py 신설. 단위 4건.
freshness incident 변경(law_365d 제거+흡수)=ranking 변경이라 별 슬라이스 defer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:07:57 +09:00

58 lines
2.0 KiB
Python

"""C-1 후속 — facets 집계 + version_status decorate 순수 로직 테스트.
version_status 의 실제 legal_meta 조회는 GPU 라이브(법령 검색)로 검증 — 여기선 facets 분포
계약 + decorate 의 law 무결과 skip 경로(DB 미접촉)만.
"""
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "app"))
from services.search.result_decorate import ( # noqa: E402
compute_facets,
decorate_version_status,
)
class _R:
"""SearchResult 흉내 — 분류 축 속성만."""
def __init__(self, material_type=None, jurisdiction=None, version_status=None, id=1):
self.material_type = material_type
self.jurisdiction = jurisdiction
self.version_status = version_status
self.id = id
def test_compute_facets_distribution():
results = [
_R("law", "KR", "current"),
_R("law", "KR", "superseded"),
_R("incident", "KR", None),
_R("paper", None, None),
]
f = compute_facets(results)
assert f["material_type"] == {"law": 2, "incident": 1, "paper": 1}
assert f["jurisdiction"] == {"KR": 3} # paper jurisdiction None 제외
assert f["version_status"] == {"current": 1, "superseded": 1} # None 제외
def test_compute_facets_empty_and_all_none():
assert compute_facets([]) == {}
assert compute_facets([_R(), _R()]) == {} # 모든 축 None → 빈 축 미포함
def test_compute_facets_excludes_empty_axes():
f = compute_facets([_R(jurisdiction="US"), _R(jurisdiction="EU")])
assert f == {"jurisdiction": {"US": 1, "EU": 1}}
assert "material_type" not in f
def test_decorate_version_status_skips_without_law():
# law 결과 없으면 legal_meta 조회 skip → session 미사용(None 으로도 무오류)
results = [_R("incident", "KR"), _R("paper")]
asyncio.run(decorate_version_status(None, results))
assert all(r.version_status is None for r in results)