// IMP-92 u5 — Frontend AI repair operational-only formatter test surface. // // Scope (Stage 2 unit u5 contract): // 1) formatAiRepairHumanReviewMessage(...) surfaces a user-facing toast // ONLY on the three operational Anthropic API error kinds (quota / // billing / auth) classified by Step 12 u2 // (classify_operational_error) and aggregated through u3 // ai_repair_status.api_error_kinds. // 2) Non-operational AI failures (validation / coverage_violated / // unsupported_kind / generic "other") return null so the // auto-pipeline stays silent per feedback_auto_pipeline_first and // the #84 operational-vs-non-operational replacement-plan contract. // 3) Replaces the prior IMP-47B u11 surface — previously rendered toasts // for error / coverage_violated / unsupported_kind. After IMP-92 the // ONLY operational reaches the user; non-operational stays silent. // // 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 (`Home.tsx:438`) that calls this helper // after `setRunMeta(...)`; covering the helper covers the user-visible // message text directly without DOM rendering. // // The test file path is preserved from IMP-47B u11 (Stage 2 plan // `Front/client/tests/imp47b_human_review_toast.test.tsx`); the assertions // inside reflect the IMP-92 u5 operational-only contract. import { describe, it, expect } from "vitest"; import { formatAiRepairHumanReviewMessage, type AiRepairStatus, } from "../src/services/designAgentApi"; const baseCounts = { total: 0, applied: 0, no_proposal: 0, no_zone_match: 0, unsupported_kind: 0, error: 0, }; const zeroKinds = { quota: 0, billing: 0, auth: 0, other: 0 }; describe("formatAiRepairHumanReviewMessage (IMP-92 u5 — operational-only)", () => { it("returns null when ai_repair_status is null / undefined", () => { expect(formatAiRepairHumanReviewMessage(null)).toBeNull(); expect(formatAiRepairHumanReviewMessage(undefined)).toBeNull(); }); it("returns null on success / no-AI path (no operational kind present)", () => { const ok: AiRepairStatus = { status: "ok", counts: { ...baseCounts }, api_error_kinds: { ...zeroKinds }, 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 quota operational alert (Anthropic 429 / RateLimitError)", () => { const ai: AiRepairStatus = { status: "error", counts: { ...baseCounts, total: 2, error: 2 }, api_error_kinds: { quota: 2, billing: 0, auth: 0, other: 0 }, unsupported_kind_records: [], error_records: [ { unit_index: 0, source_section_ids: ["03-1"], error: "RateLimitError: rate_limit_exceeded", api_error_kind: "quota", }, { unit_index: 1, source_section_ids: ["03-2"], error: "RateLimitError: rate_limit_exceeded", api_error_kind: "quota", }, ], coverage_status: "ok", dropped_section_ids: [], human_review_required: true, }; const msg = formatAiRepairHumanReviewMessage(ai); expect(msg).not.toBeNull(); expect(msg).toContain("API quota"); expect(msg).toContain("충전 필요"); expect(msg).toContain("2"); }); it("surfaces billing operational alert (Anthropic 402 / PermissionDeniedError)", () => { const ai: AiRepairStatus = { status: "error", counts: { ...baseCounts, total: 1, error: 1 }, api_error_kinds: { quota: 0, billing: 1, auth: 0, other: 0 }, unsupported_kind_records: [], error_records: [ { unit_index: 0, source_section_ids: ["03-1"], error: "PermissionDeniedError: insufficient credits", api_error_kind: "billing", }, ], coverage_status: "ok", dropped_section_ids: [], human_review_required: true, }; const msg = formatAiRepairHumanReviewMessage(ai); expect(msg).not.toBeNull(); expect(msg).toContain("API billing"); expect(msg).toContain("결제 정보 확인"); expect(msg).toContain("1"); }); it("surfaces auth operational alert (Anthropic 401 / AuthenticationError)", () => { const ai: AiRepairStatus = { status: "error", counts: { ...baseCounts, total: 1, error: 1 }, api_error_kinds: { quota: 0, billing: 0, auth: 1, other: 0 }, unsupported_kind_records: [], error_records: [ { unit_index: 0, source_section_ids: ["03-1"], error: "AuthenticationError: invalid x-api-key", api_error_kind: "auth", }, ], coverage_status: "ok", dropped_section_ids: [], human_review_required: true, }; const msg = formatAiRepairHumanReviewMessage(ai); expect(msg).not.toBeNull(); expect(msg).toContain("API key 무효"); expect(msg).toContain(".env"); expect(msg).toContain("1"); }); it("returns null on generic non-operational 'other' API error (silent)", () => { const ai: AiRepairStatus = { status: "error", counts: { ...baseCounts, total: 1, error: 1 }, api_error_kinds: { quota: 0, billing: 0, auth: 0, other: 1 }, unsupported_kind_records: [], error_records: [ { unit_index: 0, source_section_ids: ["03-1"], error: "ValidationError: proposal failed schema", api_error_kind: "other", }, ], coverage_status: "ok", dropped_section_ids: [], human_review_required: true, }; expect(formatAiRepairHumanReviewMessage(ai)).toBeNull(); }); it("returns null on coverage_violated (non-operational, silent)", () => { const ai: AiRepairStatus = { status: "coverage_violated", counts: { ...baseCounts, total: 1, applied: 1 }, api_error_kinds: { ...zeroKinds }, unsupported_kind_records: [], error_records: [], coverage_status: "violated", dropped_section_ids: ["03-2"], human_review_required: true, }; expect(formatAiRepairHumanReviewMessage(ai)).toBeNull(); }); it("returns null on unsupported_kind (non-operational, silent)", () => { const ai: AiRepairStatus = { status: "unsupported_kind", counts: { ...baseCounts, total: 1, unsupported_kind: 1 }, api_error_kinds: { ...zeroKinds }, 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, }; expect(formatAiRepairHumanReviewMessage(ai)).toBeNull(); }); it("returns null on legacy ai_repair_status without api_error_kinds (pre-u3 runs)", () => { // Backward-compat: payloads emitted before u3 plumbing landed don't // carry api_error_kinds. Operational-only contract treats the absence // as "no operational signal" → silent (no toast). const legacy: AiRepairStatus = { status: "error", counts: { ...baseCounts, total: 1, error: 1 }, // api_error_kinds intentionally omitted unsupported_kind_records: [], error_records: [ { unit_index: 0, source_section_ids: ["03-1"], error: "timeout" }, ], coverage_status: "ok", dropped_section_ids: [], human_review_required: true, }; expect(formatAiRepairHumanReviewMessage(legacy)).toBeNull(); }); it("prioritises quota when multiple operational kinds co-occur", () => { // Defensive: a run that accumulated quota + billing errors across // multiple AI repair attempts surfaces the quota line first (the // most-frequently actionable per the issue body ordering). const ai: AiRepairStatus = { status: "error", counts: { ...baseCounts, total: 2, error: 2 }, api_error_kinds: { quota: 1, billing: 1, auth: 0, other: 0 }, unsupported_kind_records: [], error_records: [ { unit_index: 0, source_section_ids: ["03-1"], error: "RateLimitError", api_error_kind: "quota", }, { unit_index: 1, source_section_ids: ["03-2"], error: "PermissionDeniedError", api_error_kind: "billing", }, ], coverage_status: "ok", dropped_section_ids: [], human_review_required: true, }; const msg = formatAiRepairHumanReviewMessage(ai); expect(msg).not.toBeNull(); expect(msg).toContain("API quota"); }); });