Files
C.E.L_Slide_test2/tests/test_phase_z2_section_assignment_override.py
T
KyeongminandClaude Opus 4.8 b836e79ee1 wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:03:42 +09:00

563 lines
25 KiB
Python

"""IMP-06 zone-section assignment override — helper unit tests (synthetic).
Lock per Claude #6 §4 L13 + Codex #2 R3 6 cases + 자체 catch 7-10 :
9 cases covering parse / helper assignment / collision / template ladder.
Fully synthetic per Codex #7 generalization guardrail (MOCK_ prefix).
NO real catalog template_id / frame_id, NO `v4_full32_result.yaml` dependency.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import pytest
from src.phase_z2_pipeline import _build_position_assignment_plan
# ─── Synthetic fixtures ──────────────────────────────────────────
@dataclass
class _FakeUnit:
"""Synthetic CompositionUnit stand-in. Only fields the helper reads."""
source_section_ids: list[str]
template_id: Optional[str] = None
frame_template_id: Optional[str] = None
@dataclass
class _FakeSection:
"""Synthetic MdxSection stand-in. Only fields the helper reads."""
section_id: str
raw_content: str = "- item A\n- item B\n"
# ─── Case 1 : single override + non-conflicting auto retain ─────────────
def test_single_zone_override_retains_non_conflicting_auto():
"""Claude #6 L13 case 4 — single override on `top`, auto units do not overlap.
Expected: top = override unit; bottom = auto unit retained.
"""
units = [
_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto_top"),
_FakeUnit(source_section_ids=["MOCK_S2"], frame_template_id="MOCK_T_auto_bottom"),
]
positions = ["top", "bottom"]
overrides = {"top": ["MOCK_S3"]}
sections_by_id = {"MOCK_S3": _FakeSection("MOCK_S3")}
override_frames = {"MOCK_S3": "MOCK_T_for_S3"} # ladder step 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"]["assignment_source"] == "cli_override"
assert by_pos["top"]["source_section_ids"] == ["MOCK_S3"]
assert by_pos["top"]["template_id"] == "MOCK_T_for_S3"
assert by_pos["bottom"]["assignment_source"] == "auto"
assert by_pos["bottom"]["source_section_ids"] == ["MOCK_S2"]
assert summary["applied_count"] == 1
assert summary["skipped_count"] == 0
# ─── Case 2 : collision — override wins, auto whole-skipped ──────────────
def test_override_collision_whole_skip_no_split_uncovered_traced():
"""Codex #2 R1 example : override section overlaps an auto merged unit.
Expected: override wins; auto [MOCK_S1, MOCK_S2] skipped whole (no split);
MOCK_S2 reported as uncovered.
"""
auto_merged = _FakeUnit(
source_section_ids=["MOCK_S1", "MOCK_S2"],
frame_template_id="MOCK_T_merged",
)
auto_solo = _FakeUnit(source_section_ids=["MOCK_S3"], frame_template_id="MOCK_T_solo")
units = [auto_merged, auto_solo]
positions = ["top", "bottom"]
overrides = {"top": ["MOCK_S1"]}
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_S1", "MOCK_S2", "MOCK_S3"]}
override_frames = {"MOCK_S1": "MOCK_T_override_S1"}
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; auto_merged that sat at top is replaced.
assert by_pos["top"]["assignment_source"] == "cli_override"
assert by_pos["top"]["source_section_ids"] == ["MOCK_S1"]
# No split : MOCK_S2 (the other half of the auto merged unit) is NOT
# re-extracted into the replaced position; instead it surfaces as uncovered.
assert by_pos["top"]["previous_source_section_ids"] == ["MOCK_S1", "MOCK_S2"]
assert by_pos["top"]["uncovered_section_ids"] == ["MOCK_S2"]
# IMP-06 Stage 4 (Codex #10): replaced_auto_unit populated for same-position
# override replacement (previous auto unit had different sections).
assert by_pos["top"]["replaced_auto_unit"] is not None
assert by_pos["top"]["replaced_auto_unit"]["unit_id"] == "MOCK_S1+MOCK_S2"
assert by_pos["top"]["replaced_auto_unit"]["reason"] == "same_position_override_replacement"
# bottom : non-overlapping auto_solo is retained (no collision).
assert by_pos["bottom"]["assignment_source"] == "auto"
assert by_pos["bottom"]["source_section_ids"] == ["MOCK_S3"]
assert by_pos["bottom"]["replaced_auto_unit"] is None
# Summary aggregates MOCK_S2 as the global uncovered section.
assert summary["uncovered_section_ids"] == ["MOCK_S2"]
# ─── Case 3 : template ladder step 1 (override_frames wins) ─────────────
def test_template_resolution_ladder_step1_override_frame_wins():
"""Codex #4 T1 ladder step 1 : --override-frame exact unit_id wins."""
units = [_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto")]
positions = ["top"]
overrides = {"top": ["MOCK_S1"]}
sections_by_id = {"MOCK_S1": _FakeSection("MOCK_S1")}
override_frames = {"MOCK_S1": "MOCK_T_explicit_override"}
plan, _ = _build_position_assignment_plan(
units=units, positions=positions,
override_section_assignments=overrides,
sections_by_id=sections_by_id,
override_frames=override_frames,
)
assert plan[0]["template_id"] == "MOCK_T_explicit_override"
assert plan[0]["skipped_reason"] is None
# ─── Case 4 : template ladder step 2 (exact auto unit reuse) ────────────
def test_template_resolution_ladder_step2_exact_auto_reuse():
"""Codex #4 T1 ladder step 2 : no override_frame; exact existing auto unit -> reuse."""
units = [_FakeUnit(source_section_ids=["MOCK_S1", "MOCK_S2"], frame_template_id="MOCK_T_auto_merged")]
positions = ["top"]
overrides = {"top": ["MOCK_S1", "MOCK_S2"]}
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_S1", "MOCK_S2"]}
plan, _ = _build_position_assignment_plan(
units=units, positions=positions,
override_section_assignments=overrides,
sections_by_id=sections_by_id,
override_frames=None, # no explicit frame override
)
# ladder step 2 hits : exact auto unit [MOCK_S1, MOCK_S2] reuse
assert plan[0]["template_id"] == "MOCK_T_auto_merged"
assert plan[0]["skipped_reason"] is None
# ─── Case 5 : template ladder step 4 (ad-hoc multi-section fail) ─────────
def test_template_resolution_ladder_step4_ad_hoc_multi_section_fail():
"""Codex #4 Additional lock : ad-hoc multi-section override without exact auto +
without explicit --override-frame -> skipped_reason = 'ad_hoc_merged_no_template'.
"""
units = [
_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_a"),
_FakeUnit(source_section_ids=["MOCK_S2"], frame_template_id="MOCK_T_b"),
]
positions = ["top"]
overrides = {"top": ["MOCK_S1", "MOCK_S2"]} # ad-hoc merge, no exact auto
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_S1", "MOCK_S2"]}
plan, _ = _build_position_assignment_plan(
units=units, positions=positions,
override_section_assignments=overrides,
sections_by_id=sections_by_id,
override_frames=None,
)
assert plan[0]["template_id"] is None
assert plan[0]["skipped_reason"] == "ad_hoc_merged_no_template"
# ─── Case 6 : unit_id naming convention ─────────────────────────────────
def test_unit_id_naming_convention_consistent_for_auto_and_override():
"""Codex T2 + Claude #4 catch 8 : unit_id = '+'.join(source_section_ids)."""
units = [
_FakeUnit(source_section_ids=["MOCK_S1", "MOCK_S2"], frame_template_id="MOCK_T_merged"),
]
positions = ["top", "bottom"]
overrides = {"bottom": ["MOCK_S3"]}
sections_by_id = {"MOCK_S3": _FakeSection("MOCK_S3")}
override_frames = {"MOCK_S3": "MOCK_T_S3"}
plan, _ = _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}
# auto merged unit
assert by_pos["top"]["unit_id"] == "MOCK_S1+MOCK_S2"
# single-section override
assert by_pos["bottom"]["unit_id"] == "MOCK_S3"
# ─── Case 7 : previous_source_section_ids semantics (position history) ──
def test_previous_source_section_ids_records_same_position_auto_history():
"""Codex T3 + Claude #4 catch 9 : previous_source_section_ids = the auto
assignment that occupied the SAME position before override applied.
"""
units = [
_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_a"),
_FakeUnit(source_section_ids=["MOCK_S2"], frame_template_id="MOCK_T_b"),
]
positions = ["top", "bottom"]
overrides = {"top": ["MOCK_S3"]}
sections_by_id = {"MOCK_S3": _FakeSection("MOCK_S3")}
override_frames = {"MOCK_S3": "MOCK_T_S3"}
plan, _ = _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 WAS MOCK_S1 before override
assert by_pos["top"]["previous_source_section_ids"] == ["MOCK_S1"]
# ─── Case 8 : empty position when no auto unit available ───────────────
def test_position_with_no_auto_unit_marked_empty():
"""When `units` has fewer entries than `positions`, extra positions = empty."""
units = [_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_a")]
positions = ["top", "bottom"] # bottom has no auto unit
sections_by_id = {"MOCK_S1": _FakeSection("MOCK_S1")}
plan, summary = _build_position_assignment_plan(
units=units, positions=positions,
override_section_assignments=None,
sections_by_id=sections_by_id,
override_frames=None,
)
by_pos = {p["position"]: p for p in plan}
assert by_pos["bottom"]["assignment_source"] == "empty"
assert by_pos["bottom"]["skipped_reason"] == "no_auto_unit_available"
assert summary["applied_count"] == 0
# ─── Case 9b : replaced_auto_unit distinguishes same-sections vs different ──
def test_replaced_auto_unit_only_when_previous_auto_differs_from_override():
"""Codex #10 R1 + Claude #10 Catch L : replaced_auto_unit populated only when
the same position previously had an auto unit AND that auto unit had
different sections than the override. Same sections (override just swaps
template via --override-frame) yields replaced_auto_unit = None.
"""
# Case A : same sections → replaced_auto_unit None (just template swap)
units_a = [_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto")]
plan_a, _ = _build_position_assignment_plan(
units=units_a,
positions=["top"],
override_section_assignments={"top": ["MOCK_S1"]},
sections_by_id={"MOCK_S1": _FakeSection("MOCK_S1")},
override_frames={"MOCK_S1": "MOCK_T_explicit_swap"},
)
assert plan_a[0]["replaced_auto_unit"] is None
assert plan_a[0]["template_id"] == "MOCK_T_explicit_swap"
# Case B : different sections → replaced_auto_unit populated
units_b = [_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto")]
plan_b, _ = _build_position_assignment_plan(
units=units_b,
positions=["top"],
override_section_assignments={"top": ["MOCK_S2"]},
sections_by_id={"MOCK_S2": _FakeSection("MOCK_S2")},
override_frames={"MOCK_S2": "MOCK_T_for_S2"},
)
assert plan_b[0]["replaced_auto_unit"] is not None
assert plan_b[0]["replaced_auto_unit"]["unit_id"] == "MOCK_S1"
assert plan_b[0]["replaced_auto_unit"]["source_section_ids"] == ["MOCK_S1"]
assert plan_b[0]["replaced_auto_unit"]["reason"] == "same_position_override_replacement"
# ─── Case 9 : summary aggregation invariants ───────────────────────────
def test_summary_aggregation_counts_applied_skipped_uncovered():
"""Claude #6 L12 single source of truth : summary derives from plan."""
auto_merged = _FakeUnit(
source_section_ids=["MOCK_S1", "MOCK_S2"],
frame_template_id="MOCK_T_merged",
)
units = [auto_merged]
positions = ["top", "bottom"]
# Two overrides : top=MOCK_S1 (causes collision with auto_merged at bottom)
overrides = {"top": ["MOCK_S1"]}
sections_by_id = {sid: _FakeSection(sid) for sid in ["MOCK_S1", "MOCK_S2"]}
override_frames = {"MOCK_S1": "MOCK_T_override_S1"}
plan, summary = _build_position_assignment_plan(
units=units, positions=positions,
override_section_assignments=overrides,
sections_by_id=sections_by_id,
override_frames=override_frames,
)
# Summary derives from plan : 1 applied, 1 collision skip, 1 uncovered.
assert summary["applied_count"] == 1
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; the no-drop fallback should keep
the displaced real section (03-1) in the other zone rather than silently
dropping it.
"""
import json
from pathlib import Path
from uuid import uuid4
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}")
# Keep the controlled integration run under the project root. Some asset
# trace code records paths relative to PROJECT_ROOT, so pytest tmp_path
# outside the repo is a separate path-hygiene axis rather than this
# override/status-laundering probe.
runs_root = PROJECT_ROOT / "data" / "test_tmp" / f"section_assignment_{uuid4().hex}"
monkeypatch.setattr(pz2, "RUNS_DIR", runs_root)
run_id = "test_imp06_override_integration"
pz2.run_phase_z2_mvp1(
sample_path,
run_id=run_id,
override_layout="horizontal-2",
override_section_assignments={"top": ["03-2"]},
)
run_dir = runs_root / 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"]
# Current no-drop policy preserves the displaced real section in the other
# zone instead of creating an empty/collision placeholder.
assert bottom.get("source_section_ids") == ["03-1"], (
f"bottom should preserve displaced real section 03-1; got "
f"{bottom.get('source_section_ids')}"
)
assert bottom.get("merge_type") == "no_drop_fallback_group", (
f"bottom merge_type should be no_drop_fallback_group; got "
f"{bottom.get('merge_type')}"
)
# ── 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" not in filtered_ids, (
f"03-1 should be preserved by no-drop fallback, not filtered; got {filtered_ids}"
)
assert payload.get("full_mdx_coverage") is True, (
f"full_mdx_coverage should remain True because 03-1 is preserved; got "
f"{payload.get('full_mdx_coverage')}"
)
# Codex #10 Catch O list-shaped filtered_section_reasons must include the
# override-uncovered entries should not be emitted because the displaced
# section is preserved by no-drop fallback.
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 not override_uncovered_reasons, (
f"filtered_section_reasons should not mark preserved 03-1 as uncovered; "
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 {}
# no-drop fallback keeps the displaced auto-top section covered.
assert "03-1" not in (sa_summary.get("uncovered_section_ids") or []), (
f"comp_debug.section_assignment_summary.uncovered_section_ids "
f"should not include preserved 03-1; got {sa_summary.get('uncovered_section_ids')}"
)