Files
C.E.L_Slide_test2/tests/test_phase_z2_structure_overrides.py
kyeongmin 4da22adb43
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
feat(#90): IMP-56 u1-u19 catch-up before final close (post-u20 push fix)
u1: text_overrides axis in user_overrides_io
u2: structure_overrides axis in user_overrides_io
u3: vite allowlist for new endpoints
u4: text_override_resolver
u5: Step 12 text_overrides apply in phase_z2_pipeline
u6: structure_override_resolver
u7: text_path_stamper
u8: SlideCanvas text-edit capture
u9: SlideCanvas structure-edit overlay
u10: userOverridesApi service extension
u11: designAgent types extension
u12: slidePlanUtils restore
u13: user_overrides endpoint tests
u14: user_overrides restore tests
u15: pipeline fallback tests
u16: edit-mode state + gating tests
u17: slide_base print mode CSS
u18: /api/connect endpoint (vite)
u19: /api/export endpoint (vite)

Recovery scope: 29 files (12 modified + 17 new). u20 already pushed in
9439575; this commit lands u1-u19 that were authored but not committed
before #90 was externally closed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:12:13 +09:00

217 lines
8.2 KiB
Python

"""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}]