Refs #6 Stage 4 split per Codex #10 acceptance: this commit lands the schema + trace refinements required before the render-path rewiring. The actual units/zones_data/Step 9/Step 20 plan-driven materialization remains in Stage 4 part 2 (follow-up commit) so each commit is reviewable on its own and regression-safe. - _build_position_assignment_plan: add replaced_auto_unit field. Populated only when the explicitly overridden position already held an auto unit AND that auto unit had different source_section_ids than the override. Documents a same-position override replacement as a distinct audit fact, separate from skipped_collided_auto_units which captures cross-position whole-skips per the locked collision policy. - Backfill replaced_auto_unit = None on the empty/collision/auto branches for schema-stable consumers. - Update the override-application comment near the helper invocation so it no longer claims the helper "reorders units"; Stage 4 part 2 will be the commit that wires the plan into the actual render path. - Helper unit tests: assert replaced_auto_unit shape in the collision scenario and add a dedicated case that distinguishes same-sections (template swap via --override-frame -> None) from different-sections same-position replacement (populated, reason="same_position_override_replacement"). No AI, no calculate_fit, no full planner rerun, no frontend, no sample hardcoding. plan_composition() signature preserved. helper remains pure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
328 lines
14 KiB
Python
328 lines
14 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, field
|
|
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"
|