a28f12b12e
plan safety-library-1 B-1 PR① (fixture-first): - law.go.kr 라이브 fixture 5종 박제 (OC 새니타이즈 검증 — 응답 법령상세링크에 키 포함 함정) - R7-M3 판정: 전문 1콜 XML = 조문 853+별표 23 전체 스냅샷(부분 실패 개념 없음) + 별표번호/가지번호 = 구조화 필드 — 조문 취득 = 전문 1콜+로컬 파싱 확정(R2-m1) - legal_acts KR 시드 26행(법령ID 라이브 실측, watch=26 전부, FK 계열 9그룹) ★ '유해ㆍ위험작업...' 정식명 = 가운뎃점 — law_monitor 하드코딩(점 없음)은 영구 미매칭 잠복 - statute_adapters/kr.py: poll_changes(lawSearch MST diff) — 순수 파서 분리, fixture 테스트 8/8 - statute_collector.py: 관찰 전용 코어(워터마크 영속 0 — ingest=PR②), 스케줄 미등록(R8-B1) - main.py: law_monitor 스케줄 제거 — 버전 체인 밖 레거시 매일 증식의 유일 경로 차단 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
3.7 KiB
Python
90 lines
3.7 KiB
Python
"""B-1 PR① — KR 어댑터 순수 파서 fixture 테스트 (plan safety-library-1).
|
|
|
|
fixture = 2026-06-13 law.go.kr 라이브 박제 (OC 새니타이즈, tests/fixtures/statute_kr/).
|
|
파서는 순수 함수라 httpx/DB 불요 — 컨테이너 밖 로컬 실행.
|
|
"""
|
|
|
|
import gzip
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "app"))
|
|
|
|
from workers.statute_adapters import ChangeEvent # noqa: E402
|
|
from workers.statute_adapters.kr import detect_change, parse_search_hit # noqa: E402
|
|
|
|
FIX = Path(__file__).parent / "fixtures" / "statute_kr"
|
|
|
|
|
|
def _read(name: str) -> str:
|
|
p = FIX / name
|
|
if name.endswith(".gz"):
|
|
return gzip.decompress(p.read_bytes()).decode("utf-8")
|
|
return p.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_parse_search_hit_exact_match():
|
|
hit = parse_search_hit(_read("lawsearch_sanab.xml"), "산업안전보건법")
|
|
assert hit is not None
|
|
assert hit["law_id"] == "001766"
|
|
assert hit["mst"] == "283449"
|
|
assert hit["promulgation_date"] == "20260219"
|
|
assert hit["effective_date"] == "20260601"
|
|
assert hit["status_code"] == "현행"
|
|
|
|
|
|
def test_parse_search_hit_rejects_partial_name():
|
|
# totalCnt 3 인 응답에서 '산업안전보건법 시행령' 등 부분 일치는 비매칭이어야 함
|
|
hit = parse_search_hit(_read("lawsearch_sanab.xml"), "산업안전보건")
|
|
assert hit is None
|
|
|
|
|
|
def test_detect_change_same_watermark_is_silent():
|
|
hit = parse_search_hit(_read("lawsearch_sanab.xml"), "산업안전보건법")
|
|
assert detect_change(hit, "kr-law:001766", "산업안전보건법", watermark="283449") is None
|
|
|
|
|
|
def test_detect_change_new_mst_is_amend():
|
|
hit = parse_search_hit(_read("lawsearch_sanab.xml"), "산업안전보건법")
|
|
ev = detect_change(hit, "kr-law:001766", "산업안전보건법", watermark="283448")
|
|
assert isinstance(ev, ChangeEvent)
|
|
assert ev.kind == "amend"
|
|
assert ev.new_version_key == "283449"
|
|
assert ev.effective_date == "20260601"
|
|
|
|
|
|
def test_detect_change_empty_watermark_is_amend():
|
|
# 첫 폴링(워터마크 부재) = 변경으로 감지 — PR② 부트스트랩 전 관찰 모드의 기대 동작
|
|
hit = parse_search_hit(_read("lawsearch_sanab.xml"), "산업안전보건법")
|
|
ev = detect_change(hit, "kr-law:001766", "산업안전보건법", watermark=None)
|
|
assert ev is not None and ev.kind == "amend"
|
|
|
|
|
|
def test_detect_change_repeal_keyword():
|
|
hit = {"mst": "9", "revision_type": "폐지", "promulgation_date": None,
|
|
"effective_date": None, "law_id": "x", "status_code": None}
|
|
ev = detect_change(hit, "kr-law:x", "x", watermark="1")
|
|
assert ev is not None and ev.kind == "repeal"
|
|
|
|
|
|
def test_lawservice_snapshot_semantics_rule():
|
|
"""R7-M3 판정 박제: 전문 1콜 XML = 조문+별표 전체 스냅샷 (PR② payload 계약의 전제)."""
|
|
root = ET.fromstring(_read("lawservice_rule.xml.gz"))
|
|
articles = root.findall(".//조문단위")
|
|
annexes = root.findall(".//별표단위")
|
|
assert len(articles) >= 800, "산안기준규칙 조문 853 기대 — 전문 1콜 판정 근거"
|
|
assert len(annexes) == 23, "별표 23 전부 본문 XML 포함 = 스냅샷 의미론"
|
|
# R7-M3 ②: 별표 식별 = 구조화 필드 (suffix 문자열 파싱 불요)
|
|
first = annexes[0]
|
|
assert first.findtext("별표번호") is not None
|
|
assert first.findtext("별표가지번호") is not None
|
|
|
|
|
|
def test_lawservice_sanab_basic_info():
|
|
root = ET.fromstring(_read("lawservice_sanab.xml.gz"))
|
|
assert root.findtext(".//법령ID") == "001766"
|
|
assert len(root.findall(".//조문단위")) >= 200
|
|
# 별표 없는 법령 = 별표단위 0 (스냅샷 의미론의 반대쪽 케이스)
|
|
assert len(root.findall(".//별표단위")) == 0
|