feat(#84): IMP-84 u1~u3 silent automation policy enforcement (FramePanel reject confirm + slide_base provisional badge/outline + IMP-30 visual assertions inverted)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 21s
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 21s
- u1 FramePanel.tsx: extract `applyFrameSelection(candidate, onFrameSelect)` pure helper; collapse `handleFrameSelect` to direct onFrameSelect for every V4 label; drop `window.confirm` reject popup (IMP-47B u11 regression noise per `feedback_auto_pipeline_first`). New vitest pin `imp84_framepanel_reject_silent.test.ts` covers helper invocation across all 4 V4 labels + source-presence pins. - u2 templates/phase_z2/slide_base.html: delete `.zone--provisional` CSS, `.zone__needs-adaptation-badge` CSS, the zone--provisional class fragment in the zone div, and the badge `<span>` render at the provisional zone. Preserve `data-provisional="1"` attribute as silent telemetry. New pytest `tests/phase_z2/test_imp84_provisional_silent_render.py` pins the silent contract independently of the IMP-30 first-render file. - u3 tests/test_phase_z2_imp30_first_render.py: invert the three IMP-30 u5 positive provisional-visual assertions to IMP-84 silent-contract negatives (no class, no badge, no CSS selectors); preserve positive `data-provisional` telemetry assertions. Docstrings updated to IMP-84 silent contract. Out of scope (Round #4 + #92 contract): Home.tsx `toast.error(aiReviewMsg)` call line, designAgentApi.ts `api_error_kinds`/`api_error_kind` schema and operational-only formatter, FramePanel reject badge/tooltip read-only labels (L102/L147/L156), and backend `zone.provisional` flag emission. Stage 4 PASS: u1 vitest 10/10, u2 pytest 5/5, u3 pytest 29/29 (incl. 3 IMP-84 inverted assertions: `test_imp84_provisional_zone_silent_no_class_no_badge`, `test_imp84_provisional_badge_never_rendered_in_mixed_zones`, `test_imp84_slide_base_css_strips_provisional_visual_selectors`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
249
tests/phase_z2/test_imp84_provisional_silent_render.py
Normal file
249
tests/phase_z2/test_imp84_provisional_silent_render.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""IMP-84 u2 — provisional zone silent-render contract.
|
||||
|
||||
Pins that `templates/phase_z2/slide_base.html` no longer surfaces the
|
||||
provisional visual treatment (dashed outline, striped wash, badge span)
|
||||
while keeping `data-provisional="1"` as silent telemetry on the zone div.
|
||||
|
||||
Stage 2 binding contract (IMP-84):
|
||||
- Remove .zone--provisional class emission on the zone div.
|
||||
- Remove .zone__needs-adaptation-badge <span> render.
|
||||
- Remove the .zone--provisional CSS block and the
|
||||
.zone__needs-adaptation-badge CSS block from the <style> section.
|
||||
- Preserve data-provisional="1" attribute emission for provisional zones
|
||||
(downstream telemetry / debug selectors). Out-of-scope: backend
|
||||
`zone.provisional` flag emission itself.
|
||||
|
||||
Helpers below intentionally mirror the IMP-30 first-render test helpers
|
||||
(_render_slide_base / _all_zone_div_openings / _all_badge_spans /
|
||||
_zone_div_for_position) so the silent-render contract is enforced by an
|
||||
independent rendering surface, not by the IMP-30 file (which u3 will
|
||||
invert separately).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
|
||||
# ─── helpers (mirrored from IMP-30 u5 to keep this test self-contained) ───
|
||||
|
||||
def _render_slide_base(
|
||||
zones: list[dict],
|
||||
*,
|
||||
layout_preset: str = "single",
|
||||
layout_css: dict | None = None,
|
||||
) -> str:
|
||||
"""Render templates/phase_z2/slide_base.html via Jinja2 with a minimal
|
||||
zones list. Bypasses render_slide() so the template-only silent-render
|
||||
contract is exercised without the pipeline (no mapper, no contracts,
|
||||
no token CSS loader). slot_payload / partial_html are stubbed so the
|
||||
assertions focus on zone div / CSS surface only."""
|
||||
template_dir = (
|
||||
Path(__file__).resolve().parents[2] / "templates" / "phase_z2"
|
||||
)
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(str(template_dir)),
|
||||
autoescape=select_autoescape(["html"]),
|
||||
)
|
||||
if layout_css is None:
|
||||
layout_css = {
|
||||
"cols": "1fr",
|
||||
"rows": "1fr",
|
||||
"areas": '"single"',
|
||||
}
|
||||
for z in zones:
|
||||
z.setdefault("partial_html", "<div class=\"_stub_partial\">stub</div>")
|
||||
base = env.get_template("slide_base.html")
|
||||
return base.render(
|
||||
slide_title="IMP-84 u2 silent-render test",
|
||||
slide_footer=None,
|
||||
zones=zones,
|
||||
layout_preset=layout_preset,
|
||||
layout_css=layout_css,
|
||||
gap_px=12,
|
||||
token_css="",
|
||||
embedded_mode="standalone",
|
||||
)
|
||||
|
||||
|
||||
def _zone_div_for_position(html: str, position: str) -> str:
|
||||
"""Return the opening `<div class="zone..." data-zone-position="X" ...>`
|
||||
tag for the zone at the given `data-zone-position`. Anchors assertions
|
||||
on zone-div-level attributes / classes only."""
|
||||
pattern = re.compile(
|
||||
r'<div class="zone[^"]*"\s+data-zone-position="'
|
||||
+ re.escape(position)
|
||||
+ r'"[^>]*>',
|
||||
re.DOTALL,
|
||||
)
|
||||
match = pattern.search(html)
|
||||
return match.group(0) if match else ""
|
||||
|
||||
|
||||
def _all_zone_div_openings(html: str) -> list[str]:
|
||||
"""Every zone-div opening tag in the layout body. Scopes class /
|
||||
attribute checks away from the <style> block (which may still contain
|
||||
selector strings if a future change re-introduces them)."""
|
||||
return re.findall(
|
||||
r'<div class="zone[^"]*"[^>]*data-zone-position="[^"]*"[^>]*>',
|
||||
html,
|
||||
)
|
||||
|
||||
|
||||
def _all_badge_spans(html: str) -> list[str]:
|
||||
"""Every `.zone__needs-adaptation-badge` <span> element in the rendered
|
||||
body. Must be empty under the IMP-84 silent-render contract regardless
|
||||
of zones[i].provisional value."""
|
||||
return re.findall(
|
||||
r'<span class="zone__needs-adaptation-badge"[^>]*>[^<]*</span>',
|
||||
html,
|
||||
)
|
||||
|
||||
|
||||
# ─── case 1 : non-provisional zone unchanged (regression boundary) ───
|
||||
|
||||
def test_imp84_non_provisional_zone_unchanged():
|
||||
"""zones[i].provisional=False must render the zone div with no
|
||||
provisional class, no data-provisional attr, no badge — byte-equivalent
|
||||
to pre-IMP-84 baseline for the non-provisional path."""
|
||||
zones = [
|
||||
{
|
||||
"position": "single",
|
||||
"template_id": "MOCK_template_direct_a",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 1},
|
||||
"min_height_px": 100,
|
||||
"provisional": False,
|
||||
}
|
||||
]
|
||||
html = _render_slide_base(zones)
|
||||
zone_open = _zone_div_for_position(html, "single")
|
||||
assert zone_open != ""
|
||||
assert "zone--provisional" not in zone_open
|
||||
assert "data-provisional" not in zone_open
|
||||
assert _all_badge_spans(html) == []
|
||||
|
||||
|
||||
# ─── case 2 : provisional zone silent — class / badge / wash removed ───
|
||||
|
||||
def test_imp84_provisional_zone_emits_data_attr_only_no_visual():
|
||||
"""The core IMP-84 silent-render contract.
|
||||
|
||||
With zones[i].provisional=True, the rendered HTML MUST:
|
||||
- NOT contain the `zone--provisional` class on any zone div
|
||||
- NOT render a `.zone__needs-adaptation-badge` <span>
|
||||
- NOT contain the human-visible "needs adaptation" label text
|
||||
- STILL emit `data-provisional="1"` on the provisional zone div
|
||||
(silent telemetry preserved for downstream selectors)
|
||||
"""
|
||||
zones = [
|
||||
{
|
||||
"position": "single",
|
||||
"template_id": "MOCK_template_restructure_a",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 1},
|
||||
"min_height_px": 100,
|
||||
"provisional": True,
|
||||
}
|
||||
]
|
||||
html = _render_slide_base(zones)
|
||||
zone_open = _zone_div_for_position(html, "single")
|
||||
assert zone_open != ""
|
||||
assert "zone--provisional" not in zone_open
|
||||
assert 'data-provisional="1"' in zone_open
|
||||
assert _all_badge_spans(html) == []
|
||||
assert "needs adaptation" not in html
|
||||
|
||||
|
||||
# ─── case 3 : mixed zones — telemetry isolation preserved ───
|
||||
|
||||
def test_imp84_mixed_zones_data_provisional_only_on_provisional_zone():
|
||||
"""In a mixed-zone slide (one provisional + one normal), the silent
|
||||
telemetry attribute must appear ONLY on the provisional zone div, and
|
||||
no visual artifact may surface on either zone."""
|
||||
zones = [
|
||||
{
|
||||
"position": "top",
|
||||
"template_id": "MOCK_template_direct_a",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 1},
|
||||
"min_height_px": 100,
|
||||
"provisional": False,
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"template_id": "MOCK_template_restructure_a",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 1},
|
||||
"min_height_px": 100,
|
||||
"provisional": True,
|
||||
},
|
||||
]
|
||||
layout_css = {
|
||||
"cols": "1fr",
|
||||
"rows": "1fr 1fr",
|
||||
"areas": '"top" "bottom"',
|
||||
}
|
||||
html = _render_slide_base(
|
||||
zones, layout_preset="vertical-2", layout_css=layout_css
|
||||
)
|
||||
zone_divs = _all_zone_div_openings(html)
|
||||
assert len(zone_divs) == 2
|
||||
|
||||
top_zone_open = _zone_div_for_position(html, "top")
|
||||
bottom_zone_open = _zone_div_for_position(html, "bottom")
|
||||
assert "data-provisional" not in top_zone_open
|
||||
assert 'data-provisional="1"' in bottom_zone_open
|
||||
|
||||
for tag in zone_divs:
|
||||
assert "zone--provisional" not in tag
|
||||
assert _all_badge_spans(html) == []
|
||||
assert "needs adaptation" not in html
|
||||
|
||||
|
||||
# ─── case 4 : <style> block free of provisional visual selectors ───
|
||||
|
||||
def test_imp84_style_block_has_no_provisional_visual_selectors():
|
||||
"""The provisional visual CSS classes are deleted at source. A future
|
||||
refactor that re-introduces `.zone--provisional` or
|
||||
`.zone__needs-adaptation-badge` selectors into slide_base.html breaks
|
||||
this test rather than silently restoring the visual badge."""
|
||||
zones = [
|
||||
{
|
||||
"position": "single",
|
||||
"template_id": "MOCK_template_restructure_a",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 1},
|
||||
"min_height_px": 100,
|
||||
"provisional": True,
|
||||
}
|
||||
]
|
||||
html = _render_slide_base(zones)
|
||||
assert ".zone--provisional" not in html
|
||||
assert ".zone__needs-adaptation-badge" not in html
|
||||
assert "zone__needs-adaptation-badge" not in html
|
||||
|
||||
|
||||
# ─── case 5 : provisional defaults to false (template fallback) ───
|
||||
|
||||
def test_imp84_provisional_none_falls_back_to_silent_non_provisional():
|
||||
"""When zones[i].provisional is explicitly None (falsy but not False),
|
||||
the template's truthy check must NOT emit `data-provisional`. Pins the
|
||||
template fallback so a refactor cannot silently invert the default."""
|
||||
zones = [
|
||||
{
|
||||
"position": "single",
|
||||
"template_id": "MOCK_template_direct_a",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 1},
|
||||
"min_height_px": 100,
|
||||
"provisional": None,
|
||||
}
|
||||
]
|
||||
html = _render_slide_base(zones)
|
||||
zone_open = _zone_div_for_position(html, "single")
|
||||
assert zone_open != ""
|
||||
assert "data-provisional" not in zone_open
|
||||
assert _all_badge_spans(html) == []
|
||||
@@ -676,12 +676,20 @@ def test_u5_zone_without_provisional_key_treated_as_non_provisional():
|
||||
# ─── u5 case 2 : provisional zone renders class + badge + data attr ───
|
||||
|
||||
|
||||
def test_u5_provisional_zone_renders_class_and_badge():
|
||||
"""Opt-in path. zones[i].provisional=True must:
|
||||
1. Append `zone--provisional` class to the zone div.
|
||||
2. Set `data-provisional="1"` data attribute (for downstream selectors).
|
||||
3. Render a `<span class="zone__needs-adaptation-badge">` element with
|
||||
the literal text "needs adaptation" (aria-label included for a11y).
|
||||
def test_imp84_provisional_zone_silent_no_class_no_badge():
|
||||
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 contract.
|
||||
Under the silent contract, zones[i].provisional=True must:
|
||||
1. NOT append `zone--provisional` class to the zone div (no user-visible
|
||||
outline / striped wash).
|
||||
2. Still set `data-provisional="1"` data attribute as silent telemetry
|
||||
for downstream selectors / inspection.
|
||||
3. NOT render any `<span class="zone__needs-adaptation-badge">` element
|
||||
and NOT surface the literal text "needs adaptation" or its
|
||||
aria-label (no user-facing badge).
|
||||
|
||||
Scope: assertions target the zone div body. The CSS <style> block must
|
||||
likewise not carry the removed visual selectors — that surface is pinned
|
||||
in `test_imp84_slide_base_css_strips_provisional_visual_selectors` below.
|
||||
"""
|
||||
zones = [
|
||||
{
|
||||
@@ -694,21 +702,26 @@ def test_u5_provisional_zone_renders_class_and_badge():
|
||||
}
|
||||
]
|
||||
html = _render_slide_base(zones)
|
||||
# zone--provisional class must appear on the zone div for position=single.
|
||||
assert "zone--provisional" in html
|
||||
# data-provisional="1" attribute must be present.
|
||||
assert 'data-provisional="1"' in html
|
||||
# Badge element with the required label text.
|
||||
assert 'class="zone__needs-adaptation-badge"' in html
|
||||
assert "needs adaptation" in html
|
||||
assert 'aria-label="needs user or AI adaptation"' in html
|
||||
zone_divs = _all_zone_div_openings(html)
|
||||
assert len(zone_divs) == 1
|
||||
# No zone--provisional class on the zone div (visual removed).
|
||||
assert "zone--provisional" not in zone_divs[0]
|
||||
# data-provisional="1" attribute still present as silent telemetry.
|
||||
assert 'data-provisional="1"' in zone_divs[0]
|
||||
# No badge <span> element and no badge label text anywhere in the body.
|
||||
assert _all_badge_spans(html) == []
|
||||
assert "needs adaptation" not in html
|
||||
assert 'aria-label="needs user or AI adaptation"' not in html
|
||||
|
||||
|
||||
def test_u5_provisional_badge_appears_inside_provisional_zone_only():
|
||||
"""Mixed-zone slide: one provisional zone + one normal zone. The badge
|
||||
+ class must appear ONLY in the provisional zone, not bleed into the
|
||||
normal one (CSS-level isolation should already prevent this, but the
|
||||
template must not emit the badge for both)."""
|
||||
def test_imp84_provisional_badge_never_rendered_in_mixed_zones():
|
||||
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 mixed-zone
|
||||
contract. Mixed-zone slide: one provisional zone + one normal zone. The
|
||||
silent contract requires that NO badge span and NO `zone--provisional`
|
||||
class be emitted on either zone div. The provisional zone is identifiable
|
||||
only through the silent `data-provisional="1"` telemetry attribute, which
|
||||
must be scoped to the provisional zone alone (no bleed onto the normal
|
||||
zone)."""
|
||||
zones = [
|
||||
{
|
||||
"position": "top",
|
||||
@@ -735,21 +748,21 @@ def test_u5_provisional_badge_appears_inside_provisional_zone_only():
|
||||
html = _render_slide_base(
|
||||
zones, layout_preset="vertical-2", layout_css=layout_css
|
||||
)
|
||||
# Exactly one badge span element should be present in the rendered body
|
||||
# (CSS selector in <style> excluded by the helper).
|
||||
assert len(_all_badge_spans(html)) == 1
|
||||
# zone--provisional must appear on exactly one zone div (CSS selector
|
||||
# in <style> excluded by the helper).
|
||||
# No badge <span> element should be rendered anywhere in the body
|
||||
# (silent-automation policy).
|
||||
assert _all_badge_spans(html) == []
|
||||
# No zone div should carry the zone--provisional class (visual removed).
|
||||
zone_divs = _all_zone_div_openings(html)
|
||||
assert len(zone_divs) == 2
|
||||
provisional_zone_divs = [d for d in zone_divs if "zone--provisional" in d]
|
||||
assert len(provisional_zone_divs) == 1
|
||||
# The provisional class must be associated with the bottom zone.
|
||||
assert all("zone--provisional" not in d for d in zone_divs)
|
||||
# data-provisional="1" telemetry must be present on the bottom (provisional)
|
||||
# zone only — never on the top (non-provisional) zone.
|
||||
bottom_zone_open = _zone_div_for_position(html, "bottom")
|
||||
assert "zone--provisional" in bottom_zone_open
|
||||
assert "zone__needs-adaptation-badge" in bottom_zone_open
|
||||
# The top zone must NOT carry the provisional class.
|
||||
assert 'data-provisional="1"' in bottom_zone_open
|
||||
assert "zone--provisional" not in bottom_zone_open
|
||||
assert "zone__needs-adaptation-badge" not in bottom_zone_open
|
||||
top_zone_open = _zone_div_for_position(html, "top")
|
||||
assert 'data-provisional="1"' not in top_zone_open
|
||||
assert "zone--provisional" not in top_zone_open
|
||||
assert "zone__needs-adaptation-badge" not in top_zone_open
|
||||
|
||||
@@ -779,15 +792,19 @@ def test_u5_zones_data_provisional_field_defaults_false_in_template():
|
||||
assert _all_badge_spans(html) == []
|
||||
|
||||
|
||||
def test_u5_slide_base_css_carries_provisional_marker_styles():
|
||||
"""The provisional visual contract (dashed outline + striped wash + badge)
|
||||
is defined in slide_base.html <style>. Pin that the relevant CSS class
|
||||
selectors exist in the rendered HTML so a refactor that removes them
|
||||
breaks this test rather than silently rendering an unstyled badge.
|
||||
def test_imp84_slide_base_css_strips_provisional_visual_selectors():
|
||||
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 CSS-presence
|
||||
contract. The provisional visual treatment (dashed outline + striped wash
|
||||
+ badge) was deleted from `slide_base.html <style>` by IMP-84 u2. Pin
|
||||
that the CSS class selectors `.zone--provisional` and
|
||||
`.zone__needs-adaptation-badge` no longer appear in the rendered HTML —
|
||||
a refactor that re-introduces them must break this test rather than
|
||||
silently re-surfacing the removed visual signal.
|
||||
|
||||
This is a class-selector existence check; it does not validate the
|
||||
specific color / dash pattern, which is a design decision intentionally
|
||||
left malleable (e.g., palette swap for a different theme)."""
|
||||
Scope: the assertion targets the entire rendered HTML (style block plus
|
||||
body). Since the body badge span is also gone (covered separately above),
|
||||
any occurrence of these strings in the rendered output would only come
|
||||
from a regressed style block."""
|
||||
zones = [
|
||||
{
|
||||
"position": "single",
|
||||
@@ -799,9 +816,9 @@ def test_u5_slide_base_css_carries_provisional_marker_styles():
|
||||
}
|
||||
]
|
||||
html = _render_slide_base(zones)
|
||||
# Style block must define .zone--provisional and the badge selector.
|
||||
assert ".zone--provisional" in html
|
||||
assert ".zone__needs-adaptation-badge" in html
|
||||
# Style block must NOT define .zone--provisional or the badge selector.
|
||||
assert ".zone--provisional" not in html
|
||||
assert ".zone__needs-adaptation-badge" not in html
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user