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>
213 lines
8.2 KiB
Python
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"
|