fix(IMP-06): Stage 4 blocker-fix — render_records + plan-aware traces
Three Codex #13 blockers in a single coherent commit. Blocker 1 (units None hazard) — drop None placeholders from `units` list. Replace with a separate `render_records` layer built AFTER frame_overrides apply. units = canonical renderable list (list[CompositionUnit] only); render_records = canonical per-position view including empty / collision- skipped / cli_override entries. Downstream loops (Step 6 print, frame_ overrides, zones_data/debug_zones, Step 9 application_plan, compute_slide_ status covered loop) no longer need None guards. Blocker 2 (no integration test) — add end-to-end pipeline integration test: `--override-section-assignment top=03-2` on sample 03 MDX produces zones_data[top].source_section_ids = ['03-2'], debug_zones[top].assignment _source = 'cli_override', debug_zones[bottom].v4_template_id = '__empty__' (override_collision whole-skip), step20 filtered_section_ids contains '03-1', and filtered_section_reasons carries a section_assignment_override _uncovered entry. Proves the render path — not only comp_debug — reflects the CLI override. Blocker 3 (Step 9/20 not plan-aware) — surface plan-aware additive fields in both render-path debug_zones/zones_data and Step 9 application_plan units: position, assignment_source, section_assignment_override, replaced_auto_unit, skipped_collided_auto_units, uncovered_section_ids, skipped_reason. compute_slide_status appends Codex #10 Catch O list-shaped filtered_section_reasons entries for override-uncovered sections and folds them into filtered_section_ids so full_coverage is re-evaluated post-override. Exact-id-only collision semantics enforced (Codex #14/#15/#16/#17): S3 and S3-1 are distinct ids; no prefix hierarchy, no parent cascade. Three new section-id invariant tests added (parent-like vs child-like, exact duplicate collision detected, distinct ids coexist). Test : 24 pytest pass (9 helper + 9 case + 3 invariant + 1 case 9b + 1 integration + 1 from v4_fallback baseline) ; smoke 11/11 PASS. Register `integration` pytest marker in pyproject.toml.
This commit is contained in:
@@ -325,3 +325,231 @@ def test_summary_aggregation_counts_applied_skipped_uncovered():
|
||||
assert summary["skipped_count"] >= 1 # collision skip
|
||||
assert summary["uncovered_section_ids"] == ["MOCK_S2"]
|
||||
assert summary["section_assignment_overrides_applied"][0]["position"] == "top"
|
||||
|
||||
|
||||
# ─── Section-id exact-id invariant (Codex #14 / #15 / #16 / #17) ─────────
|
||||
|
||||
|
||||
def test_section_id_exact_match_parent_like_does_not_collide_with_child_like():
|
||||
"""Codex #14 explicit clarification : section ids are matched exact-string only.
|
||||
`S3` and `S3-1` are distinct ids — auto plan having a parent-like id `S3` does
|
||||
not implicitly consume / uncover `S3-1` or `S3-2` via prefix matching.
|
||||
"""
|
||||
auto = _FakeUnit(source_section_ids=["MOCK_S3"], frame_template_id="MOCK_T_parent")
|
||||
units = [auto]
|
||||
positions = ["top", "bottom"]
|
||||
# Override places a child-like id S3-1 into bottom; must NOT be treated as
|
||||
# the same section as S3 by prefix.
|
||||
overrides = {"bottom": ["MOCK_S3-1"]}
|
||||
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_S3", "MOCK_S3-1"]}
|
||||
override_frames = {"MOCK_S3-1": "MOCK_T_child"}
|
||||
|
||||
plan, summary = _build_position_assignment_plan(
|
||||
units=units, positions=positions,
|
||||
override_section_assignments=overrides,
|
||||
sections_by_id=sections_by_id,
|
||||
override_frames=override_frames,
|
||||
)
|
||||
|
||||
by_pos = {p["position"]: p for p in plan}
|
||||
# top : auto plan keeps MOCK_S3 intact (NOT consumed/skipped by S3-1 override)
|
||||
assert by_pos["top"]["assignment_source"] == "auto"
|
||||
assert by_pos["top"]["source_section_ids"] == ["MOCK_S3"]
|
||||
assert by_pos["top"]["skipped_reason"] is None
|
||||
# bottom : override applies MOCK_S3-1 as a distinct section
|
||||
assert by_pos["bottom"]["assignment_source"] == "cli_override"
|
||||
assert by_pos["bottom"]["source_section_ids"] == ["MOCK_S3-1"]
|
||||
# No collision, no uncovered MOCK_S3 from "prefix match"
|
||||
assert summary["uncovered_section_ids"] == []
|
||||
|
||||
|
||||
def test_section_id_exact_duplicate_collision_detected():
|
||||
"""Codex #14 invariant : exact `S3-1` colliding with another exact `S3-1`
|
||||
IS detected as a collision (whole-skip auto, uncovered trace).
|
||||
"""
|
||||
# Auto unit with MOCK_S3-1 (exact id) sits at bottom
|
||||
auto_top = _FakeUnit(source_section_ids=["MOCK_other"], frame_template_id="MOCK_T_other")
|
||||
auto_bottom = _FakeUnit(source_section_ids=["MOCK_S3-1", "MOCK_S3-2"], frame_template_id="MOCK_T_pair")
|
||||
units = [auto_top, auto_bottom]
|
||||
positions = ["top", "bottom"]
|
||||
# Override pulls MOCK_S3-1 to top — exact-id collision with auto_bottom's S3-1.
|
||||
overrides = {"top": ["MOCK_S3-1"]}
|
||||
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_other", "MOCK_S3-1", "MOCK_S3-2"]}
|
||||
override_frames = {"MOCK_S3-1": "MOCK_T_for_S3_1"}
|
||||
|
||||
plan, summary = _build_position_assignment_plan(
|
||||
units=units, positions=positions,
|
||||
override_section_assignments=overrides,
|
||||
sections_by_id=sections_by_id,
|
||||
override_frames=override_frames,
|
||||
)
|
||||
|
||||
by_pos = {p["position"]: p for p in plan}
|
||||
# top : override wins (replaces auto_top's MOCK_other)
|
||||
assert by_pos["top"]["assignment_source"] == "cli_override"
|
||||
assert by_pos["top"]["source_section_ids"] == ["MOCK_S3-1"]
|
||||
# bottom : exact-id collision with override's MOCK_S3-1 → whole skip
|
||||
assert by_pos["bottom"]["assignment_source"] == "empty"
|
||||
assert by_pos["bottom"]["skipped_reason"] == "override_collision"
|
||||
skipped = by_pos["bottom"]["skipped_collided_auto_units"]
|
||||
assert skipped and skipped[0]["unit_id"] == "MOCK_S3-1+MOCK_S3-2"
|
||||
# MOCK_S3-2 is uncovered (exact-id only, no automatic split)
|
||||
assert "MOCK_S3-2" in summary["uncovered_section_ids"]
|
||||
|
||||
|
||||
def test_section_id_distinct_ids_coexist_in_different_positions():
|
||||
"""Codex #14 invariant : S3 and S3-1 (distinct exact ids) can coexist in
|
||||
different positions without collision or false uncoverage."""
|
||||
# Auto plan : S3 at top
|
||||
auto = _FakeUnit(source_section_ids=["MOCK_S3"], frame_template_id="MOCK_T_S3")
|
||||
units = [auto]
|
||||
positions = ["top", "bottom"]
|
||||
# Override : S3-1 into bottom (no overlap with S3 since exact-id only)
|
||||
overrides = {"bottom": ["MOCK_S3-1"]}
|
||||
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_S3", "MOCK_S3-1"]}
|
||||
override_frames = {"MOCK_S3-1": "MOCK_T_S3_1"}
|
||||
|
||||
plan, summary = _build_position_assignment_plan(
|
||||
units=units, positions=positions,
|
||||
override_section_assignments=overrides,
|
||||
sections_by_id=sections_by_id,
|
||||
override_frames=override_frames,
|
||||
)
|
||||
|
||||
by_pos = {p["position"]: p for p in plan}
|
||||
assert by_pos["top"]["source_section_ids"] == ["MOCK_S3"]
|
||||
assert by_pos["top"]["skipped_reason"] is None
|
||||
assert by_pos["bottom"]["source_section_ids"] == ["MOCK_S3-1"]
|
||||
assert summary["uncovered_section_ids"] == [] # no false uncoverage
|
||||
|
||||
|
||||
# ─── Codex #13 Blocker 2 integration proof ───────────────────────────────
|
||||
# End-to-end pipeline run with `--override-section-assignment top=03-2` on
|
||||
# sample 03 MDX. Asserts the override is reflected in the actual render-path
|
||||
# artifacts (zones_data, debug_zones, Step 9 application_plan, Step 20
|
||||
# slide_status, debug.json `zones`) — NOT only in `comp_debug.
|
||||
# section_assignment_plan`. Without this proof, the helper unit tests above
|
||||
# could pass while units/render_records/zones still showed the pre-override
|
||||
# auto plan. Heavy: invokes Selenium overflow check.
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_integration_override_reflects_in_zones_data_step9_step20(tmp_path, monkeypatch):
|
||||
"""Codex #13 Blocker 2 (non-negotiable) — integration proof that
|
||||
`--override-section-assignment top=03-2` changes actual render-path
|
||||
artifacts (zones_data / debug_zones / Step 9 / Step 20 / debug.json),
|
||||
not only `comp_debug.section_assignment_plan`.
|
||||
|
||||
Sample 03 MDX has 2 sections (03-1, 03-2). Auto plan = [top=03-1,
|
||||
bottom=03-2]. Override forces top=03-2 → exact-id collision with auto
|
||||
bottom (whole-skip) + previous auto top (03-1) becomes uncovered.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from src import phase_z2_pipeline as pz2
|
||||
|
||||
PROJECT_ROOT = Path(pz2.__file__).resolve().parent.parent
|
||||
sample_path = PROJECT_ROOT / "samples" / "mdx" / "03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx"
|
||||
if not sample_path.is_file():
|
||||
pytest.skip(f"sample MDX not present: {sample_path}")
|
||||
|
||||
# Isolate run output under tmp_path so we do not pollute data/runs/.
|
||||
monkeypatch.setattr(pz2, "RUNS_DIR", tmp_path / "runs")
|
||||
|
||||
run_id = "test_imp06_override_integration"
|
||||
pz2.run_phase_z2_mvp1(
|
||||
sample_path,
|
||||
run_id=run_id,
|
||||
override_section_assignments={"top": ["03-2"]},
|
||||
)
|
||||
|
||||
run_dir = tmp_path / "runs" / run_id / "phase_z2"
|
||||
debug_path = run_dir / "debug.json"
|
||||
assert debug_path.is_file(), f"debug.json missing at {debug_path}"
|
||||
debug = json.loads(debug_path.read_text(encoding="utf-8"))
|
||||
|
||||
# ── 1) Render-path zones (debug.json `zones` == debug_zones in code) ──
|
||||
zones = {z["position"]: z for z in debug.get("zones", [])}
|
||||
assert "top" in zones, f"top zone missing; got {list(zones)}"
|
||||
assert "bottom" in zones, f"bottom zone missing; got {list(zones)}"
|
||||
|
||||
top = zones["top"]
|
||||
# Override reflected in render-path debug_zones, not only comp_debug.
|
||||
assert top["source_section_ids"] == ["03-2"], (
|
||||
f"top debug_zone did not reflect override; source_section_ids={top.get('source_section_ids')}"
|
||||
)
|
||||
assert top.get("assignment_source") == "cli_override", (
|
||||
f"top assignment_source != cli_override; got {top.get('assignment_source')}"
|
||||
)
|
||||
# Override-flag trace surfaces on the post-override debug_zone.
|
||||
assert top.get("section_assignment_override") is True, (
|
||||
f"top.section_assignment_override flag missing/false; got {top.get('section_assignment_override')}"
|
||||
)
|
||||
|
||||
bottom = zones["bottom"]
|
||||
# Exact-id collision (override 03-2 vs auto bottom 03-2) → whole-skip.
|
||||
# The empty zone record must be present in debug_zones (zone identity preserved).
|
||||
assert bottom.get("v4_template_id") == "__empty__" or bottom.get("merge_type") == "empty", (
|
||||
f"bottom should be empty after collision; got "
|
||||
f"v4_template_id={bottom.get('v4_template_id')}, merge_type={bottom.get('merge_type')}"
|
||||
)
|
||||
assert bottom.get("skipped_reason") == "override_collision", (
|
||||
f"bottom skipped_reason != override_collision; got {bottom.get('skipped_reason')}"
|
||||
)
|
||||
|
||||
# ── 2) Step 20 slide_status — coverage invariant ──
|
||||
step20_path = run_dir / "steps" / "step20_slide_status.json"
|
||||
assert step20_path.is_file(), f"step20 missing at {step20_path}"
|
||||
step20 = json.loads(step20_path.read_text(encoding="utf-8"))
|
||||
payload = step20.get("data") if "data" in step20 else step20
|
||||
filtered_ids = payload.get("filtered_section_ids") or []
|
||||
assert "03-1" in filtered_ids, (
|
||||
f"03-1 (previous auto top displaced by override) must appear in "
|
||||
f"filtered_section_ids; got {filtered_ids}"
|
||||
)
|
||||
# full_mdx_coverage must NOT be True when override displaces a section.
|
||||
assert payload.get("full_mdx_coverage") is not True, (
|
||||
f"full_mdx_coverage should be False after override displaces 03-1; got "
|
||||
f"{payload.get('full_mdx_coverage')}"
|
||||
)
|
||||
# Codex #10 Catch O list-shaped filtered_section_reasons must include the
|
||||
# override-uncovered entry pointing to the position whose plan dropped it.
|
||||
reasons = payload.get("filtered_section_reasons") or []
|
||||
override_uncovered_reasons = [
|
||||
r for r in reasons
|
||||
if isinstance(r, dict)
|
||||
and r.get("source") == "section_assignment_override"
|
||||
and "03-1" in (r.get("section_ids") or [])
|
||||
]
|
||||
assert override_uncovered_reasons, (
|
||||
f"filtered_section_reasons missing override-uncovered entry for 03-1; "
|
||||
f"got {reasons}"
|
||||
)
|
||||
|
||||
# ── 3) Step 9 application_plan — plan-aware additive fields per unit ──
|
||||
step09_path = run_dir / "steps" / "step09_application_plan.json"
|
||||
assert step09_path.is_file(), f"step09 missing at {step09_path}"
|
||||
step09 = json.loads(step09_path.read_text(encoding="utf-8"))
|
||||
step09_data = step09.get("data") if "data" in step09 else step09
|
||||
plan_units = step09_data.get("units") or []
|
||||
# The renderable unit for 03-2 must exist with override-aware fields.
|
||||
override_units = [
|
||||
u for u in plan_units
|
||||
if u.get("unit_id") == "03-2"
|
||||
and u.get("position") == "top"
|
||||
and u.get("assignment_source") == "cli_override"
|
||||
]
|
||||
assert override_units, (
|
||||
f"Step 9 application_plan did not carry plan-aware fields; "
|
||||
f"units={[(u.get('unit_id'), u.get('position'), u.get('assignment_source')) for u in plan_units]}"
|
||||
)
|
||||
assert override_units[0].get("section_assignment_override") is True
|
||||
|
||||
# ── 4) comp_debug — pre-existing plan/summary still present (regression) ──
|
||||
cd = debug.get("composition_planner_debug") or {}
|
||||
sa_summary = cd.get("section_assignment_summary") or {}
|
||||
# uncovered_section_ids carries the displaced auto-top section.
|
||||
assert "03-1" in (sa_summary.get("uncovered_section_ids") or []), (
|
||||
f"comp_debug.section_assignment_summary.uncovered_section_ids "
|
||||
f"missing 03-1; got {sa_summary.get('uncovered_section_ids')}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user