"""issue #17 — renderable-aware provisional 선택 테스트. 계약: IMP-30 u1 provisional 합성이 rank-1 을 무조건 승격하던 것을, partial HTML 이 존재하는 첫 judgment(rank 순) 승격으로 변경 (IMP-95 u6 partial_exists 원칙). 전부 non-renderable 이면 기존대로 rank-1 (동작 보존). 실측 근거: mdx05 05-2 rank-1 F6(compensation_complaint_map, contract-only) 승격 → mapper FitError → verbatim 복구 → selected_frame_not_applied 라벨 → PARTIAL_COVERAGE. renderable rank-6 F23 선택 시 정상 경로. """ from __future__ import annotations import src.phase_z2_pipeline as _pz from src.phase_z2_pipeline import lookup_v4_match_with_fallback def _j(rank, template_id, frame_id, label, conf=0.5): return { "v4_full_rank": rank, "template_id": template_id, "frame_id": frame_id, "frame_number": rank, "confidence": conf, "label": label, } def _v4(judgments): return {"mdx_sections": {"S1": {"judgments_full32": judgments}}} def test_provisional_skips_contract_only_frames(monkeypatch): """rank1/2 는 partial 없음, rank3 존재(+verbatim builder) → rank3 승격.""" monkeypatch.setattr( _pz, "_b4_partial_exists", lambda tid: tid == "T_renderable", ) monkeypatch.setattr( _pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset({"T_renderable"}), ) v4 = _v4([ _j(1, "T_contract_only_a", "F1", "reject", 0.6), _j(2, "T_contract_only_b", "F2", "reject", 0.5), _j(3, "T_renderable", "F3", "reject", 0.4), ]) match, trace = lookup_v4_match_with_fallback( v4, "S1", allow_provisional=True, ) assert match is not None assert match.template_id == "T_renderable" assert match.provisional is True assert trace["selection_path"] == "provisional_renderable_rank_3" assert trace["selected_rank"] == 3 def test_provisional_rank1_when_renderable(monkeypatch): monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: True) monkeypatch.setattr( _pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset({"T_a", "T_b"}), ) v4 = _v4([ _j(1, "T_a", "F1", "reject", 0.6), _j(2, "T_b", "F2", "reject", 0.5), ]) match, trace = lookup_v4_match_with_fallback( v4, "S1", allow_provisional=True, ) assert match.template_id == "T_a" assert trace["selection_path"] == "provisional_rank_1" assert trace["selected_rank"] == 1 def test_provisional_all_non_renderable_keeps_rank1(monkeypatch): """전부 partial 없음 → 기존 동작 보존 (rank-1, 정직 라벨은 하류 게이트).""" monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: False) v4 = _v4([ _j(1, "T_a", "F1", "reject", 0.6), _j(2, "T_b", "F2", "reject", 0.5), ]) match, trace = lookup_v4_match_with_fallback( v4, "S1", allow_provisional=True, ) assert match.template_id == "T_a" assert trace["selection_path"] == "provisional_rank_1" def test_provisional_reject_without_verbatim_builder_not_promoted(monkeypatch): """renderable 이어도 reject + verbatim builder 미보유면 승격 금지 — mapper 네이티브 렌더의 원문 drop 위험 (mdx05 05-2 F23 1-atom 손실 실측). 조건 만족 후보 없음 → rank-1 유지 (verbatim 복구가 무손실 보장).""" monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: tid == "T_renderable") monkeypatch.setattr(_pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset()) v4 = _v4([ _j(1, "T_contract_only", "F1", "reject", 0.6), _j(2, "T_renderable", "F2", "reject", 0.4), ]) match, trace = lookup_v4_match_with_fallback( v4, "S1", allow_provisional=True, ) assert match.template_id == "T_contract_only" assert trace["selection_path"] == "provisional_rank_1" def test_provisional_non_reject_renderable_promoted_without_builder(monkeypatch): """light_edit 등 비-reject 라벨은 verbatim builder 없어도 승격 가능 (mapper 가 정상 fit 예상 — V4 판정 자체가 사용 가능 평가).""" monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: tid == "T_light") monkeypatch.setattr(_pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset()) v4 = _v4([ _j(1, "T_contract_only", "F1", "reject", 0.6), _j(2, "T_light", "F2", "light_edit", 0.5), ]) match, trace = lookup_v4_match_with_fallback( v4, "S1", allow_provisional=True, ) assert match.template_id == "T_light" # IMP-39 label-priority sort 로 light_edit 이 정렬 1위 → rank_1 경로. assert trace["selection_path"] == "provisional_rank_1"