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>
This commit is contained in:
127
tests/test_pipeline_smoke_imp85.py
Normal file
127
tests/test_pipeline_smoke_imp85.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""IMP-#85 u7 — subprocess smoke for mdx03 / mdx04 / mdx05 pipeline runs.
|
||||
|
||||
These smokes exercise the IMP-#85 catalog ↔ contract ↔ builder
|
||||
invariant + runtime VP gate end-to-end against real MDX inputs:
|
||||
|
||||
* mdx03 — non-VP rank-1 path stays clean (exit 0).
|
||||
* mdx04 — the original IMP-#85 hard-crash signature
|
||||
(``BuilderMissingError ... PAYLOAD_BUILDERS has no such entry``)
|
||||
is GONE. u1 converted the uncaught ``ValueError`` into a
|
||||
``BuilderMissingError(FitError)`` subclass; the pipeline's
|
||||
existing ``except FitError`` at ``src/phase_z2_pipeline.py:4436``
|
||||
catches it and the zone is routed to
|
||||
``adapter_needed (skip render)``. Anything that crashes
|
||||
*downstream* of that routing (e.g. layout_css zone aggregation
|
||||
when all live zones are adapter_needed) is a separate axis and
|
||||
out of scope for this issue (see follow_up_issue_candidates).
|
||||
* mdx05 — non-VP rank-1 path stays clean (exit 0).
|
||||
|
||||
Each subprocess gets a unique run_id so the runs do not collide on
|
||||
disk when pytest is invoked concurrently or with -x retry.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SAMPLES_DIR = REPO_ROOT / "samples" / "mdx_batch"
|
||||
|
||||
# Original IMP-#85 crash signature (issue body verbatim). u1 converted
|
||||
# the uncaught ``ValueError`` raised from the mapper's missing-builder
|
||||
# branch into a ``BuilderMissingError(FitError)`` subclass that the
|
||||
# pipeline catches. The string below was the marker of the uncaught
|
||||
# propagation; it must no longer appear in stdout/stderr of a mdx04
|
||||
# subprocess run.
|
||||
IMP85_OLD_CRASH_MARKER = "PAYLOAD_BUILDERS has no such entry"
|
||||
|
||||
|
||||
def _run_pipeline(mdx_name: str, run_id: str, timeout: int = 240) -> subprocess.CompletedProcess:
|
||||
"""Spawn ``python -m src.phase_z2_pipeline <mdx> <run_id>`` and capture I/O."""
|
||||
return subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"src.phase_z2_pipeline",
|
||||
str(SAMPLES_DIR / mdx_name),
|
||||
run_id,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
cwd=str(REPO_ROOT),
|
||||
)
|
||||
|
||||
|
||||
def _unique_run_id(prefix: str) -> str:
|
||||
return f"{prefix}_imp85_smoke_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mdx_name,prefix",
|
||||
[
|
||||
("03.mdx", "mdx03"),
|
||||
("05.mdx", "mdx05"),
|
||||
],
|
||||
)
|
||||
def test_non_vp_smoke_runs_clean(mdx_name: str, prefix: str) -> None:
|
||||
"""mdx03 / mdx05 hit non-VP rank-1 frames; the pipeline runs to exit 0.
|
||||
|
||||
Non-VP rank-1 selection is the normal Phase Z path and the
|
||||
primary regression guard that u1-u6 do not perturb mapper /
|
||||
pipeline behaviour for non-VP routes.
|
||||
"""
|
||||
cp = _run_pipeline(mdx_name, _unique_run_id(prefix))
|
||||
assert cp.returncode == 0, (
|
||||
f"{mdx_name} pipeline returncode={cp.returncode}\n"
|
||||
f"--- stderr tail ---\n{cp.stderr[-1500:]}\n"
|
||||
f"--- stdout tail ---\n{cp.stdout[-1500:]}"
|
||||
)
|
||||
|
||||
|
||||
def test_mdx04_no_longer_emits_imp85_crash_signature() -> None:
|
||||
"""mdx04 must no longer surface the IMP-#85 uncaught crash marker.
|
||||
|
||||
Before u1: missing-builder ``ValueError``
|
||||
(``'PAYLOAD_BUILDERS has no such entry'``) propagated uncaught and
|
||||
killed the pipeline at the mapper call site
|
||||
(``src/phase_z2_pipeline.py:4411-4413``, ``except FitError``
|
||||
only). After u1: the mapper raises
|
||||
``BuilderMissingError(FitError)``, the pipeline catches it at the
|
||||
same ``except FitError`` block, and the zone is recorded under
|
||||
``adapter_needed (skip render)``.
|
||||
|
||||
This smoke asserts only that the original IMP-#85 marker is gone
|
||||
from both stdout and stderr — downstream crashes (e.g.
|
||||
``build_layout_css`` zone aggregation when all live zones are
|
||||
adapter_needed) belong to a separate axis and are tracked as a
|
||||
follow-up issue candidate.
|
||||
"""
|
||||
cp = _run_pipeline("04.mdx", _unique_run_id("mdx04"))
|
||||
combined = cp.stdout + cp.stderr
|
||||
assert IMP85_OLD_CRASH_MARKER not in combined, (
|
||||
"IMP-#85 original crash signature still present in pipeline output:\n"
|
||||
f"--- stderr tail ---\n{cp.stderr[-1500:]}\n"
|
||||
f"--- stdout tail ---\n{cp.stdout[-1500:]}"
|
||||
)
|
||||
|
||||
|
||||
def test_conftest_env_isolation_active_for_ai_fallback_defaults() -> None:
|
||||
"""Direct assertion that ``tests/conftest.py`` isolated the AI
|
||||
fallback env vars BEFORE ``src.config`` was first imported.
|
||||
|
||||
With ``AI_FALLBACK_ENABLED=true`` in the live ``.env``, the
|
||||
Settings default-OFF contract would otherwise be violated whenever
|
||||
a developer runs ``pytest -q tests`` against a checkout that has a
|
||||
live operator ``.env``. This test pins the contract to the source
|
||||
of truth (``src/config.py`` defaults).
|
||||
"""
|
||||
from src.config import Settings
|
||||
|
||||
s = Settings()
|
||||
assert s.ai_fallback_enabled is False
|
||||
assert s.ai_fallback_auto_cache is False
|
||||
Reference in New Issue
Block a user