Files
C.E.L_Slide_test2/tests/phase_z2/test_popup_mdx_preservation.py
kyeongmin f3ef4d917c feat(#64): IMP-35 details_popup_escalation u1~u10 + Stage 3 R7 anchor re-pin
Land the production + test surface for the Step 17 cascade POPUP terminal
(DETERMINISTIC -> POPUP -> AI_REPAIR -> USER_OVERRIDE) per Stage 2 plan R2.
u11 (baseline-red invariance gate) was already landed in 7c93031 ahead of
this commit; this commit completes u1~u10 plus the Stage 3 R7 follow-up
anchor re-pin for test_imp17_comment_anchor.py.

Implementation units (Stage 2 R2 contract):
  u1  frame_reselect_insufficient failure_type + post-frame remeasure (q4)
        - src/phase_z2_failure_router.py, src/phase_z2_pipeline.py
  u2  NEXT_ACTION_BY_FAILURE row + impl_status flip
        - src/phase_z2_failure_router.py
  u3  Router details_popup_escalation MISSING->IMPLEMENTED + executor stub
        - src/phase_z2_router.py
  u4  step17.py AI split-decision contract (POPUP cascade_stage +
      route_for_label + skip_reason); API gated
        - src/phase_z2_ai_fallback/step17.py
  u5  Step 17 POPUP gate executor; popup_escalation_plan + has_popup marker
        - src/phase_z2_pipeline.py, src/phase_z2_ai_fallback/step17.py
  u6  Composition popup binding -- yaml strategy -> zone payload
        - src/phase_z2_composition.py
  u7  Pipeline composer -> render_slide wiring
      (popup_html / preview_text / has_popup)
        - src/phase_z2_pipeline.py
  u8  slide_base.html <details>/<summary> popup wrapper
        - templates/phase_z2/slide_base.html
  u9  display_strategies.yaml inline_preview + popup metadata
        - templates/phase_z2/regions/display_strategies.yaml
  u10 MDX preservation invariant: popup=full source / body=summary or subset
        (asserted by tests/phase_z2/test_popup_mdx_preservation.py)
  u11 (already in 7c93031) -- baseline-red invariance gate

Stage 3 R7 follow-up (anchor re-pin, test-only):
  - tests/orchestrator_unit/test_imp17_comment_anchor.py
    Pre-anchor additions in src/phase_z2_pipeline.py (u1 / u5 / u7) shifted
    the restructure/reject route-hint comments 578/579 -> 586/587. Re-pinned
    the two guard tests (and docstring re-pin lineage 564 -> 570 -> 578 ->
    586). Production code untouched.

Verification (Stage 4 R1):
  pytest -q tests/orchestrator_unit/test_imp17_comment_anchor.py
    -> 2 passed / 0.02s
  pytest -q <10 IMP-35 unit files in tests/phase_z2 + tests/phase_z2_ai_fallback>
    -> 136 passed / 15.94s
  Baseline-red invariance gate
    (tests/test_imp47b_step12_ai_wiring.py +
     tests/test_phase_z2_ai_fallback_config.py)
    -> 4 failed / 6 passed; FAILED set === IMP35_BASELINE_RED_NODE_IDS
    (frozen registry from 7c93031). Contract holds.
  Codex Stage 4 R1 = YES (independent verify).

Guardrails honored:
  - MDX content preservation: popup carries full source, body holds
    summary or subset only (CLAUDE.md 자세히보기 원칙;
    feedback_phase_z_spacing_direction -- capacity expanded, no margin shrink).
  - AI isolation contract: Step 17 POPUP gate is deterministic; AI hook
    surface is split-decision contract only, API call gated.
  - No hardcoding: escalation thresholds derived from existing overflow
    detector outputs; preview_chars deterministic from container px.
  - 1 commit = 1 decision unit: u1~u10 land together as the planned
    production surface; u11 was deliberately split into 7c93031 as Stage 3
    R7 carve-out, and the R7 anchor re-pin rides with this commit because
    it is the direct shift consequence of the u1/u5/u7 pre-anchor additions.
  - Scope-locked: .claude/settings.json explicitly excluded
    (Stage 4 exit report contract).

Out of scope (per Stage 1 + Stage 2):
  - AI_REPAIR API activation (post IMP-35 axis).
  - IMP-34 zone resize, IMP-36 responsive fit (chain partners,
    separate issues).
  - Print-time auto-expand JavaScript for <details>.
  - Popup escalation in stages other than Step 17.
  - Baseline-red body repair (4 frozen failures) -- separate follow-up
    issue; u11 only guards the count.
  - frame_reselect algorithm changes (entry point only).
  - templates/phase_z2/slide_base.html path rename.

source_comment_ids:
  Stage 1: claude_stage1_problem_review_imp35, codex_stage1_verification_imp35_yes
  Stage 2: Claude #4 R2 plan, Codex #5 R2 YES
  Stage 3: Claude #86 (R7 anchor re-pin), Codex #87 YES
  Stage 4: Claude #88 R1, Codex #89 R1 YES

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 07:36:57 +09:00

306 lines
12 KiB
Python

"""IMP-35 (#64) u10 — MDX preservation guard tests.
Stage 2 binding contract (unit u10):
After Step 17 POPUP gate (u5) stamps the unit, composition (u6) binds
the strategy, pipeline (u7) wires the render context, and slide_base
(u8) renders the ``<details>/<summary>`` wrapper, the end-to-end
invariant the user lock requires is:
MDX 원문 무손실 보존 (오답노트 #5 / IMPROVEMENT-REDESIGN.md §3.6
line 110, CLAUDE.md 자세히보기 원칙):
- popup body == FULL ``raw_content`` (byte-for-byte verbatim)
- body preview == SUBSET of ``raw_content`` (deterministic
leading-substring CUT — never a rewrite, never a re-summary)
- the original is ALWAYS reachable via the popup; the preview
loses no information because the popup holds the full source
- no structural element is dropped: text_block / table / image
/ ``<details>`` counts in popup body match the original
u6 and u7 each lock pieces of this invariant on their own surface.
u10 locks the END-TO-END no-content-drop guarantee on the rendered
payload — the surface a downstream verifier (Selenium / vision gate)
would inspect — so a future refactor on either u6 or u7 cannot
silently degrade MDX preservation without this test failing first.
Key invariants this file locks:
1. popup_html (full source) preserves every structural element from
raw_content byte-for-byte: bullet lines, paragraph blocks, markdown
table rows, image markdown, and nested ``<details>`` blocks.
2. preview_text is a deterministic leading-substring CUT of
raw_content — ``raw_content.startswith(preview_text)`` holds when
truncation happened.
3. Combined invariant: popup_html holds the FULL original even when
preview_text is shorter, so no content is dropped — the full
source is always reachable via the popup.
4. has_popup=False path: popup_html / preview_text are both None.
There is no popup escalation, so by definition no escalation can
drop content; the frame's partial_html (rendered separately by
slide_base.html and not part of u7 popup wiring) holds the inline
body.
5. AI isolation contract — pure deterministic preservation check;
no anthropic import, no route_ai_fallback path.
Cross-references:
- u6 composition popup binding (popup_body_source = full raw_content):
tests/phase_z2/test_composition_popup_strategy.py
- u7 pipeline wiring (popup_html = popup_body_source verbatim;
preview_text is a deterministic line-budget cut):
tests/phase_z2/test_phase_z2_pipeline_popup_wiring.py
- u8 slide_base.html render surface (autoescaped popup body):
tests/phase_z2/test_slide_base_popup_render.py
- u9 display_strategies.yaml catalog (preserves_original=True for the
popup-bearing strategy):
tests/phase_z2/test_display_strategies_popup.py
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Optional
import pytest
from src.phase_z2_composition import compose_zone_popup_payload
# ─── Synthetic stubs ─────────────────────────────────────────────────
@dataclass
class _StubUnit:
"""Minimal duck-typed CompositionUnit for u10 preservation tests."""
raw_content: str = "MOCK_ORIGINAL_CONTENT"
has_popup: bool = False
popup_escalation_plan: Optional[dict] = None
def _stub_popup_plan() -> dict:
"""Mirror the plan_details_popup_escalation feasible-escalation shape
(u3). u10 only echoes the plan into the unit so the binder reaches
the popup branch; no field is consumed here."""
return {
"action": "details_popup_escalation",
"stub": True,
"feasible": True,
"category": "structural_major_overflow",
"needs_split_decision": True,
"rationale": "MOCK_RATIONALE",
"mapping_source": "IMP-35 u3 plan_details_popup_escalation stub",
}
# ─── Deterministic structural-element counters ──────────────────────
def _count_markdown_bullet_lines(text: str) -> int:
"""Count leading-``-`` markdown bullet lines (- / * / + at line start)."""
return sum(
1 for line in text.splitlines() if re.match(r"^\s*[-*+]\s+", line)
)
def _count_markdown_table_rows(text: str) -> int:
"""Count markdown table rows (lines with ``|`` somewhere)."""
return sum(1 for line in text.splitlines() if "|" in line)
def _count_markdown_images(text: str) -> int:
"""Count markdown image references ``![...](...)``."""
return len(re.findall(r"!\[[^\]]*\]\([^)]+\)", text))
def _count_details_blocks(text: str) -> int:
"""Count nested ``<details>`` blocks in raw_content (rare — used to
lock the invariant even when MDX already carries native popups)."""
return len(re.findall(r"<details\b", text, flags=re.IGNORECASE))
# ─── Sample MDX content (structural diversity for the count guard) ──
_FULL_MDX_SAMPLE = (
"## MOCK_SECTION_TITLE\n"
"\n"
"Paragraph one explaining the MOCK topic. Lorem ipsum dolor sit amet.\n"
"\n"
"- bullet one with **bold** marker\n"
"- bullet two with *italic* marker\n"
"- bullet three trailing\n"
"\n"
"| col_a | col_b |\n"
"| --- | --- |\n"
"| MOCK_A | MOCK_B |\n"
"| MOCK_C | MOCK_D |\n"
"\n"
"![MOCK_ALT](mock/path/to/image_a.png)\n"
"![MOCK_ALT_TWO](mock/path/to/image_b.png)\n"
"\n"
"<details><summary>MOCK_NESTED_TRIGGER</summary>"
"<p>MOCK_NESTED_BODY</p></details>\n"
"\n"
"Paragraph two — closing remarks for the MOCK topic.\n"
)
# ─── Popup body = full raw_content (byte-for-byte) ───────────────────
def test_popup_body_byte_for_byte_equal_to_raw_content():
"""u10 — the end-to-end invariant: popup_html on the rendered payload
is byte-for-byte equal to the unit's raw_content. u6 + u7 already
lock this on their own surface; u10 re-asserts on the payload a
downstream verifier (Selenium / vision gate) would inspect."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
payload = compose_zone_popup_payload(unit, container_height_px=200)
assert payload["popup_html"] == _FULL_MDX_SAMPLE
assert len(payload["popup_html"]) == len(_FULL_MDX_SAMPLE)
def test_popup_body_preserves_bullet_line_count():
"""u10 — text_block count equality. Every bullet line present in
raw_content MUST also be present in popup_html. A future refactor
that accidentally trims popup body to a summary would drop bullets
and fail this guard."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
payload = compose_zone_popup_payload(unit, container_height_px=200)
assert _count_markdown_bullet_lines(payload["popup_html"]) == (
_count_markdown_bullet_lines(_FULL_MDX_SAMPLE)
)
def test_popup_body_preserves_markdown_table_row_count():
"""u10 — table count equality. Markdown table rows (header / divider
/ data) MUST all survive the popup wiring."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
payload = compose_zone_popup_payload(unit, container_height_px=200)
assert _count_markdown_table_rows(payload["popup_html"]) == (
_count_markdown_table_rows(_FULL_MDX_SAMPLE)
)
def test_popup_body_preserves_image_reference_count():
"""u10 — image count equality. Markdown ``![alt](src)`` references
MUST all survive (CLAUDE.md: 이미지는 원본 그대로 사용, 크기만 조절 —
popup escalation must not silently drop image refs)."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
payload = compose_zone_popup_payload(unit, container_height_px=200)
assert _count_markdown_images(payload["popup_html"]) == (
_count_markdown_images(_FULL_MDX_SAMPLE)
)
def test_popup_body_preserves_nested_details_block_count():
"""u10 — nested ``<details>`` blocks. Even when MDX already carries
a native popup, the u10 popup escalation MUST NOT collapse or drop
nested ``<details>`` markers."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
payload = compose_zone_popup_payload(unit, container_height_px=200)
assert _count_details_blocks(payload["popup_html"]) == (
_count_details_blocks(_FULL_MDX_SAMPLE)
)
# ─── Preview = deterministic leading-substring CUT ──────────────────
def test_preview_text_is_a_leading_substring_of_raw_content_when_truncated():
"""u10 — preview is a CUT, never a rewrite. When truncation happens,
raw_content MUST start with preview_text verbatim (line-boundary
cut semantics; popup body retains the FULL original)."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
# 2-line budget — far smaller than the multi-line sample.
payload = compose_zone_popup_payload(unit, container_height_px=36)
preview = payload["preview_text"]
assert preview, "preview_text must be non-empty when truncation fires"
assert _FULL_MDX_SAMPLE.startswith(preview), (
"preview_text must be a leading-substring of raw_content "
"(MDX 원문 무손실 보존 — preview is a CUT, never a rewrite)."
)
# The popup body still holds the FULL original — no information loss.
assert payload["popup_html"] == _FULL_MDX_SAMPLE
def test_no_content_drop_when_preview_is_shorter_than_popup_body():
"""u10 — combined no-drop invariant. preview_text may be a strict
prefix of popup_html (shorter), but the popup body always holds the
full original. The user can always reach every line of the source
via the popup, even when the inline preview shows only the head."""
unit = _StubUnit(
raw_content=_FULL_MDX_SAMPLE,
has_popup=True,
popup_escalation_plan=_stub_popup_plan(),
)
payload = compose_zone_popup_payload(unit, container_height_px=36)
preview = payload["preview_text"]
popup_body = payload["popup_html"]
# preview is strictly shorter when truncation fires.
assert len(preview) < len(popup_body)
# popup_body is the FULL original — every line of raw_content is
# present in popup_body regardless of the inline preview budget.
for line in _FULL_MDX_SAMPLE.splitlines():
assert line in popup_body, (
f"MDX preservation guard violated — line {line!r} not present "
f"in popup body."
)
# ─── has_popup=False path: no popup, no escalation, no drop ─────────
def test_no_popup_path_yields_no_popup_html_no_preview_text():
"""u10 — when the Step 17 POPUP gate did not fire, no popup
escalation happens. popup_html and preview_text are both None.
By construction this branch cannot drop content (no escalation),
and the frame's partial_html (rendered separately by slide_base
and not part of u7 popup wiring) holds the inline body."""
unit = _StubUnit(raw_content=_FULL_MDX_SAMPLE, has_popup=False)
payload = compose_zone_popup_payload(unit, container_height_px=200)
assert payload["has_popup"] is False
assert payload["popup_html"] is None
assert payload["preview_text"] is None
# ─── AI isolation contract (structural import lock) ─────────────────
def test_popup_mdx_preservation_module_has_no_ai_imports():
"""u10 — preservation guard MUST stay AI-free. Structural guard:
composition module (where compose_zone_popup_payload lives) is
allowed to consult the catalog and unit state, never the Anthropic
SDK / route_ai_fallback path. Mirrors u6 / u7 import-isolation
pattern (feedback_ai_isolation_contract)."""
import src.phase_z2_composition as composition_module
source = composition_module.__file__
assert source is not None
with open(source, encoding="utf-8") as f:
text = f.read()
assert "import anthropic" not in text
assert "from anthropic" not in text
assert "route_ai_fallback" not in text