feat(#67): IMP-38 V4 max_rank policy formalization (u1~u3, 4 round consensus)
- u1: separate templates/phase_z2/catalog/v4_fallback_policy.yaml + load_v4_fallback_policy() loader (catalog pollution prevention — Codex #1 correction) - u2: dynamic effective max_rank in lookup_v4_match_with_fallback (3-variable ceiling min, Codex #2 correction: min(configured, len(judgments_full32))) + 3-tier usable predicate (status + catalog + optional capacity) + trace 8 fields (requested/default/configured_extended/ judgments_count/effective_extended_ceiling/effective_max_rank/usable_count/policy_applied) - u3: 2 production call site cleanup (max_rank=3 removed, HEAD baseline) + tracked Front/vite.config.ts PHASE_Z_MAX_RANK env retired + 4 regression scenarios verified: 32 passed (IMP-38 focused scope) — IMP-05 L4 dedup / L2 schema preserved, IMP-30 allow_provisional byte-identical, caller_override backward compat (tests) Stage cycle (#67, 7 round Claude + 5 round Codex): - Stage 1: Claude #1 -> Codex #1 YES + 5 corrections - Stage 2 r1+r2: Claude #2-#4 -> Codex #2 Q2 -> Codex #3 YES (4 round consensus LOCK 23195) - Stage 3 U1+U2+U3: Claude #5-#9 -> Codex #6 NO 4to3 correction -> Codex #7 YES -> Codex #8 YES - Stage 4: Claude #11 -> Codex #9 (anchor attribution nuance) -> Codex #10 readiness -> Codex #11 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
109
tests/test_v4_fallback_policy_loader.py
Normal file
109
tests/test_v4_fallback_policy_loader.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""IMP-38 U1 — v4_fallback_policy.yaml loader test.
|
||||
|
||||
Verify:
|
||||
- load_v4_fallback_policy() returns dict with expected keys
|
||||
- yaml parsed correctly (usable_threshold, default_max_rank, extended_max_rank, policy_type)
|
||||
- graceful fallback when yaml missing → _V4_FALLBACK_POLICY_DEFAULT
|
||||
- _V4_FALLBACK_POLICY_CACHE pattern (lazy load, mirror of _CATALOG_CACHE)
|
||||
- load_frame_contracts() shape unchanged (separate yaml, catalog 오염 X)
|
||||
|
||||
4 round 합의 (#67):
|
||||
- Codex #1: separate yaml (not frame_contracts.yaml top-level)
|
||||
- Codex #3: load_frame_contracts() shape 변경 X
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
V4_POLICY_PATH = PROJECT_ROOT / "templates" / "phase_z2" / "catalog" / "v4_fallback_policy.yaml"
|
||||
CATALOG_PATH = PROJECT_ROOT / "templates" / "phase_z2" / "catalog" / "frame_contracts.yaml"
|
||||
|
||||
|
||||
def _reset_caches():
|
||||
"""Reset module-level caches for test isolation."""
|
||||
import src.phase_z2_mapper as mapper
|
||||
mapper._V4_FALLBACK_POLICY_CACHE = None
|
||||
mapper._CATALOG_CACHE = None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_caches():
|
||||
_reset_caches()
|
||||
yield
|
||||
_reset_caches()
|
||||
|
||||
|
||||
def test_v4_fallback_policy_yaml_exists():
|
||||
"""IMP-38 U1 — separate yaml file must exist."""
|
||||
assert V4_POLICY_PATH.exists(), (
|
||||
f"v4_fallback_policy.yaml not found at {V4_POLICY_PATH}. "
|
||||
"IMP-38 U1 expects separate yaml (Codex #1 corr — not frame_contracts.yaml top-level)."
|
||||
)
|
||||
|
||||
|
||||
def test_load_v4_fallback_policy_returns_dict_with_expected_keys():
|
||||
"""load_v4_fallback_policy() must return dict with policy keys."""
|
||||
from src.phase_z2_mapper import load_v4_fallback_policy
|
||||
policy = load_v4_fallback_policy()
|
||||
assert isinstance(policy, dict)
|
||||
expected_keys = {"policy_type", "usable_threshold", "default_max_rank", "extended_max_rank"}
|
||||
missing = expected_keys - set(policy.keys())
|
||||
assert not missing, f"missing keys in v4_fallback_policy: {missing}"
|
||||
|
||||
|
||||
def test_load_v4_fallback_policy_values_match_yaml():
|
||||
"""Loaded policy values must match v4_fallback_policy.yaml (initial commit)."""
|
||||
from src.phase_z2_mapper import load_v4_fallback_policy
|
||||
policy = load_v4_fallback_policy()
|
||||
assert policy["policy_type"] == "dynamic_usable_count_based"
|
||||
assert policy["usable_threshold"] == 1
|
||||
assert policy["default_max_rank"] == 3
|
||||
assert policy["extended_max_rank"] == 32
|
||||
|
||||
|
||||
def test_load_v4_fallback_policy_cache_pattern():
|
||||
"""_V4_FALLBACK_POLICY_CACHE pattern — second call returns same dict (lazy load)."""
|
||||
from src.phase_z2_mapper import load_v4_fallback_policy
|
||||
policy_a = load_v4_fallback_policy()
|
||||
policy_b = load_v4_fallback_policy()
|
||||
assert policy_a is policy_b, "cache pattern violated (should return same dict instance)"
|
||||
|
||||
|
||||
def test_load_v4_fallback_policy_graceful_when_yaml_missing():
|
||||
"""yaml 파일 없을 시 → _V4_FALLBACK_POLICY_DEFAULT (extended_max_rank=3, byte-identical pre-IMP-38)."""
|
||||
import src.phase_z2_mapper as mapper
|
||||
with patch.object(mapper, "V4_FALLBACK_POLICY_PATH", PROJECT_ROOT / "tests" / "__nonexistent_policy.yaml"):
|
||||
# reset cache to force reload via patched path
|
||||
mapper._V4_FALLBACK_POLICY_CACHE = None
|
||||
policy = mapper.load_v4_fallback_policy()
|
||||
assert policy["default_max_rank"] == 3
|
||||
assert policy["extended_max_rank"] == 3, (
|
||||
"graceful fallback must keep extended==default (byte-identical pre-IMP-38)"
|
||||
)
|
||||
|
||||
|
||||
def test_load_frame_contracts_shape_unchanged():
|
||||
"""Codex #3 LOCK — load_frame_contracts() must still return template_id → entry dict."""
|
||||
from src.phase_z2_mapper import load_frame_contracts, load_v4_fallback_policy
|
||||
catalog = load_frame_contracts()
|
||||
policy = load_v4_fallback_policy()
|
||||
|
||||
# catalog 의 key 가 모두 frame entry (dict with template_id/frame_id) 여야 함
|
||||
for key, entry in catalog.items():
|
||||
assert isinstance(entry, dict), f"catalog entry {key} should be dict"
|
||||
assert "template_id" in entry, f"catalog entry {key} missing template_id (policy bleed?)"
|
||||
|
||||
# policy keys 는 catalog 에 안 들어감
|
||||
policy_keys = {"policy_type", "usable_threshold", "default_max_rank", "extended_max_rank"}
|
||||
catalog_top_keys = set(catalog.keys())
|
||||
bleed = policy_keys & catalog_top_keys
|
||||
assert not bleed, (
|
||||
f"policy keys leaked into frame_contracts.yaml: {bleed}. "
|
||||
"Codex #1 corr violated — policy must stay in separate v4_fallback_policy.yaml."
|
||||
)
|
||||
Reference in New Issue
Block a user