feat(#87): IMP-87 u1~u5 empty_shell honesty gate + BLOCKED exit
EMPTY_SHELL_NO_CONTENT overall enum + 3-marker detection (frame_template_id="__empty__" OR label="empty_shell" OR merge_type="empty_shell") routes empty-placeholder-only slides to BLOCKED CLI exit 1 + red final_status.html, blocking fake PASS reports (feedback_artifact_status_naming). Coverage accounting split: legacy covered_section_ids preserved + new content_rendered_section_ids / empty_shell_section_ids. mdx05 Case B (zero V4 evidence) honestly classified instead of synthesizing fabricated rank-1 reject frames. IMP-30 u6/u7 stale empty-shell PASS assertions inverted (29 tests). IMP-85 smoke parametrize: mdx05 removed from exit-0 list + dedicated BLOCKED exit test added (4 tests). No production behavior change for chain_exhausted Case A; no AI route activation; no mdx-id hardcoding. 53 targeted + 76 adjacent Phase Z tests PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2936,6 +2936,71 @@ def write_overflow_error(run_dir: Path, overflow: dict) -> Path:
|
||||
|
||||
# ─── Debug.json (single slide + zones[]) ───────────────────────
|
||||
|
||||
|
||||
def _is_empty_shell_unit(u: CompositionUnit) -> bool:
|
||||
"""IMP-87 u1 — true when a CompositionUnit is the IMP-30 u4 empty-shell
|
||||
placeholder (frame_template_id="__empty__" / label="empty_shell" /
|
||||
merge_type="empty_shell").
|
||||
|
||||
Used by compute_slide_status to redefine `full_mdx_coverage` over
|
||||
rendered content units only: an empty-shell unit attaches the aligned
|
||||
section_ids to a ``__empty__`` frame for layout purposes, but the slide
|
||||
surface carries no MDX content for those sections. Counting it as
|
||||
coverage would violate feedback_artifact_status_naming (overall /
|
||||
coverage must reflect actual content state, not pipeline completion).
|
||||
|
||||
The three markers are checked as independent OR-branches so a
|
||||
CompositionUnit synthesised by any of the IMP-30 u4 entry points
|
||||
(phase A / phase B / e2e) is classified consistently.
|
||||
"""
|
||||
if getattr(u, "frame_template_id", None) == "__empty__":
|
||||
return True
|
||||
if getattr(u, "label", None) == "empty_shell":
|
||||
return True
|
||||
if getattr(u, "merge_type", None) == "empty_shell":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _final_status_html_class(overall: str) -> str:
|
||||
"""IMP-87 u3 — map ``overall`` enum string → CSS class for the step20
|
||||
final_status.html dashboard.
|
||||
|
||||
EMPTY_SHELL_NO_CONTENT MUST resolve to ``"fail"`` (red) so the Case B
|
||||
honesty defect (Stage 1 mdx05) surfaces in the same colour band as visual
|
||||
failures and regressions, not the legacy ``"partial"`` amber band. The
|
||||
explicit check runs BEFORE the legacy substring-based mapping because
|
||||
the literal ``"EMPTY_SHELL_NO_CONTENT"`` contains neither ``"PASS"`` nor
|
||||
``"FAIL"`` / ``"REGRESSION"`` and would otherwise default to
|
||||
``"partial"`` (Stage 2 axis A5 lock).
|
||||
|
||||
All other enums preserve pre-IMP-87 substring semantics so the legacy
|
||||
PASS / RENDERED_WITH_VISUAL_REGRESSION / PARTIAL_COVERAGE / etc. paths
|
||||
keep the colour they had before u3.
|
||||
"""
|
||||
if overall == "EMPTY_SHELL_NO_CONTENT":
|
||||
return "fail"
|
||||
if "PASS" in overall:
|
||||
return "pass"
|
||||
if "FAIL" in overall or "REGRESSION" in overall:
|
||||
return "fail"
|
||||
return "partial"
|
||||
|
||||
|
||||
def _is_blocked_overall(overall: str) -> bool:
|
||||
"""IMP-87 u3 — true iff ``overall`` warrants a BLOCKED CLI exit
|
||||
(returncode 1) independent of the visual_check / full_mdx_coverage axes.
|
||||
|
||||
Currently the single blocked enum is EMPTY_SHELL_NO_CONTENT (Stage 1
|
||||
mdx05 Case B). The CLI consults this helper BEFORE the legacy
|
||||
visual_fail / partial_coverage branches so a content-empty placeholder
|
||||
slide that happens to pass Selenium overflow checks (no content → no
|
||||
overflow) cannot silently return a content-empty artifact without an
|
||||
exit signal (Stage 2 axis A4 lock).
|
||||
"""
|
||||
return overall == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
def compute_slide_status(sections: list[MdxSection],
|
||||
units: list[CompositionUnit],
|
||||
comp_debug: dict,
|
||||
@@ -2955,6 +3020,13 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
u4 empty-shell — needs user/AI adaptation 신호)
|
||||
|
||||
overall enum :
|
||||
EMPTY_SHELL_NO_CONTENT — IMP-87 u2 : every selected unit is an
|
||||
IMP-30 u4 empty-shell placeholder
|
||||
(no content-rendered units). Takes
|
||||
precedence over the 4-way ladder below
|
||||
because such a slide can technically
|
||||
pass Selenium overflow checks but
|
||||
carries no MDX content.
|
||||
PASS — visual OK + full coverage + adapter_needed=0
|
||||
RENDERED_WITH_VISUAL_REGRESSION — full coverage 이지만 visual fail
|
||||
PARTIAL_COVERAGE — 일부 section 필터됨, 렌더된 부분만 visual OK
|
||||
@@ -2964,13 +3036,43 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
Stage 1 Q3 + Codex #10 D4 lock.)
|
||||
"""
|
||||
aligned_ids = [s.section_id for s in sections]
|
||||
covered = set()
|
||||
# IMP-87 u1 — split coverage into legacy display vs honesty axis.
|
||||
# ``covered`` (legacy, preserved for downstream display / IMP-05 / IMP-06
|
||||
# readers) still tracks every section attached to any selected unit.
|
||||
# ``content_covered`` is the new honesty axis: only non-empty-shell units
|
||||
# count as "rendered with content". Sections attached solely to an
|
||||
# empty-shell placeholder (IMP-30 u4 frame_template_id="__empty__")
|
||||
# are routed into ``filtered_section_ids`` so an EMPTY-SHELL-only slide
|
||||
# cannot inherit full_mdx_coverage=True (Case B honesty defect lock,
|
||||
# Stage 1 anchor c53722ad).
|
||||
covered: set = set()
|
||||
content_covered: set = set()
|
||||
for u in units:
|
||||
covered.update(u.source_section_ids)
|
||||
filtered_ids = sorted(set(aligned_ids) - covered)
|
||||
if not _is_empty_shell_unit(u):
|
||||
content_covered.update(u.source_section_ids)
|
||||
filtered_ids = sorted(set(aligned_ids) - content_covered)
|
||||
full_coverage = len(filtered_ids) == 0
|
||||
visual_passed = bool(overflow.get("passed", False))
|
||||
|
||||
# IMP-87 u2 — Additive empty/content accounting (used by the overall enum
|
||||
# precedence block below and surfaced on the return dict for downstream
|
||||
# introspection). ``content_rendered_section_ids`` mirrors the new honesty
|
||||
# axis (content_covered) at the surface level so consumers can ask "how
|
||||
# much real MDX content actually rendered" without re-running the helper.
|
||||
# ``empty_shell_section_ids`` exposes the placeholder counterpart for the
|
||||
# same reason. Both pairs are purely additive — no existing field is
|
||||
# removed or repurposed (legacy ``covered_section_ids`` display semantics
|
||||
# locked by u1).
|
||||
empty_shell_units_list = [u for u in units if _is_empty_shell_unit(u)]
|
||||
content_units_list = [u for u in units if not _is_empty_shell_unit(u)]
|
||||
empty_shell_section_ids = sorted({
|
||||
sid
|
||||
for u in empty_shell_units_list
|
||||
for sid in (u.source_section_ids or [])
|
||||
})
|
||||
content_rendered_section_ids = sorted(content_covered)
|
||||
|
||||
adapter_needed_units = list(adapter_needed_units or [])
|
||||
content_truncated = []
|
||||
fallback_selections = []
|
||||
@@ -3057,7 +3159,24 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
"position": source_position,
|
||||
})
|
||||
|
||||
if full_coverage and visual_passed:
|
||||
# IMP-87 u2 — EMPTY_SHELL_NO_CONTENT precedence over the legacy 4-way
|
||||
# ladder (Stage 2 axis A3). If the slide has aligned MDX sections but
|
||||
# every selected unit is an IMP-30 u4 empty-shell placeholder (zero
|
||||
# content units selected, at least one empty-shell unit selected), the
|
||||
# visible artifact carries no real content — ``overall`` MUST report
|
||||
# EMPTY_SHELL_NO_CONTENT so the u3 CLI exit / final_status.html styling
|
||||
# can branch on it. The check runs before the visual/coverage ladder
|
||||
# because a content-empty placeholder slide can technically pass Selenium
|
||||
# overflow checks (no content → no overflow); without this precedence the
|
||||
# ladder would mislabel it PASS (Stage 1 mdx05 Case B honesty defect).
|
||||
is_empty_shell_only = (
|
||||
len(aligned_ids) > 0
|
||||
and len(empty_shell_units_list) > 0
|
||||
and len(content_units_list) == 0
|
||||
)
|
||||
if is_empty_shell_only:
|
||||
overall = "EMPTY_SHELL_NO_CONTENT"
|
||||
elif full_coverage and visual_passed:
|
||||
overall = "PASS"
|
||||
elif full_coverage and not visual_passed:
|
||||
overall = "RENDERED_WITH_VISUAL_REGRESSION"
|
||||
@@ -3102,6 +3221,11 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
"full_mdx_coverage": full_coverage,
|
||||
"aligned_section_ids": aligned_ids,
|
||||
"covered_section_ids": sorted(covered),
|
||||
# IMP-87 u2 — additive empty/content accounting (overall enum precedence above).
|
||||
"content_rendered_section_ids": content_rendered_section_ids,
|
||||
"content_rendered_unit_count": len(content_units_list),
|
||||
"empty_shell_section_ids": empty_shell_section_ids,
|
||||
"empty_shell_unit_count": len(empty_shell_units_list),
|
||||
"filtered_section_ids": filtered_ids,
|
||||
"filtered_section_reasons": filtered_section_reasons,
|
||||
"selection_path": "fallback_used" if fallback_selections else "rank_1",
|
||||
@@ -5929,7 +6053,11 @@ def run_phase_z2_mvp1(
|
||||
)
|
||||
# Step 20 HTML — 최종 판정 시각 보고
|
||||
_overall = slide_status.get("overall", "?")
|
||||
_ov_class = "pass" if "PASS" in _overall else "fail" if "FAIL" in _overall or "REGRESSION" in _overall else "partial"
|
||||
# IMP-87 u3 — route the dashboard CSS class through _final_status_html_class
|
||||
# so EMPTY_SHELL_NO_CONTENT renders as "fail" (red) instead of the legacy
|
||||
# substring-default "partial" amber. Other enums keep their pre-IMP-87
|
||||
# colour (Stage 2 axis A5 lock).
|
||||
_ov_class = _final_status_html_class(_overall)
|
||||
_vfs = slide_status.get("visual_fail_reasons") or []
|
||||
_vfs_html = (
|
||||
"<ul>" + "".join(f"<li>{v}</li>" for v in _vfs) + "</ul>"
|
||||
@@ -6048,6 +6176,36 @@ def run_phase_z2_mvp1(
|
||||
f"(impl_status={nap.get('next_action_implementation_status')})")
|
||||
|
||||
# 13. Exit 정책 — visual fail 은 abort, partial coverage 는 abort 안 하지만 PASS 도 아님
|
||||
# IMP-87 u3 — BLOCKED exit on EMPTY_SHELL_NO_CONTENT precedes the legacy
|
||||
# visual_fail / partial_coverage branches (Stage 2 axis A4). A slide whose
|
||||
# every selected unit is an IMP-30 u4 empty-shell placeholder carries no
|
||||
# MDX content; without this branch a Case B run could pass Selenium
|
||||
# overflow checks (nothing to overflow) and silently return overall
|
||||
# EMPTY_SHELL_NO_CONTENT without any CLI exit signal, violating
|
||||
# feedback_artifact_status_naming.
|
||||
if _is_blocked_overall(overall):
|
||||
_aligned = slide_status.get("aligned_section_ids") or []
|
||||
_empty_ids = slide_status.get("empty_shell_section_ids") or []
|
||||
_empty_count = slide_status.get("empty_shell_unit_count", 0)
|
||||
_content_count = slide_status.get("content_rendered_unit_count", 0)
|
||||
print(
|
||||
f"\n[Phase Z-2 IMP-87 u3] BLOCKED @ empty_shell_no_content ({overall})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
" reason : every selected unit is IMP-30 u4 empty-shell placeholder "
|
||||
"(no MDX content rendered)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(f" aligned : {_aligned}", file=sys.stderr)
|
||||
print(f" shell : {_empty_ids}", file=sys.stderr)
|
||||
print(
|
||||
f" units : empty_shell_unit_count={_empty_count} "
|
||||
f"content_rendered_unit_count={_content_count}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not slide_status["visual_check_passed"]:
|
||||
err_path = write_overflow_error(run_dir, overflow)
|
||||
print(f"\n[Phase Z-2 MVP-1.5b] FAIL @ visual_runtime_check ({overall})", file=sys.stderr)
|
||||
|
||||
@@ -1012,11 +1012,15 @@ def test_u6_empty_shell_unit_listed_with_empty_identifiers():
|
||||
assert entry["selection_path"] == "empty_shell"
|
||||
assert entry["fallback_reason"] == "no_v4_rank_1_for_any_section"
|
||||
assert entry["v4_rank"] is None
|
||||
# full_mdx_coverage holds because shell.source_section_ids covers every
|
||||
# aligned section id — u4 deliberately sets this so coverage stays True
|
||||
# under the terminal first-render invariant.
|
||||
assert status["full_mdx_coverage"] is True
|
||||
assert status["overall"] == "PASS"
|
||||
# IMP-87 u4 — honesty defect inversion. The shell.source_section_ids
|
||||
# still feeds legacy covered_section_ids for display, but the content-
|
||||
# rendered axis (u1) excludes empty-shell units, so full_mdx_coverage
|
||||
# MUST flip to False. Overall (u2) MUST elevate to
|
||||
# EMPTY_SHELL_NO_CONTENT before the legacy ladder, otherwise a slide
|
||||
# whose sole rendered unit is __empty__ would be reported as PASS —
|
||||
# the exact Stage 1 mdx05 honesty defect this issue exists to fix.
|
||||
assert status["full_mdx_coverage"] is False
|
||||
assert status["overall"] == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
# ─── u6 case 4 : mixed selection — provisional + normal units coexist ──
|
||||
@@ -1357,10 +1361,14 @@ def test_u7_e2e_zero_v4_empty_shell_status_surface(u7_patch_selector_deps):
|
||||
assert shell_entry["phase_z_status"] == "empty_shell"
|
||||
assert shell_entry["frame_template_id"] == "__empty__"
|
||||
assert shell_entry["source_section_ids"] == ["S1", "S2"]
|
||||
# Coverage check — both sections counted as covered by the shell unit
|
||||
# (rendered=True path; PASS enum unchanged by provisional qualifier).
|
||||
assert status["full_mdx_coverage"] is True
|
||||
assert status["overall"] == "PASS"
|
||||
# IMP-87 u4 — honesty defect inversion. The shell unit still attaches
|
||||
# both sections to legacy covered_section_ids (display preserved), but
|
||||
# the content-rendered axis (u1) excludes empty-shell units, so
|
||||
# full_mdx_coverage MUST flip to False. Overall (u2) MUST elevate to
|
||||
# EMPTY_SHELL_NO_CONTENT before the legacy 4-way ladder, so a zero-V4
|
||||
# slide cannot disguise itself as PASS through visual-overflow alone.
|
||||
assert status["full_mdx_coverage"] is False
|
||||
assert status["overall"] == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
# ─── u7 case 3 : e2e normal path unchanged when opt-in flags both on ─────
|
||||
|
||||
460
tests/test_phase_z2_imp87_empty_shell_honesty.py
Normal file
460
tests/test_phase_z2_imp87_empty_shell_honesty.py
Normal file
@@ -0,0 +1,460 @@
|
||||
"""IMP-87 empty-shell honesty regression tests — u1+u2+u3 scope.
|
||||
|
||||
Anchors the Stage 1 contract that an empty-shell-only run MUST NOT report
|
||||
full_mdx_coverage=True (u1), MUST surface a distinct ``overall`` enum plus
|
||||
additive empty/content accounting (u2), AND MUST resolve to a BLOCKED CLI
|
||||
exit (returncode 1) with a red ``"fail"`` CSS class on the step20
|
||||
final_status.html dashboard (u3).
|
||||
|
||||
u1 (baseline):
|
||||
1) ``_is_empty_shell_unit`` helper — three independent OR-branches over
|
||||
``frame_template_id == "__empty__"`` / ``label == "empty_shell"`` /
|
||||
``merge_type == "empty_shell"``. Any IMP-30 u4 phase A / phase B / e2e
|
||||
synthesised placeholder is classified consistently.
|
||||
2) ``compute_slide_status`` coverage split — the legacy ``covered``
|
||||
accumulator (display semantics, preserved for IMP-05 / IMP-06 readers)
|
||||
still tracks every section attached to any selected unit, while the new
|
||||
``content_covered`` axis counts only non-empty-shell units. The
|
||||
``filtered_section_ids`` / ``full_mdx_coverage`` axes derive from
|
||||
``content_covered`` so an EMPTY-SHELL-only slide cannot inherit
|
||||
full_mdx_coverage=True (Case B honesty defect lock, Stage 1 anchor
|
||||
c53722ad).
|
||||
|
||||
u2:
|
||||
3) ``overall == "EMPTY_SHELL_NO_CONTENT"`` precedence over the legacy
|
||||
4-way ladder when every selected unit is an empty-shell placeholder
|
||||
(zero content units selected, at least one empty-shell unit selected,
|
||||
at least one aligned section). The precedence runs BEFORE the
|
||||
visual/coverage ladder so a content-empty slide that happens to pass
|
||||
Selenium overflow checks cannot be mislabelled PASS.
|
||||
4) Additive return-dict fields ``content_rendered_section_ids`` /
|
||||
``content_rendered_unit_count`` / ``empty_shell_section_ids`` /
|
||||
``empty_shell_unit_count``. No legacy field is removed or repurposed.
|
||||
|
||||
u3 (added by this revision):
|
||||
5) ``_final_status_html_class`` helper — EMPTY_SHELL_NO_CONTENT resolves
|
||||
to ``"fail"`` (red) so the step20 final_status.html dashboard surfaces
|
||||
the Case B honesty defect in the same colour band as visual failures
|
||||
and regressions, not the legacy ``"partial"`` amber band. Existing
|
||||
PASS / RENDERED_WITH_VISUAL_REGRESSION / PARTIAL_COVERAGE / etc.
|
||||
enums keep their pre-IMP-87 substring-based colour (Stage 2 axis A5).
|
||||
6) ``_is_blocked_overall`` helper — true iff ``overall`` warrants a
|
||||
BLOCKED CLI exit (returncode 1) independent of the visual_check /
|
||||
full_mdx_coverage axes. Currently only EMPTY_SHELL_NO_CONTENT is
|
||||
blocked; every other enum returns False so the legacy CLI ladder
|
||||
(visual_fail → exit 1, partial_coverage → return, PASS → return) is
|
||||
preserved bit-for-bit (Stage 2 axis A4).
|
||||
|
||||
Per feedback_scope_qualified_verification, each test declares which case it
|
||||
covers (A=content only, B=empty-shell only, C=mixed) and whether the
|
||||
assertion is the u1 surface (helper/coverage), the u2 surface (overall enum
|
||||
/ accounting fields), or the u3 surface (CLI exit helper / HTML class).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from src.phase_z2_composition import CompositionUnit
|
||||
from src.phase_z2_pipeline import (
|
||||
MdxSection,
|
||||
_final_status_html_class,
|
||||
_is_blocked_overall,
|
||||
_is_empty_shell_unit,
|
||||
compute_slide_status,
|
||||
)
|
||||
|
||||
|
||||
def _mk_section(section_id: str) -> MdxSection:
|
||||
return MdxSection(
|
||||
section_id=section_id,
|
||||
section_num=int(section_id.lstrip("S") or "0"),
|
||||
title=f"Section {section_id}",
|
||||
raw_content=f"raw {section_id}",
|
||||
)
|
||||
|
||||
|
||||
def _mk_content_unit(*, section_ids: list[str], **overrides) -> CompositionUnit:
|
||||
base = dict(
|
||||
source_section_ids=list(section_ids),
|
||||
merge_type="single",
|
||||
frame_template_id="MOCK_template_direct_a",
|
||||
frame_id="MOCK_frame_001",
|
||||
frame_number=1,
|
||||
confidence=0.9,
|
||||
label="use_as_is",
|
||||
phase_z_status="matched_zone",
|
||||
raw_content="alpha",
|
||||
title="MOCK content",
|
||||
v4_rank=1,
|
||||
selection_path="rank_1",
|
||||
fallback_reason=None,
|
||||
score=1.0,
|
||||
provisional=False,
|
||||
)
|
||||
base.update(overrides)
|
||||
return CompositionUnit(**base)
|
||||
|
||||
|
||||
def _mk_empty_shell_unit(*, section_ids: list[str]) -> CompositionUnit:
|
||||
"""Mirror of IMP-30 u4 phase B empty-shell synthesis. All three marker
|
||||
fields set so _is_empty_shell_unit returns True via every branch
|
||||
independently — the per-marker tests below override individually."""
|
||||
return CompositionUnit(
|
||||
source_section_ids=list(section_ids),
|
||||
merge_type="empty_shell",
|
||||
frame_template_id="__empty__",
|
||||
frame_id="__empty__",
|
||||
frame_number=0,
|
||||
confidence=0.0,
|
||||
label="empty_shell",
|
||||
phase_z_status="empty_shell",
|
||||
raw_content="\n\n".join(f"raw {sid}" for sid in section_ids),
|
||||
title=" / ".join(f"Section {sid}" for sid in section_ids),
|
||||
v4_rank=None,
|
||||
selection_path="empty_shell",
|
||||
fallback_reason="no_v4_rank_1_for_any_section",
|
||||
score=0.0,
|
||||
provisional=True,
|
||||
)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# Helper unit tests — _is_empty_shell_unit detection (3-marker OR)
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def test_helper_detects_unit_via_frame_template_id_marker_only():
|
||||
"""Empty-shell classifier triggers on frame_template_id="__empty__"
|
||||
even when label / merge_type look like a normal content unit. Guards
|
||||
a future code path that only sets the frame_template_id field."""
|
||||
u = _mk_content_unit(
|
||||
section_ids=["S1"],
|
||||
frame_template_id="__empty__",
|
||||
)
|
||||
assert _is_empty_shell_unit(u) is True
|
||||
|
||||
|
||||
def test_helper_detects_unit_via_label_marker_only():
|
||||
"""Classifier triggers on label="empty_shell" alone."""
|
||||
u = _mk_content_unit(section_ids=["S1"], label="empty_shell")
|
||||
assert _is_empty_shell_unit(u) is True
|
||||
|
||||
|
||||
def test_helper_detects_unit_via_merge_type_marker_only():
|
||||
"""Classifier triggers on merge_type="empty_shell" alone."""
|
||||
u = _mk_content_unit(section_ids=["S1"], merge_type="empty_shell")
|
||||
assert _is_empty_shell_unit(u) is True
|
||||
|
||||
|
||||
def test_helper_rejects_normal_content_unit():
|
||||
"""A vanilla rank-1 use_as_is unit is NOT an empty shell."""
|
||||
u = _mk_content_unit(section_ids=["S1"])
|
||||
assert _is_empty_shell_unit(u) is False
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# Case B — empty-shell only (mdx05 Stage 1 reproduction, u1 surface axes)
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def test_case_b_empty_shell_only_breaks_full_mdx_coverage():
|
||||
"""Stage 1 mdx05 reproduction at the u1 surface. With one empty-shell
|
||||
unit covering every aligned section, the new content_covered axis is
|
||||
empty, so ``full_mdx_coverage`` MUST be False and every aligned
|
||||
section MUST surface in ``filtered_section_ids``. The legacy
|
||||
``covered_section_ids`` field (display semantics) still includes the
|
||||
shell-covered sections — preserved for IMP-05 / IMP-06 readers."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2"), _mk_section("S3")]
|
||||
shell = _mk_empty_shell_unit(section_ids=["S1", "S2", "S3"])
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, [shell], comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["full_mdx_coverage"] is False
|
||||
assert status["filtered_section_ids"] == ["S1", "S2", "S3"]
|
||||
# Legacy covered axis preserved — IMP-87 u1 does NOT change display
|
||||
# semantics for downstream readers; it only redefines the honesty axis.
|
||||
assert status["covered_section_ids"] == ["S1", "S2", "S3"]
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# Case C — mixed empty + content units (partial coverage realism)
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def test_case_c_mixed_empty_and_content_filters_shell_sections_only():
|
||||
"""A content unit covers S1; an empty_shell placeholder covers S2.
|
||||
Only S2 should surface as filtered (the shell does not count as content
|
||||
coverage), but the legacy ``covered_section_ids`` field still lists
|
||||
both sections (display semantics unchanged by u1)."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2")]
|
||||
units = [
|
||||
_mk_content_unit(section_ids=["S1"]),
|
||||
_mk_empty_shell_unit(section_ids=["S2"]),
|
||||
]
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, units, comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["full_mdx_coverage"] is False
|
||||
assert status["filtered_section_ids"] == ["S2"]
|
||||
assert status["covered_section_ids"] == ["S1", "S2"]
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# Case A — normal content-only PASS preserved
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def test_case_a_normal_content_only_preserves_full_coverage():
|
||||
"""IMP-05 / IMP-30 regression guard at the u1 surface. A slide whose
|
||||
every selected unit is non-empty-shell content must continue to report
|
||||
``full_mdx_coverage`` == True with an empty ``filtered_section_ids``.
|
||||
No behavioral change vs pre-IMP-87 baseline — u1 must be additive."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2")]
|
||||
units = [
|
||||
_mk_content_unit(section_ids=["S1"]),
|
||||
_mk_content_unit(section_ids=["S2"], frame_id="MOCK_frame_002"),
|
||||
]
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, units, comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["full_mdx_coverage"] is True
|
||||
assert status["filtered_section_ids"] == []
|
||||
assert status["covered_section_ids"] == ["S1", "S2"]
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# IMP-87 u2 — EMPTY_SHELL_NO_CONTENT overall + additive accounting fields
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# These tests assert the u2 surface ONLY:
|
||||
# - ``overall`` enum precedence (EMPTY_SHELL_NO_CONTENT before the 4-way
|
||||
# ladder) for Case B; legacy ladder preserved for Cases A and C.
|
||||
# - Additive return-dict fields populate correctly across the three cases.
|
||||
# CLI exit codes and final_status.html styling are u3 scope and remain
|
||||
# unasserted here.
|
||||
|
||||
|
||||
def test_case_b_u2_overall_is_empty_shell_no_content_even_when_visual_passes():
|
||||
"""Case B (empty-shell only) — Stage 1 mdx05 reproduction at the u2
|
||||
surface. The precedence check MUST fire before the visual/coverage
|
||||
ladder: even though ``overflow_pass={passed: True}`` would otherwise
|
||||
push the ladder to PASS (the content-empty slide has nothing to
|
||||
overflow), the u2 precedence forces ``overall`` to
|
||||
EMPTY_SHELL_NO_CONTENT. Guards the Stage 1 honesty defect."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2"), _mk_section("S3")]
|
||||
shell = _mk_empty_shell_unit(section_ids=["S1", "S2", "S3"])
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, [shell], comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["overall"] == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
def test_case_b_u2_overall_remains_empty_shell_no_content_when_visual_fails():
|
||||
"""Case B — precedence is unconditional on visual_passed. A visual fail
|
||||
must NOT downgrade EMPTY_SHELL_NO_CONTENT to a ladder enum; the empty-
|
||||
shell signal dominates because the slide carries no MDX content to
|
||||
rescue regardless of overflow status."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2")]
|
||||
shell = _mk_empty_shell_unit(section_ids=["S1", "S2"])
|
||||
overflow_fail = {"passed": False, "fail_reasons": ["mock_overflow"]}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, [shell], comp_debug, overflow_fail,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["overall"] == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
def test_case_b_u2_accounting_fields_populate_for_empty_shell_only():
|
||||
"""Case B — additive accounting fields. All aligned sections appear in
|
||||
``empty_shell_section_ids`` and none in ``content_rendered_section_ids``.
|
||||
Counts mirror the unit partition (1 shell unit, 0 content units)."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2"), _mk_section("S3")]
|
||||
shell = _mk_empty_shell_unit(section_ids=["S1", "S2", "S3"])
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, [shell], comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["empty_shell_section_ids"] == ["S1", "S2", "S3"]
|
||||
assert status["content_rendered_section_ids"] == []
|
||||
assert status["empty_shell_unit_count"] == 1
|
||||
assert status["content_rendered_unit_count"] == 0
|
||||
|
||||
|
||||
def test_case_a_u2_overall_pass_preserved_for_content_only():
|
||||
"""Case A — content-only slide. u2 precedence MUST NOT fire (zero
|
||||
empty-shell units) so the legacy ladder still reports PASS when visual
|
||||
+ full coverage line up. Regression guard against precedence over-firing."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2")]
|
||||
units = [
|
||||
_mk_content_unit(section_ids=["S1"]),
|
||||
_mk_content_unit(section_ids=["S2"], frame_id="MOCK_frame_002"),
|
||||
]
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, units, comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["overall"] == "PASS"
|
||||
assert status["empty_shell_section_ids"] == []
|
||||
assert status["content_rendered_section_ids"] == ["S1", "S2"]
|
||||
assert status["empty_shell_unit_count"] == 0
|
||||
assert status["content_rendered_unit_count"] == 2
|
||||
|
||||
|
||||
def test_case_c_u2_mixed_falls_to_legacy_partial_coverage_ladder():
|
||||
"""Case C — mixed empty + content. u2 precedence MUST NOT fire (one
|
||||
content unit exists), so the legacy ladder takes over: not-full-coverage
|
||||
+ visual-pass → PARTIAL_COVERAGE. Accounting fields split the unit
|
||||
partition cleanly (S1 = content, S2 = shell)."""
|
||||
sections = [_mk_section("S1"), _mk_section("S2")]
|
||||
units = [
|
||||
_mk_content_unit(section_ids=["S1"]),
|
||||
_mk_empty_shell_unit(section_ids=["S2"]),
|
||||
]
|
||||
overflow_pass = {"passed": True, "fail_reasons": []}
|
||||
comp_debug = {"candidates_summary": []}
|
||||
|
||||
status = compute_slide_status(
|
||||
sections, units, comp_debug, overflow_pass,
|
||||
adapter_needed_units=None, debug_zones=None,
|
||||
)
|
||||
|
||||
assert status["overall"] == "PARTIAL_COVERAGE"
|
||||
assert status["empty_shell_section_ids"] == ["S2"]
|
||||
assert status["content_rendered_section_ids"] == ["S1"]
|
||||
assert status["empty_shell_unit_count"] == 1
|
||||
assert status["content_rendered_unit_count"] == 1
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# IMP-87 u3 — _final_status_html_class CSS class mapping
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# These tests assert the u3 dashboard CSS-class surface ONLY. CLI exit
|
||||
# decisions are covered by the _is_blocked_overall tests further down. End-
|
||||
# to-end pipeline integration (full run → BLOCKED returncode, red HTML in
|
||||
# the actual artifact file) is intentionally left to the smoke layer in
|
||||
# u5; here the helper-level guarantee is enough because the call site
|
||||
# (``_ov_class = _final_status_html_class(_overall)``) is a direct
|
||||
# substitution of the previous inline expression.
|
||||
|
||||
|
||||
def test_u3_html_class_empty_shell_no_content_is_fail_red():
|
||||
"""Case B u3 surface — EMPTY_SHELL_NO_CONTENT MUST map to ``"fail"`` so
|
||||
the dashboard surfaces the honesty defect in the red colour band. Stage
|
||||
2 axis A5 lock: the literal does NOT contain ``"PASS"`` / ``"FAIL"`` /
|
||||
``"REGRESSION"`` so without the explicit branch the helper would default
|
||||
to ``"partial"`` (the amber legacy band), which is the exact mislabel
|
||||
u3 corrects."""
|
||||
assert _final_status_html_class("EMPTY_SHELL_NO_CONTENT") == "fail"
|
||||
|
||||
|
||||
def test_u3_html_class_pass_preserves_legacy_substring_mapping():
|
||||
"""Case A u3 surface — the legacy substring rule for ``"PASS"`` MUST
|
||||
stay intact so existing dashboards keep the green band for healthy
|
||||
runs. Guards against the u3 explicit branch over-firing on any string
|
||||
that happens to contain ``"PASS"``."""
|
||||
assert _final_status_html_class("PASS") == "pass"
|
||||
|
||||
|
||||
def test_u3_html_class_regression_and_fail_substrings_preserved():
|
||||
"""u3 surface — the legacy ``"FAIL"`` / ``"REGRESSION"`` substring rule
|
||||
MUST stay intact for the existing visual-regression enums so dashboards
|
||||
keep the red band for overflow / regression failures. The pre-IMP-87
|
||||
expression is preserved verbatim in the fallback branch; this test
|
||||
guards that preservation."""
|
||||
assert (
|
||||
_final_status_html_class("RENDERED_WITH_VISUAL_REGRESSION") == "fail"
|
||||
)
|
||||
assert (
|
||||
_final_status_html_class("PARTIAL_COVERAGE_WITH_VISUAL_REGRESSION")
|
||||
== "fail"
|
||||
)
|
||||
|
||||
|
||||
def test_u3_html_class_partial_coverage_remains_amber_partial():
|
||||
"""u3 surface — ``"PARTIAL_COVERAGE"`` MUST stay in the amber
|
||||
``"partial"`` band: it carries some rendered content (unlike Case B)
|
||||
and does not deserve the red band. Guards against the EMPTY_SHELL
|
||||
branch accidentally widening to the partial enum."""
|
||||
assert _final_status_html_class("PARTIAL_COVERAGE") == "partial"
|
||||
|
||||
|
||||
def test_u3_html_class_unknown_enum_falls_back_to_partial():
|
||||
"""u3 surface — defensive default. An unrecognised enum string (e.g.
|
||||
legacy ``"?"`` from the ``slide_status.get("overall", "?")`` fallback
|
||||
at the call site) MUST keep the legacy amber default so the dashboard
|
||||
does not silently shift colour bands when a new enum is added without
|
||||
updating the helper."""
|
||||
assert _final_status_html_class("?") == "partial"
|
||||
assert _final_status_html_class("UNKNOWN_FUTURE_ENUM") == "partial"
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
# IMP-87 u3 — _is_blocked_overall CLI exit gating
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def test_u3_blocked_overall_true_for_empty_shell_no_content():
|
||||
"""Case B u3 surface — EMPTY_SHELL_NO_CONTENT MUST be flagged for the
|
||||
BLOCKED CLI exit branch. The CLI gates ``sys.exit(1)`` on this helper
|
||||
BEFORE the legacy visual_fail / partial_coverage branches so a
|
||||
content-empty slide that passes Selenium overflow checks cannot
|
||||
silently return without an exit signal (Stage 2 axis A4 lock,
|
||||
feedback_artifact_status_naming guardrail)."""
|
||||
assert _is_blocked_overall("EMPTY_SHELL_NO_CONTENT") is True
|
||||
|
||||
|
||||
def test_u3_blocked_overall_false_for_pass_and_legacy_failure_enums():
|
||||
"""Case A u3 surface — every pre-IMP-87 enum MUST stay False so the
|
||||
legacy CLI ladder is preserved bit-for-bit (visual_fail → exit 1,
|
||||
partial_coverage → return without exit, PASS → return without exit).
|
||||
Regression guard against the BLOCKED branch widening beyond the Case B
|
||||
honesty defect."""
|
||||
for enum in (
|
||||
"PASS",
|
||||
"RENDERED_WITH_VISUAL_REGRESSION",
|
||||
"PARTIAL_COVERAGE",
|
||||
"PARTIAL_COVERAGE_WITH_VISUAL_REGRESSION",
|
||||
):
|
||||
assert _is_blocked_overall(enum) is False, (
|
||||
f"{enum} must NOT trigger the IMP-87 u3 BLOCKED exit; only "
|
||||
f"EMPTY_SHELL_NO_CONTENT is gated."
|
||||
)
|
||||
|
||||
|
||||
def test_u3_blocked_overall_false_for_defensive_unknown_enum():
|
||||
"""u3 surface — defensive default. An unknown / sentinel enum string
|
||||
MUST stay out of the BLOCKED branch so a future enum addition does
|
||||
not accidentally enable exit-1 behaviour without an explicit opt-in
|
||||
here (Stage 2 axis A4 narrow allow-list lock)."""
|
||||
assert _is_blocked_overall("?") is False
|
||||
assert _is_blocked_overall("UNKNOWN_FUTURE_ENUM") is False
|
||||
@@ -14,13 +14,29 @@ invariant + runtime VP gate end-to-end against real MDX inputs:
|
||||
*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).
|
||||
* 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
|
||||
@@ -30,6 +46,7 @@ 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
|
||||
@@ -65,15 +82,22 @@ def _unique_run_id(prefix: str) -> str:
|
||||
"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.
|
||||
"""mdx03 hits 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.
|
||||
primary regression guard that IMP-#85 u1-u6 do not perturb
|
||||
mapper / pipeline behaviour for non-VP routes.
|
||||
|
||||
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, (
|
||||
@@ -83,6 +107,69 @@ def test_non_vp_smoke_runs_clean(mdx_name: str, prefix: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_mdx05_blocked_exit_empty_shell_no_content() -> None:
|
||||
"""mdx05 must exit 1 (BLOCKED) with ``overall=EMPTY_SHELL_NO_CONTENT``.
|
||||
|
||||
IMP-#87 u5 — mdx05 is the canonical Case B fixture (zero V4
|
||||
evidence for any aligned section per Stage 1; ``judgments_full32 = 0``
|
||||
in step05). The pre-IMP-#87 pipeline mislabelled this state as
|
||||
``overall=PASS`` + ``full_mdx_coverage=True`` because the only
|
||||
rendered unit was an IMP-#30 u4 EMPTY-SHELL placeholder
|
||||
(``frame_template_id="__empty__"``) which trivially passes the
|
||||
Selenium overflow check. IMP-#87 u1 splits content-rendered
|
||||
coverage from legacy ``covered_section_ids``, u2 elevates the
|
||||
overall enum to ``EMPTY_SHELL_NO_CONTENT`` before the legacy
|
||||
ladder, and u3 routes that enum to a BLOCKED CLI exit (1).
|
||||
|
||||
This smoke pins the post-IMP-#87 contract on the real mdx05
|
||||
pipeline run:
|
||||
|
||||
* subprocess returncode == 1 (BLOCKED, u3 axis A4).
|
||||
* ``step20_slide_status.json`` ``overall`` ==
|
||||
``"EMPTY_SHELL_NO_CONTENT"`` (u2 axis A3 precedence over the
|
||||
legacy 4-way ladder).
|
||||
* ``step20_slide_status.json`` ``full_mdx_coverage`` is False
|
||||
(u1 axis A2 content-rendered coverage split).
|
||||
* The IMP-#85 original crash marker
|
||||
(``PAYLOAD_BUILDERS has no such entry``) is absent from both
|
||||
stdout and stderr — the IMP-#85 crash-marker guard is
|
||||
preserved on the mdx05 path even though mdx05 itself no
|
||||
longer exits 0.
|
||||
"""
|
||||
run_id = _unique_run_id("mdx05")
|
||||
cp = _run_pipeline("05.mdx", run_id)
|
||||
|
||||
assert cp.returncode == 1, (
|
||||
f"mdx05 expected BLOCKED exit 1, 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", (
|
||||
f"mdx05 overall expected EMPTY_SHELL_NO_CONTENT, got "
|
||||
f"{status_data.get('overall')!r}"
|
||||
)
|
||||
assert status_data.get("full_mdx_coverage") is False, (
|
||||
f"mdx05 full_mdx_coverage expected False, 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user