"""IMP-56 (#90) u7 — Step 12 ``structure_overrides`` apply unit tests. Synthetic — exercises ``_apply_structure_overrides_to_zones`` directly without running the full Phase Z 22-step pipeline. The helper is decoupled from ``MdxSection`` / ``CompositionUnit`` graphs and only consumes a minimal ``[{position, slot_payload}, ...]`` zone list, so a synthetic fixture is sufficient to lock the contract. Coverage axes (Stage 2 plan u7 + Stage 1 binding contract) : - reorder happy path : ``slot_order`` partial reorder mutates ``zone['slot_payload']`` key order in place (caller reference stays valid via clear+update rebuild contract documented at u6). - hide happy path : ``hidden_slots`` pops the listed keys. - stale slot_key : absent slot_keys silently no-op (count toward ``skipped_zones`` if the whole override produces no mutation). - SCOPE LOCK : frame-swap-shaped inner keys (``frame_id``, ``template_id``) are dropped by the u6 validate gate and therefore never reach the apply path here. - raw_content preservation : per-slot ``list[str]`` line content untouched; out-of-band sentinels (mirror of ``debug_zones`` graph) stay byte-identical. - audit shape : ``applied_zones`` / ``skipped_zones`` / ``per_zone`` keys present and counts consistent with the input batch. - empty / ``None`` batch is a no-op (empty audit). Fully synthetic per Codex generalization guardrail (MOCK_ prefix). NO real catalog template_id / frame_id, NO ``v4_full32_result.yaml`` dependency. """ from __future__ import annotations import pytest from src.phase_z2_pipeline import _apply_structure_overrides_to_zones # ─── Synthetic fixture helpers ────────────────────────────────────── def _zone(position: str, slot_payload: dict) -> dict: """Minimal zone dict mirroring the Step 12 ``zones_data[i]`` shape.""" return { "position": position, "template_id": "MOCK_T_phase_z2_structure_overrides", "slot_payload": slot_payload, "content_weight": 1.0, "min_height_px": 200, } # ─── Case 1 : reorder happy path ────────────────────────────────────── def test_apply_reorders_slot_payload_keys_in_place(): payload = {"slot_a": ["A"], "slot_b": ["B"], "slot_c": ["C"]} zones = [_zone("top", payload)] overrides = {"top": {"slot_order": ["slot_c", "slot_a"]}} audit = _apply_structure_overrides_to_zones(overrides, zones) # Caller reference (payload) stays valid via clear+update rebuild. assert zones[0]["slot_payload"] is payload assert list(payload.keys()) == ["slot_c", "slot_a", "slot_b"] # Per-slot list[str] content untouched (raw_content invariant). assert payload["slot_a"] == ["A"] assert payload["slot_b"] == ["B"] assert payload["slot_c"] == ["C"] assert audit["applied_zones"] == 1 assert audit["skipped_zones"] == 0 assert audit["per_zone"] == [{"position": "top", "mutated": True}] # ─── Case 2 : hide happy path ───────────────────────────────────────── def test_apply_hides_listed_slot_keys(): payload = {"slot_a": ["A"], "slot_b": ["B"], "slot_c": ["C"]} zones = [_zone("top", payload)] overrides = {"top": {"hidden_slots": ["slot_b"]}} audit = _apply_structure_overrides_to_zones(overrides, zones) assert "slot_b" not in payload assert list(payload.keys()) == ["slot_a", "slot_c"] assert audit["applied_zones"] == 1 assert audit["per_zone"] == [{"position": "top", "mutated": True}] # ─── Case 3 : stale slot_key — frame swap / layout regression ───────── def test_stale_slot_key_silently_no_op(): """Absent slot_keys produce no mutation; the zone counts toward skipped_zones.""" payload = {"slot_a": ["A"]} zones = [_zone("top", payload)] overrides = { "top": { "hidden_slots": ["slot_missing"], # absent — no-op "slot_order": ["slot_also_missing"], # absent — no-op }, } audit = _apply_structure_overrides_to_zones(overrides, zones) assert list(payload.keys()) == ["slot_a"] assert audit["applied_zones"] == 0 assert audit["skipped_zones"] == 1 assert audit["per_zone"] == [{"position": "top", "mutated": False}] # ─── Case 4 : SCOPE LOCK — frame swap shape dropped at validate ─────── def test_frame_swap_keys_dropped_at_validate_no_mutation(): payload = {"slot_a": ["A"], "slot_b": ["B"]} zones = [_zone("top", payload)] # frame_id / template_id / slot_payload as inner keys are the canonical # frame-swap / DOM-rebuild shapes the SCOPE LOCK rejects. overrides = { "top": { "frame_id": "MOCK_OTHER_FRAME", "template_id": "MOCK_OTHER_TEMPLATE", "slot_payload": {"slot_a": ["overwritten"]}, }, } audit = _apply_structure_overrides_to_zones(overrides, zones) # No mutation: validate gate drops the whole zone payload (no allowed # inner key remains), so the zone never reaches the apply loop. assert payload == {"slot_a": ["A"], "slot_b": ["B"]} assert audit["applied_zones"] == 0 assert audit["skipped_zones"] == 0 assert audit["per_zone"] == [] # ─── Case 5 : raw_content preservation invariant ────────────────────── def test_raw_content_sentinel_untouched(): """Helper must not mutate anything outside zone['slot_payload'] AND must not mutate per-slot list[str] line content.""" raw_sentinel = ["MOCK_S1", "MOCK_S2"] payload = {"slot_a": ["line 1", "line 2"], "slot_b": ["line 3"]} zones = [_zone("top", payload)] zones[0]["source_section_ids_sentinel"] = raw_sentinel # out-of-band zones[0]["raw_content_sentinel"] = "- line 1\n- line 2\n" _apply_structure_overrides_to_zones( {"top": {"slot_order": ["slot_b", "slot_a"]}}, zones, ) # Out-of-band fields untouched. assert zones[0]["source_section_ids_sentinel"] is raw_sentinel assert zones[0]["source_section_ids_sentinel"] == ["MOCK_S1", "MOCK_S2"] assert zones[0]["raw_content_sentinel"] == "- line 1\n- line 2\n" # Per-slot list[str] line content byte-identical. assert payload["slot_a"] == ["line 1", "line 2"] assert payload["slot_b"] == ["line 3"] # ─── Case 6 : empty / None / irrelevant batch is no-op ──────────────── @pytest.mark.parametrize( "batch", [ None, {}, {"top": {}}, # empty per-zone {"missing_zone": {"slot_order": ["slot_a"]}}, # stale zone_id ], ) def test_empty_or_irrelevant_batch_is_noop(batch): payload = {"slot_a": ["A"]} zones = [_zone("top", payload)] audit = _apply_structure_overrides_to_zones(batch, zones) assert list(payload.keys()) == ["slot_a"] assert audit["applied_zones"] == 0 assert audit["skipped_zones"] == 0 assert audit["per_zone"] == [] # ─── Case 7 : zone without slot_payload skipped (defensive) ─────────── def test_zone_without_slot_payload_skipped(): zones = [{"position": "top"}] # no slot_payload key (defensive contract) audit = _apply_structure_overrides_to_zones( {"top": {"slot_order": ["slot_a"]}}, zones, ) assert audit["per_zone"] == [] assert audit["applied_zones"] == 0 assert audit["skipped_zones"] == 0 # ─── Case 8 : combined reorder + hide in a single zone ──────────────── def test_combined_reorder_and_hide_in_one_zone(): payload = {"slot_a": ["A"], "slot_b": ["B"], "slot_c": ["C"]} zones = [_zone("top", payload)] overrides = { "top": { "hidden_slots": ["slot_b"], "slot_order": ["slot_c", "slot_a"], }, } audit = _apply_structure_overrides_to_zones(overrides, zones) assert list(payload.keys()) == ["slot_c", "slot_a"] assert audit["applied_zones"] == 1 assert audit["per_zone"] == [{"position": "top", "mutated": True}]