- AiRepairStatus interface mirrors backend step20 u8 schema
- formatAiRepairHumanReviewMessage(): pure helper for the three failure axes
(error / coverage_violated / unsupported_kind) — null on success/no-AI
- Home.tsx: toast.error(aiReviewMsg) after run completion
- FramePanel.tsx: reject-click window.confirm guard ("frame 유지 + AI 재구성")
- imp47b_human_review_toast.test.tsx: 6 vitest cases (null/false/3 axes/other)
Verification (frontend node_modules junction from main worktree):
- vitest imp47b_human_review_toast.test.tsx: 6/6 passed
- vitest full suite: 19/19 passed (imp41_application_mode 13 + u11 6, zero regression)
Hunk-split rationale:
- stash@{0} (imp47b-frontend-u11-pre-rebase, captured before IMP-41 merged)
contained inline IMP-41 helpers alongside u11 changes
- HEAD already has IMP-41 helper-based implementation (buildBadgeTitle /
mergeApplicationCandidates from services/applicationMode.ts, f358604)
- This commit adds ONLY the u11 surface on top of HEAD's IMP-41 baseline
- No IMP-41 hunk regression: buildBadgeTitle / mergeApplicationCandidates /
applicationMode forwarding preserved verbatim
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
// IMP-47B u11 — Frontend ai_repair_status notification surfacing.
|
|
//
|
|
// Scope (Stage 2 unit u11 contract):
|
|
// 1) loadRun → RunMeta.ai_repair_status exposes the u8 step20 payload.
|
|
// 2) formatAiRepairHumanReviewMessage(...) returns user-facing notification
|
|
// text on the three failure axes (error / coverage_violated /
|
|
// unsupported_kind) and returns null on success / no-AI paths.
|
|
//
|
|
// Pure-function unit test (no React Testing Library required — vitest is
|
|
// already in devDependencies; @testing-library/* is NOT installed). The
|
|
// Home.tsx wiring is a 2-line site that calls this helper after
|
|
// setRunMeta(...); covering the helper covers the user-visible message text
|
|
// directly without DOM rendering.
|
|
//
|
|
// File extension is `.tsx` per Stage 2 unit contract path; no JSX is required
|
|
// for these assertions but the extension allows future RTL-based tests to
|
|
// land here without renaming.
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
formatAiRepairHumanReviewMessage,
|
|
type AiRepairStatus,
|
|
} from "../src/services/designAgentApi";
|
|
|
|
const baseCounts = {
|
|
total: 1,
|
|
applied: 0,
|
|
no_proposal: 0,
|
|
no_zone_match: 0,
|
|
unsupported_kind: 0,
|
|
error: 0,
|
|
};
|
|
|
|
describe("formatAiRepairHumanReviewMessage (IMP-47B u11)", () => {
|
|
it("returns null when ai_repair_status is null (legacy / pre-Step12 abort)", () => {
|
|
expect(formatAiRepairHumanReviewMessage(null)).toBeNull();
|
|
expect(formatAiRepairHumanReviewMessage(undefined)).toBeNull();
|
|
});
|
|
|
|
it("returns null when human_review_required=false (success / no-AI path)", () => {
|
|
const ok: AiRepairStatus = {
|
|
status: "ok",
|
|
counts: { ...baseCounts, total: 0 },
|
|
unsupported_kind_records: [],
|
|
error_records: [],
|
|
coverage_status: "ok",
|
|
dropped_section_ids: [],
|
|
human_review_required: false,
|
|
};
|
|
expect(formatAiRepairHumanReviewMessage(ok)).toBeNull();
|
|
|
|
const applied: AiRepairStatus = {
|
|
...ok,
|
|
status: "applied",
|
|
counts: { ...baseCounts, total: 1, applied: 1 },
|
|
};
|
|
expect(formatAiRepairHumanReviewMessage(applied)).toBeNull();
|
|
});
|
|
|
|
it("surfaces AI call failures with count + frame/manual guidance", () => {
|
|
const errored: AiRepairStatus = {
|
|
status: "error",
|
|
counts: { ...baseCounts, total: 2, error: 2 },
|
|
unsupported_kind_records: [],
|
|
error_records: [
|
|
{ unit_index: 0, source_section_ids: ["03-1"], error: "timeout" },
|
|
{ unit_index: 1, source_section_ids: ["03-2"], error: "validation" },
|
|
],
|
|
coverage_status: "ok",
|
|
dropped_section_ids: [],
|
|
human_review_required: true,
|
|
};
|
|
const msg = formatAiRepairHumanReviewMessage(errored);
|
|
expect(msg).not.toBeNull();
|
|
expect(msg).toContain("AI 재구성 호출 실패");
|
|
expect(msg).toContain("2");
|
|
expect(msg).toContain("다른 frame 선택 또는 수동 편집 필요");
|
|
});
|
|
|
|
it("surfaces coverage violations with the dropped section ids", () => {
|
|
const dropped: AiRepairStatus = {
|
|
status: "coverage_violated",
|
|
counts: { ...baseCounts, total: 1, applied: 1 },
|
|
unsupported_kind_records: [],
|
|
error_records: [],
|
|
coverage_status: "violated",
|
|
dropped_section_ids: ["03-2"],
|
|
human_review_required: true,
|
|
};
|
|
const msg = formatAiRepairHumanReviewMessage(dropped);
|
|
expect(msg).not.toBeNull();
|
|
expect(msg).toContain("콘텐츠 누락");
|
|
expect(msg).toContain("03-2");
|
|
expect(msg).toContain("다른 frame 선택 또는 수동 편집 필요");
|
|
});
|
|
|
|
it("surfaces unsupported proposal kinds with the unsupported count", () => {
|
|
const unsupported: AiRepairStatus = {
|
|
status: "unsupported_kind",
|
|
counts: { ...baseCounts, total: 1, unsupported_kind: 1 },
|
|
unsupported_kind_records: [
|
|
{
|
|
unit_index: 0,
|
|
source_section_ids: ["03-1"],
|
|
apply_status: "unsupported_kind_for_reject_route:builder_options_patch",
|
|
},
|
|
],
|
|
error_records: [],
|
|
coverage_status: "ok",
|
|
dropped_section_ids: [],
|
|
human_review_required: true,
|
|
};
|
|
const msg = formatAiRepairHumanReviewMessage(unsupported);
|
|
expect(msg).not.toBeNull();
|
|
expect(msg).toContain("AI 제안 형식 미지원");
|
|
expect(msg).toContain("1");
|
|
expect(msg).toContain("다른 frame 선택 또는 수동 편집 필요");
|
|
});
|
|
|
|
it("falls back to a generic human_review message on unknown status enums", () => {
|
|
const future: AiRepairStatus = {
|
|
status: "future_axis_not_yet_mapped",
|
|
counts: { ...baseCounts, total: 0 },
|
|
unsupported_kind_records: [],
|
|
error_records: [],
|
|
coverage_status: "ok",
|
|
dropped_section_ids: [],
|
|
human_review_required: true,
|
|
};
|
|
const msg = formatAiRepairHumanReviewMessage(future);
|
|
expect(msg).not.toBeNull();
|
|
expect(msg).toContain("human_review");
|
|
expect(msg).toContain("future_axis_not_yet_mapped");
|
|
});
|
|
});
|