- 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>
250 lines
7.7 KiB
Python
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
|