Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
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>
217 lines
8.2 KiB
Python
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}]
|