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:
@@ -20,6 +20,19 @@ interface FramePanelProps {
|
|||||||
onNoDesignToggle: () => void;
|
onNoDesignToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMP-#84 u1 — silent-automation contract: frame selection delegates directly
|
||||||
|
// to onFrameSelect for every V4 label (use_as_is / light_edit / restructure /
|
||||||
|
// reject). Prior IMP-47B u11 surfaced a window.confirm popup on reject; that
|
||||||
|
// popup is informational UI noise per `feedback_auto_pipeline_first` and is
|
||||||
|
// removed. Frame identity is preserved on reject (AI 재구성 = content-only,
|
||||||
|
// per AI 격리 contract); the popup never gated that contract.
|
||||||
|
export function applyFrameSelection(
|
||||||
|
candidate: FrameCandidate,
|
||||||
|
onFrameSelect: (frameId: string) => void,
|
||||||
|
): void {
|
||||||
|
onFrameSelect(candidate.id);
|
||||||
|
}
|
||||||
|
|
||||||
export default function FramePanel({
|
export default function FramePanel({
|
||||||
slidePlan,
|
slidePlan,
|
||||||
selectedZone,
|
selectedZone,
|
||||||
@@ -49,17 +62,9 @@ export default function FramePanel({
|
|||||||
|
|
||||||
const handleFrameSelect = React.useCallback(
|
const handleFrameSelect = React.useCallback(
|
||||||
(candidate: FrameCandidate) => {
|
(candidate: FrameCandidate) => {
|
||||||
const isReject = candidate.label === "reject";
|
applyFrameSelection(candidate, onFrameSelect);
|
||||||
const alreadyApplied = currentFrameId === candidate.id;
|
|
||||||
if (isReject && !alreadyApplied) {
|
|
||||||
const ok = window.confirm(
|
|
||||||
`"${candidate.name}" 은 V4 reject 라벨입니다.\n선택 시 frame 은 유지되고 AI 가 콘텐츠를 frame 구조에 맞게 재구성합니다.\n계속하시겠습니까?`,
|
|
||||||
);
|
|
||||||
if (!ok) return;
|
|
||||||
}
|
|
||||||
onFrameSelect(candidate.id);
|
|
||||||
},
|
},
|
||||||
[currentFrameId, onFrameSelect],
|
[onFrameSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedZone) {
|
if (!selectedZone) {
|
||||||
|
|||||||
122
Front/client/tests/imp84_framepanel_reject_silent.test.ts
Normal file
122
Front/client/tests/imp84_framepanel_reject_silent.test.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// IMP-#84 u1 — FramePanel reject silent-automation contract.
|
||||||
|
//
|
||||||
|
// Stage 2 unit u1 scope:
|
||||||
|
// 1) `applyFrameSelection(candidate, onFrameSelect)` invokes onFrameSelect
|
||||||
|
// with candidate.id verbatim for EVERY V4 label
|
||||||
|
// (use_as_is / light_edit / restructure / reject) — no window.confirm
|
||||||
|
// gate, no label-conditional branch, no frame swap.
|
||||||
|
// 2) Source-presence checks pin the FramePanel.tsx wiring so the runtime
|
||||||
|
// button → handler → helper chain stays intact even though we cannot
|
||||||
|
// mount React (no jsdom / RTL / happy-dom in Front devDependencies —
|
||||||
|
// verified against the IMP-56 u20 `imp90_bottom_actions.test.ts` and
|
||||||
|
// IMP-92 u5 `imp47b_human_review_toast.test.tsx` precedent that
|
||||||
|
// explicitly skip DOM mounting).
|
||||||
|
// 3) No `window.confirm` substring remains in FramePanel.tsx after u1.
|
||||||
|
//
|
||||||
|
// Out of scope (Stage 2 exit-report contract):
|
||||||
|
// - Home.tsx:523-524 `toast.error(aiReviewMsg)` (#92 operational-only).
|
||||||
|
// - FramePanel reject badge/tooltip read-only labels at L102/L147/L156
|
||||||
|
// (no popup trigger; preserved as silent operator hint).
|
||||||
|
// - Backend `zone.provisional` emission (handled by u2 template-only).
|
||||||
|
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { applyFrameSelection } from "../src/components/FramePanel";
|
||||||
|
import type { FrameCandidate } from "../src/types/designAgent";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const FRAME_PANEL_SOURCE = readFileSync(
|
||||||
|
resolve(__dirname, "../src/components/FramePanel.tsx"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
function makeCandidate(
|
||||||
|
label: FrameCandidate["label"],
|
||||||
|
id: string,
|
||||||
|
): FrameCandidate {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: `Frame ${id}`,
|
||||||
|
score: 0.5,
|
||||||
|
confidence: "medium",
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("applyFrameSelection (IMP-#84 u1 — silent-automation contract)", () => {
|
||||||
|
it("forwards candidate.id to onFrameSelect for use_as_is label", () => {
|
||||||
|
const onFrameSelect = vi.fn();
|
||||||
|
applyFrameSelection(makeCandidate("use_as_is", "frame_a"), onFrameSelect);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledWith("frame_a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards candidate.id to onFrameSelect for light_edit label", () => {
|
||||||
|
const onFrameSelect = vi.fn();
|
||||||
|
applyFrameSelection(makeCandidate("light_edit", "frame_b"), onFrameSelect);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledWith("frame_b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards candidate.id to onFrameSelect for restructure label", () => {
|
||||||
|
const onFrameSelect = vi.fn();
|
||||||
|
applyFrameSelection(makeCandidate("restructure", "frame_c"), onFrameSelect);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledWith("frame_c");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards candidate.id to onFrameSelect for reject label — no popup, no frame swap", () => {
|
||||||
|
// Reject is the silent-automation pivot case: prior IMP-47B u11 gated
|
||||||
|
// this path with window.confirm; post-IMP-#84 the helper invokes
|
||||||
|
// onFrameSelect with the reject frame.id directly. Backend / AI 격리
|
||||||
|
// contract handles AI 재구성 (content-only, frame preserved).
|
||||||
|
const onFrameSelect = vi.fn();
|
||||||
|
applyFrameSelection(makeCandidate("reject", "frame_d"), onFrameSelect);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledWith("frame_d");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call onFrameSelect more than once per invocation", () => {
|
||||||
|
const onFrameSelect = vi.fn();
|
||||||
|
applyFrameSelection(makeCandidate("reject", "frame_e"), onFrameSelect);
|
||||||
|
applyFrameSelection(makeCandidate("use_as_is", "frame_f"), onFrameSelect);
|
||||||
|
expect(onFrameSelect).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onFrameSelect).toHaveBeenNthCalledWith(1, "frame_e");
|
||||||
|
expect(onFrameSelect).toHaveBeenNthCalledWith(2, "frame_f");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FramePanel.tsx source — silent-automation wiring pins (IMP-#84 u1)", () => {
|
||||||
|
it("has no window.confirm(...) call (popup removed; narrative mentions in comments are allowed)", () => {
|
||||||
|
// Match the call form `window.confirm(` rather than the bare substring
|
||||||
|
// so that explanatory comments documenting the removed popup are not
|
||||||
|
// flagged. A re-introduced call would carry an opening paren.
|
||||||
|
expect(FRAME_PANEL_SOURCE).not.toMatch(/\bwindow\.confirm\s*\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not embed the legacy reject-confirm Korean prompt body", () => {
|
||||||
|
// Prior IMP-47B u11 string fragment; absence guards against re-introduction.
|
||||||
|
expect(FRAME_PANEL_SOURCE).not.toContain("V4 reject 라벨입니다");
|
||||||
|
expect(FRAME_PANEL_SOURCE).not.toContain("계속하시겠습니까?");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wires the button onClick to handleFrameSelect(candidate)", () => {
|
||||||
|
expect(FRAME_PANEL_SOURCE).toContain(
|
||||||
|
"onClick={() => handleFrameSelect(candidate)}",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates handleFrameSelect body to applyFrameSelection", () => {
|
||||||
|
expect(FRAME_PANEL_SOURCE).toContain(
|
||||||
|
"applyFrameSelection(candidate, onFrameSelect)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exports applyFrameSelection as a named export for caller-independent reuse", () => {
|
||||||
|
expect(FRAME_PANEL_SOURCE).toMatch(
|
||||||
|
/export function applyFrameSelection\(/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -114,42 +114,10 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── IMP-30 u5 : provisional zone marker (first-render invariant) ──
|
/* IMP-84: provisional zone visual treatment removed (silent-automation
|
||||||
When V4 rank-1 candidate falls outside MVP1_ALLOWED_STATUSES (chain_exhausted)
|
policy). `data-provisional="1"` attribute is still emitted on the
|
||||||
the pipeline still renders the rank-1 frame so the first-render invariant
|
zone div as silent telemetry for downstream selectors / inspection;
|
||||||
holds, but the zone is tagged `provisional` so the user/AI can adapt later
|
no user-visible outline, wash, or badge. */
|
||||||
(IMP-31). Visual contract:
|
|
||||||
- dashed amber border + striped wash → "needs adaptation" at a glance
|
|
||||||
- inline badge top-right → text label for non-color-perceiving readers
|
|
||||||
MDX content is preserved as-is; no shrink, no rewrite. */
|
|
||||||
.zone--provisional {
|
|
||||||
outline: 2px dashed #b8860b;
|
|
||||||
outline-offset: -2px;
|
|
||||||
background-image: repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgba(184, 134, 11, 0.04) 0,
|
|
||||||
rgba(184, 134, 11, 0.04) 8px,
|
|
||||||
transparent 8px,
|
|
||||||
transparent 16px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.zone--provisional .zone__needs-adaptation-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
right: 4px;
|
|
||||||
z-index: 10;
|
|
||||||
padding: 2px 6px;
|
|
||||||
background: #b8860b;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
border-radius: 2px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
pointer-events: none;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Frame-family text layout contract (shared, reusable) ──
|
/* ── Frame-family text layout contract (shared, reusable) ──
|
||||||
feedback-1 (mvp1.5b_test7): visible improvement 강화.
|
feedback-1 (mvp1.5b_test7): visible improvement 강화.
|
||||||
@@ -398,8 +366,7 @@
|
|||||||
<div class="slide-body">
|
<div class="slide-body">
|
||||||
<div class="layout-{{ layout_preset }}">
|
<div class="layout-{{ layout_preset }}">
|
||||||
{% for zone in zones %}
|
{% for zone in zones %}
|
||||||
<div class="zone{% if zone.provisional %} zone--provisional{% endif %}" data-zone-position="{{ zone.position }}" data-template-id="{{ zone.template_id }}"{% if zone.provisional %} data-provisional="1"{% endif %}{% if zone.has_popup %} data-has-popup="1"{% endif %} style="grid-area: {{ zone.position }};">
|
<div class="zone" data-zone-position="{{ zone.position }}" data-template-id="{{ zone.template_id }}"{% if zone.provisional %} data-provisional="1"{% endif %}{% if zone.has_popup %} data-has-popup="1"{% endif %} style="grid-area: {{ zone.position }};">
|
||||||
{% if zone.provisional %}<span class="zone__needs-adaptation-badge" aria-label="needs user or AI adaptation">needs adaptation</span>{% endif %}
|
|
||||||
{{ zone.partial_html | safe }}
|
{{ zone.partial_html | safe }}
|
||||||
{% if zone.has_popup %}
|
{% if zone.has_popup %}
|
||||||
{% set _popup_trigger = (zone.popup_binding.detail_trigger if zone.popup_binding else None) or {} %}
|
{% set _popup_trigger = (zone.popup_binding.detail_trigger if zone.popup_binding else None) or {} %}
|
||||||
|
|||||||
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 ───
|
# ─── u5 case 2 : provisional zone renders class + badge + data attr ───
|
||||||
|
|
||||||
|
|
||||||
def test_u5_provisional_zone_renders_class_and_badge():
|
def test_imp84_provisional_zone_silent_no_class_no_badge():
|
||||||
"""Opt-in path. zones[i].provisional=True must:
|
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 contract.
|
||||||
1. Append `zone--provisional` class to the zone div.
|
Under the silent contract, zones[i].provisional=True must:
|
||||||
2. Set `data-provisional="1"` data attribute (for downstream selectors).
|
1. NOT append `zone--provisional` class to the zone div (no user-visible
|
||||||
3. Render a `<span class="zone__needs-adaptation-badge">` element with
|
outline / striped wash).
|
||||||
the literal text "needs adaptation" (aria-label included for a11y).
|
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 = [
|
zones = [
|
||||||
{
|
{
|
||||||
@@ -694,21 +702,26 @@ def test_u5_provisional_zone_renders_class_and_badge():
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
html = _render_slide_base(zones)
|
html = _render_slide_base(zones)
|
||||||
# zone--provisional class must appear on the zone div for position=single.
|
zone_divs = _all_zone_div_openings(html)
|
||||||
assert "zone--provisional" in html
|
assert len(zone_divs) == 1
|
||||||
# data-provisional="1" attribute must be present.
|
# No zone--provisional class on the zone div (visual removed).
|
||||||
assert 'data-provisional="1"' in html
|
assert "zone--provisional" not in zone_divs[0]
|
||||||
# Badge element with the required label text.
|
# data-provisional="1" attribute still present as silent telemetry.
|
||||||
assert 'class="zone__needs-adaptation-badge"' in html
|
assert 'data-provisional="1"' in zone_divs[0]
|
||||||
assert "needs adaptation" in html
|
# No badge <span> element and no badge label text anywhere in the body.
|
||||||
assert 'aria-label="needs user or AI adaptation"' in html
|
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():
|
def test_imp84_provisional_badge_never_rendered_in_mixed_zones():
|
||||||
"""Mixed-zone slide: one provisional zone + one normal zone. The badge
|
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 mixed-zone
|
||||||
+ class must appear ONLY in the provisional zone, not bleed into the
|
contract. Mixed-zone slide: one provisional zone + one normal zone. The
|
||||||
normal one (CSS-level isolation should already prevent this, but the
|
silent contract requires that NO badge span and NO `zone--provisional`
|
||||||
template must not emit the badge for both)."""
|
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 = [
|
zones = [
|
||||||
{
|
{
|
||||||
"position": "top",
|
"position": "top",
|
||||||
@@ -735,21 +748,21 @@ def test_u5_provisional_badge_appears_inside_provisional_zone_only():
|
|||||||
html = _render_slide_base(
|
html = _render_slide_base(
|
||||||
zones, layout_preset="vertical-2", layout_css=layout_css
|
zones, layout_preset="vertical-2", layout_css=layout_css
|
||||||
)
|
)
|
||||||
# Exactly one badge span element should be present in the rendered body
|
# No badge <span> element should be rendered anywhere in the body
|
||||||
# (CSS selector in <style> excluded by the helper).
|
# (silent-automation policy).
|
||||||
assert len(_all_badge_spans(html)) == 1
|
assert _all_badge_spans(html) == []
|
||||||
# zone--provisional must appear on exactly one zone div (CSS selector
|
# No zone div should carry the zone--provisional class (visual removed).
|
||||||
# in <style> excluded by the helper).
|
|
||||||
zone_divs = _all_zone_div_openings(html)
|
zone_divs = _all_zone_div_openings(html)
|
||||||
assert len(zone_divs) == 2
|
assert len(zone_divs) == 2
|
||||||
provisional_zone_divs = [d for d in zone_divs if "zone--provisional" in d]
|
assert all("zone--provisional" not in d for d in zone_divs)
|
||||||
assert len(provisional_zone_divs) == 1
|
# data-provisional="1" telemetry must be present on the bottom (provisional)
|
||||||
# The provisional class must be associated with the bottom zone.
|
# zone only — never on the top (non-provisional) zone.
|
||||||
bottom_zone_open = _zone_div_for_position(html, "bottom")
|
bottom_zone_open = _zone_div_for_position(html, "bottom")
|
||||||
assert "zone--provisional" in bottom_zone_open
|
assert 'data-provisional="1"' in bottom_zone_open
|
||||||
assert "zone__needs-adaptation-badge" in bottom_zone_open
|
assert "zone--provisional" not in bottom_zone_open
|
||||||
# The top zone must NOT carry the provisional class.
|
assert "zone__needs-adaptation-badge" not in bottom_zone_open
|
||||||
top_zone_open = _zone_div_for_position(html, "top")
|
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--provisional" not in top_zone_open
|
||||||
assert "zone__needs-adaptation-badge" 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) == []
|
assert _all_badge_spans(html) == []
|
||||||
|
|
||||||
|
|
||||||
def test_u5_slide_base_css_carries_provisional_marker_styles():
|
def test_imp84_slide_base_css_strips_provisional_visual_selectors():
|
||||||
"""The provisional visual contract (dashed outline + striped wash + badge)
|
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 CSS-presence
|
||||||
is defined in slide_base.html <style>. Pin that the relevant CSS class
|
contract. The provisional visual treatment (dashed outline + striped wash
|
||||||
selectors exist in the rendered HTML so a refactor that removes them
|
+ badge) was deleted from `slide_base.html <style>` by IMP-84 u2. Pin
|
||||||
breaks this test rather than silently rendering an unstyled badge.
|
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
|
Scope: the assertion targets the entire rendered HTML (style block plus
|
||||||
specific color / dash pattern, which is a design decision intentionally
|
body). Since the body badge span is also gone (covered separately above),
|
||||||
left malleable (e.g., palette swap for a different theme)."""
|
any occurrence of these strings in the rendered output would only come
|
||||||
|
from a regressed style block."""
|
||||||
zones = [
|
zones = [
|
||||||
{
|
{
|
||||||
"position": "single",
|
"position": "single",
|
||||||
@@ -799,9 +816,9 @@ def test_u5_slide_base_css_carries_provisional_marker_styles():
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
html = _render_slide_base(zones)
|
html = _render_slide_base(zones)
|
||||||
# Style block must define .zone--provisional and the badge selector.
|
# Style block must NOT define .zone--provisional or the badge selector.
|
||||||
assert ".zone--provisional" in html
|
assert ".zone--provisional" not in html
|
||||||
assert ".zone__needs-adaptation-badge" in html
|
assert ".zone__needs-adaptation-badge" not in html
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user