"""IMP-38 U3 regression — call site cleanup (max_rank=3 제거) 후 policy 활성 검증. Scenarios: (A) normal case: rank 1~default_max_rank window 에 usable candidate 충분 → effective_max_rank=default_max_rank (rank-3-preserved) → mdx03 식: rank 1 use_as_is 매칭 정상 case 보호 확인 (B) extended case: rank 1~default_max_rank window 에 usable candidate 0 → effective_max_rank=effective_extended_ceiling (rank-extended) → mdx05-2 식: rank 1~9 미등록/reject + rank 10+ 등록 frame case 처리 4 round 합의 (#67): - Codex #1: 별 yaml + loader (catalog 오염 방지) - Codex #2: min(configured, len(judgments)) 정정 - Codex #6: 2 call site cleanup (HEAD 기준 — IMP-47B 가 추가한 3 번째는 별 axis) - Codex #7: U3 execute ready """ from __future__ import annotations import pytest @pytest.fixture(autouse=True) def _reset_policy_cache(): """Reset module-level _V4_FALLBACK_POLICY_CACHE for test isolation.""" import src.phase_z2_mapper as mapper mapper._V4_FALLBACK_POLICY_CACHE = None yield mapper._V4_FALLBACK_POLICY_CACHE = None def _make_v4_section(judgments: list[dict]) -> dict: return {"mdx_sections": {"sec-1": {"judgments_full32": judgments}}} def _judgment(template_id: str, label: str, confidence: float = 0.5, frame_id: int = 0) -> dict: return { "template_id": template_id, "frame_id": frame_id or (hash(template_id) % 10000), "frame_number": 0, "confidence": confidence, "label": label, } # ─── Scenario A — normal case (rank-3-preserved) ────────────────── def test_normal_case_with_usable_candidates_preserves_default_max_rank(): """rank 1~3 window 에 usable >= threshold(1) 시 effective_max_rank=default_max_rank(3).""" from src.phase_z2_pipeline import lookup_v4_match_with_fallback from src.phase_z2_mapper import load_frame_contracts # mdx03 식 — 첫 rank 가 catalog 등록 + use_as_is/light_edit/restructure(allowed) # 실제 catalog 등록 frame 사용 (catalog hardcode 의존 — 단 frame 32 중 어느 게 등록인지는 yaml 기반) catalog = load_frame_contracts() registered_template_ids = [k for k, v in catalog.items() if isinstance(v, dict)] assert len(registered_template_ids) >= 1, "catalog 등록 frame 1+ 필요 (mdx03 식 fixture)" # rank 1 = registered frame + use_as_is (auto-renderable) # rank 2~3 = reject (catalog 등록 무관) first_registered = registered_template_ids[0] judgments = [ _judgment(first_registered, "use_as_is", 0.95), _judgment("dummy_rank2", "reject", 0.3), _judgment("dummy_rank3", "reject", 0.2), ] v4 = _make_v4_section(judgments) _match, trace = lookup_v4_match_with_fallback(v4, "sec-1") # no explicit max_rank → policy assert trace["policy_applied"] == "default_max_rank", ( f"normal case 에서 default 유지 기대, got {trace['policy_applied']}" ) assert trace["effective_max_rank"] == trace["default_max_rank"] assert trace["usable_count"] >= 1 # ─── Scenario B — extended case (rank-extended) ──────────────────── def test_extended_case_with_no_usable_in_default_window_expands_to_ceiling(): """rank 1~3 window 에 0 usable 시 effective_max_rank=effective_extended_ceiling.""" from src.phase_z2_pipeline import lookup_v4_match_with_fallback # mdx05-2 식 — rank 1~3 미등록 (template_id 가 catalog 에 없음) + reject 라벨 # rank 4~ 도 등록 안 됨 (fixture 단순화) # 다만 judgments_count=10 으로 충분 → effective_extended_ceiling = min(extended, 10) = 10 judgments = [ _judgment(f"unregistered_t{i}", "reject", 0.1 + i * 0.01) for i in range(10) ] v4 = _make_v4_section(judgments) _match, trace = lookup_v4_match_with_fallback(v4, "sec-1") assert trace["policy_applied"] == "extended_max_rank", ( f"extended case 기대, got {trace['policy_applied']}" ) assert trace["usable_count"] == 0 assert trace["judgments_count"] == 10 # Codex #2 정정: min(configured, 10) — configured 32 면 10, 5 면 5 assert trace["effective_extended_ceiling"] == min( trace["configured_extended_max_rank"], 10 ) assert trace["effective_max_rank"] == trace["effective_extended_ceiling"] # ─── Scenario C — call site cleanup byte-identical (caller_override 제거 후 policy 활성) ─ def test_default_call_site_now_uses_policy_after_cleanup(): """U3 cleanup 후 call site = no explicit max_rank → policy path 자동 활성. 이전: caller 가 max_rank=3 명시 → policy_applied=caller_override U3 후: caller 가 명시 X → policy_applied=default_max_rank (usable >= 1 시) or extended_max_rank """ from src.phase_z2_pipeline import lookup_v4_match_with_fallback judgments = [_judgment(f"unregistered_t{i}", "reject") for i in range(5)] v4 = _make_v4_section(judgments) # caller 가 max_rank 명시 X (U3 cleanup 후 production caller 의 새 동작) _match, trace = lookup_v4_match_with_fallback(v4, "sec-1") assert trace["policy_applied"] in {"default_max_rank", "extended_max_rank"} assert trace["policy_applied"] != "caller_override", ( "U3 cleanup 후 production caller = no explicit, policy path 활성 기대" ) # ─── Scenario D — explicit caller_override 여전히 동작 (test path 보호) ──── def test_explicit_caller_override_still_works_for_tests(): """test 에서 explicit max_rank=N 보낼 시 caller_override 그대로 동작 (backward compat).""" from src.phase_z2_pipeline import lookup_v4_match_with_fallback judgments = [_judgment(f"unregistered_t{i}", "reject") for i in range(10)] v4 = _make_v4_section(judgments) _match, trace = lookup_v4_match_with_fallback(v4, "sec-1", max_rank=5) assert trace["policy_applied"] == "caller_override" assert trace["effective_max_rank"] == 5