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 in7c93031ahead 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 in7c93031) -- 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 from7c93031). 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 into7c93031as 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>
306 lines
12 KiB
Python
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"
|
|
"\n"
|
|
"\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 ```` 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
|