Forward backend Step 9 `unit.application_candidates[]` (application_mode /
auto_applicable / delegated_to) onto FrameCandidate and surface the
application_mode as a Korean consequence phrase in the FramePanel V4-label
inline badge tooltip. Deterministic frontend-only refactor; no LLM call,
no V4-label color change, no outer composedTitle change.
u1: types/designAgent.ts — add optional applicationMode / autoApplicable /
delegatedTo on FrameCandidate (legacy fixtures keep undefined).
u2: services/applicationMode.ts (new) — pure helper exporting
ApplicationMode union, APPLICATION_MODE_TOOLTIP_KR (keyed by backend
mode VALUE, NOT V4 label), buildBadgeTitle, mergeApplicationCandidates.
u3: tests/imp41_application_mode.test.ts (new) — 13 Vitest cases pinning
composite output per mode, undefined/unknown→legacy fallback, merge by
template_id, skip missing/empty/non-string keys, first-wins on dupes,
empty/null/non-array input.
u4: services/designAgentApi.ts — bridge consumes mergeApplicationCandidates
and forwards three fields onto FrameCandidate while preserving
LABEL_PRIORITY sort and TOP_N_FRAMES slicing.
u5: components/FramePanel.tsx — V4-label badge `title` now calls
`buildBadgeTitle(candidate.label, candidate.applicationMode)`;
badge color className map preserved verbatim; outer composedTitle
untouched.
Scope-qualified verification (5 files, IMP-41 axis only):
- Vitest: client/tests/imp41_application_mode.test.ts — 13/13 PASS.
- Diff↔Plan parity: 5 files match Stage 2 plan, no scope creep.
- AI-isolation contract honored: tooltip values originate from backend
enum; no frontend re-derivation from V4 label.
- No spacing/font shrink; clipping resolution stays at layout/zone/frame
layer (feedback_phase_z_spacing_direction).
Pre-existing unrelated diagnostics (BottomActions.tsx,
imp47b_human_review_toast.test.tsx) remain open on their own axes and are
not gated by this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
// IMP-41 u3 — Vitest coverage for application_mode helper (issue #70).
|
|
//
|
|
// Scope (Stage 2 unit u3 contract):
|
|
// 1) buildBadgeTitle: composite output for each known mode + legacy fallback
|
|
// (undefined applicationMode) + unknown fallback (string not in
|
|
// APPLICATION_MODE_TOOLTIP_KR).
|
|
// 2) mergeApplicationCandidates: array → Map<template_id, candidate>
|
|
// semantics, including skip-missing-key and empty-input.
|
|
//
|
|
// Pure helper unit test — no React, no DOM, no fetch. Aligns with the
|
|
// AI-isolation contract: assertions key by backend application_mode VALUE,
|
|
// never by V4 label.
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
buildBadgeTitle,
|
|
mergeApplicationCandidates,
|
|
APPLICATION_MODE_TOOLTIP_KR,
|
|
} from "../src/services/applicationMode";
|
|
|
|
describe("buildBadgeTitle (IMP-41 u3)", () => {
|
|
it("returns composite '<consequence> (<mode>)' for direct_insert", () => {
|
|
expect(buildBadgeTitle("use_as_is", "direct_insert")).toBe(
|
|
`${APPLICATION_MODE_TOOLTIP_KR.direct_insert} (direct_insert)`,
|
|
);
|
|
});
|
|
|
|
it("returns composite output for same_frame_with_adjustment", () => {
|
|
expect(
|
|
buildBadgeTitle("light_edit", "same_frame_with_adjustment"),
|
|
).toBe(
|
|
`${APPLICATION_MODE_TOOLTIP_KR.same_frame_with_adjustment} (same_frame_with_adjustment)`,
|
|
);
|
|
});
|
|
|
|
it("returns composite output for layout_or_region_change", () => {
|
|
expect(
|
|
buildBadgeTitle("restructure", "layout_or_region_change"),
|
|
).toBe(
|
|
`${APPLICATION_MODE_TOOLTIP_KR.layout_or_region_change} (layout_or_region_change)`,
|
|
);
|
|
});
|
|
|
|
it("returns composite output for exclude", () => {
|
|
expect(buildBadgeTitle("reject", "exclude")).toBe(
|
|
`${APPLICATION_MODE_TOOLTIP_KR.exclude} (exclude)`,
|
|
);
|
|
});
|
|
|
|
it("falls back to 'V4 label: <label>' when applicationMode is undefined (legacy fixtures pre-IMP-32)", () => {
|
|
expect(buildBadgeTitle("use_as_is", undefined)).toBe("V4 label: use_as_is");
|
|
});
|
|
|
|
it("falls back to 'V4 label: <label>' when applicationMode is an unknown string", () => {
|
|
expect(buildBadgeTitle("light_edit", "some_future_mode")).toBe(
|
|
"V4 label: light_edit",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("mergeApplicationCandidates (IMP-41 u3)", () => {
|
|
it("returns empty Map when input is undefined", () => {
|
|
const result = mergeApplicationCandidates(undefined);
|
|
expect(result).toBeInstanceOf(Map);
|
|
expect(result.size).toBe(0);
|
|
});
|
|
|
|
it("returns empty Map when input is null", () => {
|
|
const result = mergeApplicationCandidates(null);
|
|
expect(result.size).toBe(0);
|
|
});
|
|
|
|
it("returns empty Map when input is not an array", () => {
|
|
expect(mergeApplicationCandidates({ template_id: "f01" }).size).toBe(0);
|
|
expect(mergeApplicationCandidates("f01").size).toBe(0);
|
|
expect(mergeApplicationCandidates(42).size).toBe(0);
|
|
});
|
|
|
|
it("returns empty Map when input is an empty array", () => {
|
|
expect(mergeApplicationCandidates([]).size).toBe(0);
|
|
});
|
|
|
|
it("keys entries by template_id and preserves the candidate payload", () => {
|
|
const ac1 = {
|
|
template_id: "f01",
|
|
label: "use_as_is",
|
|
application_mode: "direct_insert",
|
|
auto_applicable: true,
|
|
delegated_to: null,
|
|
};
|
|
const ac2 = {
|
|
template_id: "f17",
|
|
label: "light_edit",
|
|
application_mode: "same_frame_with_adjustment",
|
|
auto_applicable: false,
|
|
delegated_to: "step10_contract_check",
|
|
};
|
|
const result = mergeApplicationCandidates([ac1, ac2]);
|
|
expect(result.size).toBe(2);
|
|
expect(result.get("f01")).toBe(ac1);
|
|
expect(result.get("f17")).toBe(ac2);
|
|
});
|
|
|
|
it("skips entries with missing or non-string template_id", () => {
|
|
const result = mergeApplicationCandidates([
|
|
{ label: "use_as_is" }, // missing template_id
|
|
{ template_id: "", label: "light_edit" }, // empty string
|
|
{ template_id: 17, label: "restructure" }, // non-string
|
|
{ template_id: "f29", label: "reject" }, // valid
|
|
]);
|
|
expect(result.size).toBe(1);
|
|
expect(result.has("f29")).toBe(true);
|
|
expect(result.has("")).toBe(false);
|
|
});
|
|
|
|
it("keeps the first occurrence on duplicate template_id keys (deterministic)", () => {
|
|
const first = { template_id: "f01", label: "use_as_is" };
|
|
const second = { template_id: "f01", label: "reject" };
|
|
const result = mergeApplicationCandidates([first, second]);
|
|
expect(result.size).toBe(1);
|
|
expect(result.get("f01")).toBe(first);
|
|
});
|
|
});
|