Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 9m49s
- tests/integration/scripts/regenerate_snapshots.py: 스냅샷 일괄 재생성 도구 - 통합 스냅샷 9종 갱신, 회귀/유닛 테스트 7건 보강 - ISSUE_DRAFTS_2026-07-02.md: 7/2 전수 검토 이슈 초안 보존 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
213 lines
9.2 KiB
Python
213 lines
9.2 KiB
Python
"""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 — IMP-#87 u5 inversion. mdx05 has ZERO V4 evidence for any
|
|
section (``judgments_full32 = 0``, Case B per IMP-#87 Stage 1),
|
|
so the composition planner emits an IMP-#30 u4 EMPTY-SHELL
|
|
placeholder for the whole slide. Before IMP-#87 the pipeline
|
|
reported ``overall=PASS`` + ``full_mdx_coverage=True`` for this
|
|
state — the honesty defect this issue fixes. After IMP-#87 u2/u3
|
|
the same run elevates ``overall`` to
|
|
``EMPTY_SHELL_NO_CONTENT`` and the CLI exits 1 (BLOCKED). The old
|
|
exit-0 mdx05 smoke is therefore stale; this module now (a) keeps
|
|
mdx03 in the exit-0 non-VP parametrization, (b) adds a dedicated
|
|
mdx05 blocked-exit assertion that verifies the new
|
|
``EMPTY_SHELL_NO_CONTENT`` status surface, and (c) preserves the
|
|
IMP-#85 crash-marker guard on the mdx05 path so future
|
|
regressions cannot re-introduce the original uncaught
|
|
``BuilderMissingError`` propagation under cover of the blocked
|
|
exit.
|
|
|
|
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 json
|
|
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"
|
|
RUNS_DIR = REPO_ROOT / "data" / "runs"
|
|
|
|
# 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",
|
|
[
|
|
("01.mdx", "mdx01"),
|
|
("02.mdx", "mdx02"),
|
|
("03.mdx", "mdx03"),
|
|
],
|
|
)
|
|
def test_non_vp_smoke_runs_clean(mdx_name: str, prefix: str) -> None:
|
|
"""mdx01/02/03 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 IMP-#85 u1-u6 do not perturb
|
|
mapper / pipeline behaviour for non-VP routes. IMP-#91 u1 extends
|
|
the parametrization from mdx03-only to the mdx01/02/03 acceptance
|
|
triple — closing the subprocess-axis coverage gap for the two
|
|
non-VP mdx that had only in-process B4 SHA parity coverage
|
|
(tests/regression/test_b4_mapper_source_sha_parity.py).
|
|
|
|
IMP-#87 u5 — mdx05 was removed from this parametrization because
|
|
its V4 evidence is empty for every aligned section (Case B,
|
|
Stage 1 lock). The IMP-#87 u2 ``EMPTY_SHELL_NO_CONTENT`` enum
|
|
+ u3 BLOCKED CLI exit make the post-IMP-#87 mdx05 run exit 1,
|
|
not 0, so an exit-0 parametrization would now be stale. The
|
|
dedicated mdx05 blocked-exit coverage lives in
|
|
``test_mdx05_blocked_exit_empty_shell_no_content`` below.
|
|
"""
|
|
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_mdx05_renders_clean_post_emergency_p3() -> None:
|
|
"""mdx05 renders successfully (exit 0) — IMP-87 BLOCKED 계약의 후속.
|
|
|
|
구 IMP-#87 u5 계약(mdx05 = canonical empty-shell Case B → exit 1)은
|
|
Emergency P3 (generic_fallback mandatory, GitHub #9) + GitHub #17
|
|
(05-1/05-2 V4 evidence 정식 평가)로 전제가 소멸했다. 본 smoke 는
|
|
현 계약(렌더 성공 + empty-shell terminal 부재 + full coverage)과
|
|
IMP-#85 crash-marker 가드 존속을 함께 잠근다.
|
|
"""
|
|
# ── 2026-07 갱신 (GitHub #29) — IMP-87 전제 소멸 ──────────────────
|
|
# 구 계약: mdx05 = canonical Case B (V4 evidence 0) → EMPTY_SHELL_NO_CONTENT
|
|
# + BLOCKED exit 1. 이후 두 개선으로 전제 자체가 사라짐:
|
|
# 1. Emergency P3 (2026-05-26): raw 후보 0 → generic_fallback mandatory
|
|
# (empty_shell terminal 제거 — GitHub #9 검증)
|
|
# 2. GitHub #17 (2026-07-07): 05-1/05-2 V4 evidence 를 pipeline_17b 로
|
|
# 정식 평가·병합 — "judgments_full32 = 0" 이 더 이상 사실 아님
|
|
# 현 계약: mdx05 는 렌더 성공 (exit 0) + 전 섹션 커버 + 텍스트 무손실.
|
|
# IMP-85 crash-marker 가드는 그대로 존속.
|
|
run_id = _unique_run_id("mdx05")
|
|
cp = _run_pipeline("05.mdx", run_id)
|
|
|
|
assert cp.returncode == 0, (
|
|
f"mdx05 expected rendered exit 0 (post Emergency-P3/#17), got {cp.returncode}\n"
|
|
f"--- stderr tail ---\n{cp.stderr[-1500:]}\n"
|
|
f"--- stdout tail ---\n{cp.stdout[-1500:]}"
|
|
)
|
|
|
|
combined = cp.stdout + cp.stderr
|
|
assert IMP85_OLD_CRASH_MARKER not in combined, (
|
|
"IMP-#85 original crash signature regressed on mdx05 path:\n"
|
|
f"--- stderr tail ---\n{cp.stderr[-1500:]}\n"
|
|
f"--- stdout tail ---\n{cp.stdout[-1500:]}"
|
|
)
|
|
|
|
status_path = RUNS_DIR / run_id / "phase_z2" / "steps" / "step20_slide_status.json"
|
|
assert status_path.is_file(), (
|
|
f"mdx05 step20_slide_status.json not found at {status_path}\n"
|
|
f"--- stderr tail ---\n{cp.stderr[-1500:]}\n"
|
|
f"--- stdout tail ---\n{cp.stdout[-1500:]}"
|
|
)
|
|
status_payload = json.loads(status_path.read_text(encoding="utf-8"))
|
|
status_data = status_payload.get("data") or {}
|
|
assert status_data.get("overall") != "EMPTY_SHELL_NO_CONTENT", (
|
|
"mdx05 empty-shell 회귀 — Emergency P3/#17 이후 금지된 terminal"
|
|
)
|
|
assert status_data.get("overall") in {"PASS", "PARTIAL_COVERAGE"}, (
|
|
f"mdx05 overall expected PASS/PARTIAL_COVERAGE, got "
|
|
f"{status_data.get('overall')!r}"
|
|
)
|
|
assert status_data.get("full_mdx_coverage") is True, (
|
|
f"mdx05 full_mdx_coverage expected True, got "
|
|
f"{status_data.get('full_mdx_coverage')!r}"
|
|
)
|
|
|
|
|
|
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
|