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

213 lines
8.2 KiB
Python

"""IMP-56 (#90) u9 — Step 13 ``text_path_stamper`` wiring tests.
Verifies that :func:`src.phase_z2_pipeline.render_slide` stamps each
rendered ``text-line`` opening tag with
``data-text-path="{slot_key}.{line_index}"`` via the u8 stamper
(``src.text_path_stamper.stamp_zone_html``). This is the wiring unit
companion to the u8 module-level tests at
``tests/test_text_path_stamper.py``.
Coverage axes (Stage 2 plan u9 + Stage 1 binding contract) :
- happy path : real ``bim_current_problems_paired`` template emits
``text-line`` divs for list-valued slots; the stamper attaches
``data-text-path`` with matching ``{slot_key}.{line_index}``.
- non-list slots skipped : ``title`` / ``row_*_left_label`` (scalars)
do NOT receive ``data-text-path`` attributes.
- empty list slots emit no stamps : rows whose body list is empty
contribute zero stamps.
- deterministic : repeated calls produce byte-identical HTML
(no nondeterministic mutation of ``slot_payload`` between renders).
- empty zone : the ``__empty__`` template_id short-circuit emits no
``data-text-path`` (the stamper short-circuits on empty stamps).
Fully synthetic slot_payload — no real Phase Z run, no
``v4_full32_result.yaml`` dependency. Uses the real
``bim_current_problems_paired`` family template only to exercise the
genuine Jinja2 + slide_base render path.
"""
from __future__ import annotations
import re
import pytest
from src.phase_z2_pipeline import render_slide
# ─── Fixture helpers ────────────────────────────────────────────────
def _layout_css() -> dict:
"""Minimal valid layout_css for a single-zone slide.
Mirrors tests/phase_z2/test_slide_base_embedded_mode.py shape.
"""
return {"areas": '"primary"', "cols": "1fr", "rows": "1fr"}
def _paired_slot_payload(
*,
left_lines: list[str] | None = None,
right_lines: list[str] | None = None,
) -> dict:
"""Build a slot_payload for the bim_current_problems_paired family.
Only row_1 is populated by default; rows 2-4 stay empty so the
template short-circuits to zero text-line divs for them.
"""
left_lines = left_lines if left_lines is not None else ["L1a", "L1b"]
right_lines = right_lines if right_lines is not None else ["R1a"]
payload: dict = {
"title": "Synthetic Title",
"row_1_left_label": "left pill 1",
"row_1_left_body": [{"text": t, "indent": 0} for t in left_lines],
"row_1_right_label": "right pill 1",
"row_1_right_body": [{"text": t, "indent": 0} for t in right_lines],
}
for r in (2, 3, 4):
payload[f"row_{r}_left_label"] = f"left pill {r}"
payload[f"row_{r}_left_body"] = []
payload[f"row_{r}_right_label"] = f"right pill {r}"
payload[f"row_{r}_right_body"] = []
return payload
def _zone(template_id: str, slot_payload: dict) -> dict:
return {
"position": "primary",
"template_id": template_id,
"slot_payload": slot_payload,
}
def _render(zones: list[dict]) -> str:
return render_slide(
slide_title="t",
slide_footer=None,
zones_data=zones,
layout_preset="single",
layout_css=_layout_css(),
gap_px=14,
embedded_mode="embedded",
)
# ─── Case 1 : happy path — list-valued slots stamped ─────────────────
def test_render_slide_stamps_text_path_per_line():
"""Each list-valued slot line gets data-text-path={slot}.{index}."""
payload = _paired_slot_payload(
left_lines=["left line A", "left line B"],
right_lines=["right line A"],
)
html = _render([_zone("bim_current_problems_paired", payload)])
# left body 2 lines + right body 1 line = 3 stamps in row 1.
assert 'data-text-path="row_1_left_body.0"' in html
assert 'data-text-path="row_1_left_body.1"' in html
assert 'data-text-path="row_1_right_body.0"' in html
# row 2-4 are empty → no stamps for those slot_keys.
assert "row_2_left_body" not in html
assert "row_3_right_body" not in html
def test_stamps_preserve_class_attribute():
"""data-text-path injected before existing class attribute, both present."""
payload = _paired_slot_payload(left_lines=["only left"], right_lines=[])
html = _render([_zone("bim_current_problems_paired", payload)])
# The original class="text-line..." must survive verbatim alongside
# the injected data-text-path attribute on the same opening tag.
assert re.search(
r'<div\s+data-text-path="row_1_left_body\.0"\s+class="text-line[^"]*">',
html,
) is not None
# ─── Case 2 : non-list slots are NOT stamped ─────────────────────────
def test_non_list_slots_not_stamped():
"""Scalar slot values (title, *_label) get no data-text-path."""
payload = _paired_slot_payload(left_lines=["x"], right_lines=["y"])
html = _render([_zone("bim_current_problems_paired", payload)])
# Scalar slots present in slot_payload as strings — must not receive
# data-text-path stamps (u8 contract: scalar slots skipped silently
# because they render outside text-line divs).
assert 'data-text-path="title' not in html
assert 'data-text-path="row_1_left_label' not in html
assert 'data-text-path="row_1_right_label' not in html
# ─── Case 3 : empty list slots contribute no stamps ──────────────────
def test_empty_list_slots_no_stamps():
"""Empty list slot yields zero stamps; template emits zero text-line divs."""
payload = _paired_slot_payload(left_lines=[], right_lines=[])
html = _render([_zone("bim_current_problems_paired", payload)])
# No row 1 lines at all (both bodies empty) → no row_1_*_body stamps.
assert "data-text-path" not in html
# ─── Case 4 : deterministic — repeated render produces same HTML ─────
def test_render_with_stamp_is_deterministic():
"""Same slot_payload → byte-identical HTML across two render_slide calls.
Guards against the wiring layer accidentally mutating slot_payload
between renders (the stamper itself only reads slot_payload; it
operates on rendered_partial). Also guards against double-stamping.
"""
payload_1 = _paired_slot_payload()
payload_2 = _paired_slot_payload()
html_1 = _render([_zone("bim_current_problems_paired", payload_1)])
html_2 = _render([_zone("bim_current_problems_paired", payload_2)])
assert html_1 == html_2
# Counts must match — no double-stamp side effect on shared module state.
assert html_1.count("data-text-path=") == html_2.count("data-text-path=")
# 2 left + 1 right = 3 stamps for the default fixture.
assert html_1.count("data-text-path=") == 3
# ─── Case 5 : __empty__ short-circuit emits no stamps ────────────────
def test_empty_template_short_circuit_no_stamps():
"""``template_id=__empty__`` short-circuits before stamping; no stamps."""
html = _render([_zone("__empty__", {})])
assert "data-text-path" not in html
# ─── Case 6 : slot_payload preserved (raw_content invariant) ─────────
def test_render_does_not_mutate_slot_payload():
"""Stamping must not mutate slot_payload list/dict contents.
The stamper operates on rendered_partial HTML; the source
slot_payload should be byte-identical before and after render_slide.
Locks the raw_content preservation invariant at the wiring layer.
"""
payload = _paired_slot_payload(
left_lines=["preserved A", "preserved B"],
right_lines=["preserved C"],
)
# Snapshot key list/dict identities and content.
snapshot_left = list(payload["row_1_left_body"])
snapshot_left_text = [item["text"] for item in snapshot_left]
snapshot_right_text = [item["text"] for item in payload["row_1_right_body"]]
_ = _render([_zone("bim_current_problems_paired", payload)])
assert [item["text"] for item in payload["row_1_left_body"]] == snapshot_left_text
assert [item["text"] for item in payload["row_1_right_body"]] == snapshot_right_text
# Scalar slots untouched too.
assert payload["title"] == "Synthetic Title"