Files
C.E.L_Slide_test2/tests/test_audit_frame_invariants_i1_i3.py
kyeongmin cacc5b30db feat(#85): IMP catalog builder invariant + VP runtime gate (u1~u7)
- u1: BuilderMissingError(FitError) — narrow exception aligned with pipeline catch
- u2: load_frame_contracts catalog invariant + VP skip + CatalogInvariantError
- u3a: audit CLI I1~I3 (partial existence / declared builder / registry membership)
- u3b: audit CLI I4 (slot_payload refs vs declared/generated payload keys)
- u4: lookup_v4_candidates VP filter (lookup_v4_all_judgments raw telemetry untouched)
- u5: catalog invariant regression coverage + temp non-VP failure fixtures
- u6: mdx04 VP routing fixture tests (sw_dependency_four_problems excluded from live)
- u7: tests/conftest.py env isolation + mdx03/mdx04/mdx05 subprocess smoke

Targeted 74 PASS (12.31s). Full regression 1063 PASS (87.70s). Audit CLI clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:56:38 +09:00

250 lines
7.7 KiB
Python

"""IMP-#85 u3a — Audit CLI invariants I1-I3.
Scope (Stage 2 lock):
I1 partial existence — `templates/phase_z2/families/{template_id}.html`
must exist for live (non-VP) contracts.
I2 builder declared — live contracts must declare non-empty
`payload.builder`.
I3 builder registered — declared builders must be in PAYLOAD_BUILDERS.
`visual_pending: true` skipped for all of I1-I3 (data-driven from catalog,
no hard-coded frame allow-list; matches u2 invariant scope).
Out of scope (별 axis):
- I4 slot_payload references (u3b).
- V4 runtime VP filter (u4).
- Implementing the 17 missing VP builders.
"""
from __future__ import annotations
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
SCRIPT_PATH = REPO_ROOT / "scripts" / "audit_frame_invariants.py"
def _write_yaml(path: Path, payload: dict) -> Path:
path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8")
return path
def _run_cli(catalog: Path, partials: Path) -> subprocess.CompletedProcess:
return subprocess.run(
[
sys.executable,
str(SCRIPT_PATH),
"--catalog",
str(catalog),
"--partials-dir",
str(partials),
],
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
)
def test_prod_catalog_audit_passes(tmp_path):
"""Prod catalog + prod partials dir → I1-I3 PASS (live contracts clean)."""
from scripts.audit_frame_invariants import (
DEFAULT_CATALOG_PATH,
DEFAULT_PARTIALS_DIR,
run_audit,
)
violations = run_audit(DEFAULT_CATALOG_PATH, DEFAULT_PARTIALS_DIR)
assert violations == [], (
"Prod live contracts (non-VP) must satisfy I1-I3 invariants. "
f"Got: {violations}"
)
def test_i1_partial_missing_for_live_contract(tmp_path):
"""Live contract without families/{template_id}.html → I1 violation."""
from src.phase_z2_mapper import PAYLOAD_BUILDERS
from scripts.audit_frame_invariants import check_i1_partial_existence
sample_builder = next(iter(PAYLOAD_BUILDERS.keys()))
catalog = {
"missing_partial_frame": {
"template_id": "missing_partial_frame",
"payload": {"builder": sample_builder},
},
}
partials_dir = tmp_path / "families"
partials_dir.mkdir()
violations = check_i1_partial_existence(catalog, partials_dir)
assert len(violations) == 1
assert "I1 partial-missing" in violations[0]
assert "missing_partial_frame" in violations[0]
def test_i1_partial_present_no_violation(tmp_path):
"""Live contract with partial on disk → no I1 violation."""
from scripts.audit_frame_invariants import check_i1_partial_existence
catalog = {
"ok_frame": {
"template_id": "ok_frame",
"payload": {"builder": "items_with_role"},
},
}
partials_dir = tmp_path / "families"
partials_dir.mkdir()
(partials_dir / "ok_frame.html").write_text("<div/>", encoding="utf-8")
assert check_i1_partial_existence(catalog, partials_dir) == []
def test_i1_skips_visual_pending(tmp_path):
"""visual_pending: true with no partial → I1 skip (no violation)."""
from scripts.audit_frame_invariants import check_i1_partial_existence
catalog = {
"vp_frame": {
"template_id": "vp_frame",
"visual_pending": True,
"payload": {"builder": "definitely_not_registered"},
},
}
partials_dir = tmp_path / "families"
partials_dir.mkdir()
assert check_i1_partial_existence(catalog, partials_dir) == []
def test_i2_missing_builder_field():
"""Live contract without payload.builder → I2 violation."""
from scripts.audit_frame_invariants import check_i2_builder_declared
catalog = {
"no_builder_frame": {
"template_id": "no_builder_frame",
"payload": {},
},
}
violations = check_i2_builder_declared(catalog)
assert len(violations) == 1
assert "I2 builder-undeclared" in violations[0]
assert "no_builder_frame" in violations[0]
def test_i2_skips_visual_pending():
"""visual_pending: true without builder → I2 skip."""
from scripts.audit_frame_invariants import check_i2_builder_declared
catalog = {
"vp_frame": {
"template_id": "vp_frame",
"visual_pending": True,
"payload": {},
},
}
assert check_i2_builder_declared(catalog) == []
def test_i3_unregistered_builder():
"""Live contract with unknown builder → I3 violation."""
from scripts.audit_frame_invariants import check_i3_builder_registered
catalog = {
"ghost_frame": {
"template_id": "ghost_frame",
"payload": {"builder": "ghost_builder_xyz"},
},
}
violations = check_i3_builder_registered(
catalog, registered_builders={"items_with_role"}
)
assert len(violations) == 1
assert "I3 builder-unregistered" in violations[0]
assert "ghost_frame" in violations[0]
assert "ghost_builder_xyz" in violations[0]
def test_i3_registered_builder_passes():
"""Live contract with registered builder → no I3 violation."""
from scripts.audit_frame_invariants import check_i3_builder_registered
catalog = {
"ok_frame": {
"template_id": "ok_frame",
"payload": {"builder": "items_with_role"},
},
}
assert check_i3_builder_registered(
catalog, registered_builders={"items_with_role"}
) == []
def test_i3_skips_visual_pending():
"""visual_pending: true with unregistered builder → I3 skip."""
from scripts.audit_frame_invariants import check_i3_builder_registered
catalog = {
"vp_frame": {
"template_id": "vp_frame",
"visual_pending": True,
"payload": {"builder": "vp_only_builder"},
},
}
assert check_i3_builder_registered(
catalog, registered_builders={"items_with_role"}
) == []
def test_cli_exit_zero_on_clean_catalog(tmp_path):
"""CLI exit code 0 + PASS line on clean (live) catalog."""
catalog_path = tmp_path / "catalog.yaml"
partials_dir = tmp_path / "families"
partials_dir.mkdir()
(partials_dir / "ok_frame.html").write_text("<div/>", encoding="utf-8")
_write_yaml(
catalog_path,
{
"ok_frame": {
"template_id": "ok_frame",
"payload": {"builder": "items_with_role"},
},
"vp_frame": {
"template_id": "vp_frame",
"visual_pending": True,
"payload": {"builder": "unregistered_xyz"},
},
},
)
result = _run_cli(catalog_path, partials_dir)
assert result.returncode == 0, result.stdout + result.stderr
assert "PASS" in result.stdout
def test_cli_exit_one_on_violations(tmp_path):
"""CLI exit code 1 + aggregated violations listed on drift."""
catalog_path = tmp_path / "catalog.yaml"
partials_dir = tmp_path / "families"
partials_dir.mkdir()
_write_yaml(
catalog_path,
{
"frame_a": {
"template_id": "frame_a",
"payload": {"builder": "ghost_builder"},
},
"frame_b": {
"template_id": "frame_b",
"payload": {},
},
},
)
result = _run_cli(catalog_path, partials_dir)
assert result.returncode == 1, result.stdout + result.stderr
assert "FAIL" in result.stdout
assert "frame_a" in result.stdout
assert "frame_b" in result.stdout
assert "I1" in result.stdout
assert "I2" in result.stdout or "I3" in result.stdout