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;
|
||||
}
|
||||
|
||||
// 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({
|
||||
slidePlan,
|
||||
selectedZone,
|
||||
@@ -49,17 +62,9 @@ export default function FramePanel({
|
||||
|
||||
const handleFrameSelect = React.useCallback(
|
||||
(candidate: FrameCandidate) => {
|
||||
const isReject = candidate.label === "reject";
|
||||
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);
|
||||
applyFrameSelection(candidate, onFrameSelect);
|
||||
},
|
||||
[currentFrameId, onFrameSelect],
|
||||
[onFrameSelect],
|
||||
);
|
||||
|
||||
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\(/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user