Files
C.E.L_Slide_test2/tests/test_phase_z2_text_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

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