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>
159 lines
6.3 KiB
Python
159 lines
6.3 KiB
Python
"""IMP-56 (#90) u5 — Step 12 ``text_overrides`` apply unit tests.
|
|
|
|
Synthetic — exercises ``_apply_text_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 u5 + Stage 1 binding contract) :
|
|
|
|
- sanitized batch : malformed text_path / non-string value drops per-row
|
|
(mirrors ``image_id_stamper.build_image_overrides_style`` u7 tolerance).
|
|
- stale path : frame swap / layout regression → ``skipped``, not error.
|
|
- raw_content preservation : helper never touches ``debug_zones`` / unit
|
|
graph (asserted by zero-mutation on an out-of-band sentinel mapping).
|
|
- audit shape : ``applied`` / ``skipped`` / ``per_zone`` keys present and
|
|
counts consistent with the input batch.
|
|
- empty / ``None`` override input 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_text_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_text_overrides",
|
|
"slot_payload": slot_payload,
|
|
"content_weight": 1.0,
|
|
"min_height_px": 200,
|
|
}
|
|
|
|
|
|
# ─── Case 1 : happy path — list[str] slot mutation ────────────────────
|
|
|
|
|
|
def test_apply_replaces_list_line_in_place():
|
|
zones = [
|
|
_zone("top", {"slot_title": ["original headline"], "slot_body": ["a", "b", "c"]}),
|
|
_zone("bottom", {"slot_caption": ["caption A"]}),
|
|
]
|
|
overrides = {
|
|
"top": {"slot_title.0": "edited headline", "slot_body.1": "edited B"},
|
|
}
|
|
|
|
audit = _apply_text_overrides_to_zones(overrides, zones)
|
|
|
|
assert zones[0]["slot_payload"]["slot_title"] == ["edited headline"]
|
|
assert zones[0]["slot_payload"]["slot_body"] == ["a", "edited B", "c"]
|
|
# bottom zone untouched (no override entry)
|
|
assert zones[1]["slot_payload"]["slot_caption"] == ["caption A"]
|
|
assert audit["applied"] == 2
|
|
assert audit["skipped"] == 0
|
|
assert audit["per_zone"] == [{"position": "top", "applied": 2, "skipped": 0}]
|
|
|
|
|
|
# ─── Case 2 : stale text_path — frame swap / layout regression ────────
|
|
|
|
|
|
def test_stale_text_path_skipped_silently():
|
|
"""Absent slot_key + out-of-range line_index both count as skipped, not errors."""
|
|
zones = [_zone("top", {"slot_title": ["only one line"]})]
|
|
overrides = {
|
|
"top": {
|
|
"slot_title.0": "ok", # applied
|
|
"slot_title.99": "out of range", # skipped (idx > len)
|
|
"slot_missing.0": "stale frame", # skipped (slot absent)
|
|
},
|
|
}
|
|
|
|
audit = _apply_text_overrides_to_zones(overrides, zones)
|
|
|
|
assert zones[0]["slot_payload"]["slot_title"] == ["ok"]
|
|
assert "slot_missing" not in zones[0]["slot_payload"]
|
|
assert audit["applied"] == 1
|
|
assert audit["skipped"] == 2
|
|
assert audit["per_zone"][0] == {"position": "top", "applied": 1, "skipped": 2}
|
|
|
|
|
|
# ─── Case 3 : malformed input — per-entry tolerance ────────────────────
|
|
|
|
|
|
def test_malformed_entries_dropped_in_validate():
|
|
"""Non-string value / malformed text_path drop in ``validate_text_overrides``."""
|
|
zones = [_zone("top", {"slot_title": ["original", "second"]})]
|
|
overrides = {
|
|
"top": {
|
|
"slot_title.0": "good", # kept
|
|
"slot_title.bad": "ignored", # dropped (non-int idx)
|
|
"slot_title.1": 123, # dropped (non-str value)
|
|
"no_dot": "ignored", # dropped (missing '.')
|
|
},
|
|
"": {"slot_title.0": "empty zone id dropped"}, # zone_id sanitization
|
|
123: {"slot_title.0": "non-string zone id dropped"},
|
|
}
|
|
|
|
audit = _apply_text_overrides_to_zones(overrides, zones)
|
|
|
|
# only the well-formed entry applied
|
|
assert zones[0]["slot_payload"]["slot_title"] == ["good", "second"]
|
|
assert audit["applied"] == 1
|
|
assert audit["skipped"] == 0
|
|
|
|
|
|
# ─── Case 4 : raw_content preservation invariant ──────────────────────
|
|
|
|
|
|
def test_raw_content_sentinel_untouched():
|
|
"""Helper must not mutate anything outside ``zone['slot_payload']``.
|
|
|
|
Out-of-band fields (mirror of ``debug_zones[i].source_section_ids`` /
|
|
MdxSection graph) stay byte-identical — Stage 1 binding contract.
|
|
"""
|
|
raw_sentinel = ["MOCK_S1", "MOCK_S2"]
|
|
zones = [_zone("top", {"slot_title": ["original"]})]
|
|
zones[0]["source_section_ids_sentinel"] = raw_sentinel # out-of-band
|
|
zones[0]["raw_content_sentinel"] = "- original bullet\n- second\n"
|
|
|
|
_apply_text_overrides_to_zones({"top": {"slot_title.0": "edited"}}, zones)
|
|
|
|
assert zones[0]["source_section_ids_sentinel"] is raw_sentinel # same object
|
|
assert zones[0]["source_section_ids_sentinel"] == ["MOCK_S1", "MOCK_S2"]
|
|
assert zones[0]["raw_content_sentinel"] == "- original bullet\n- second\n"
|
|
|
|
|
|
# ─── Case 5 : empty / None batch is no-op ──────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize("payload", [None, {}, {"top": {}}, {"missing_zone": {"slot.0": "x"}}])
|
|
def test_empty_or_irrelevant_batch_is_noop(payload):
|
|
zones = [_zone("top", {"slot_title": ["unchanged"]})]
|
|
audit = _apply_text_overrides_to_zones(payload, zones)
|
|
|
|
assert zones[0]["slot_payload"]["slot_title"] == ["unchanged"]
|
|
assert audit["applied"] == 0
|
|
assert audit["skipped"] == 0
|
|
|
|
|
|
# ─── Case 6 : zone without slot_payload skipped (defensive) ───────────
|
|
|
|
|
|
def test_zone_without_slot_payload_skipped():
|
|
zones = [{"position": "top"}] # no slot_payload key (defensive contract)
|
|
audit = _apply_text_overrides_to_zones({"top": {"slot.0": "x"}}, zones)
|
|
assert audit["per_zone"] == []
|
|
assert audit["applied"] == 0
|
|
assert audit["skipped"] == 0
|