"""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" # ─── 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')}" )