test(ui): cover fusion mutations and validation

This commit is contained in:
moe-berri 2026-09-03 16:34:45 -07:00
parent a486537fe6
commit a01c230990
3 changed files with 88 additions and 13 deletions

View file

@ -10,6 +10,8 @@ const modelPatchUpdateCall = vi.fn();
const modelDeleteCall = vi.fn();
const invalidate = vi.fn().mockResolvedValue(undefined);
const useFusionRouters = vi.fn();
const toastSuccess = vi.fn();
const toastFromError = vi.fn();
vi.mock("@/components/networking", () => ({
modelCreateCall: (...args: unknown[]) => modelCreateCall(...args),
@ -58,9 +60,21 @@ vi.mock("@/components/common_components/team_dropdown", () => ({
),
}));
vi.mock("@/components/common_components/DeleteResourceModal", () => ({ default: () => null }));
vi.mock("@/components/common_components/DeleteResourceModal", () => ({
default: ({ title, message, onOk }: { title: string; message: string; onOk: () => void }) => (
<div role="dialog" aria-label={title}>
<p>{message}</p>
<button type="button" onClick={onOk}>
Confirm delete
</button>
</div>
),
}));
vi.mock("@/lib/toast", () => ({
toast: { success: vi.fn(), fromError: vi.fn() },
toast: {
success: (...args: unknown[]) => toastSuccess(...args),
fromError: (...args: unknown[]) => toastFromError(...args),
},
}));
const existingDeployment = {
@ -101,6 +115,7 @@ describe("FusionModelsPanel", () => {
useFusionRouters.mockReturnValue({ data: [], isLoading: false });
modelCreateCall.mockResolvedValue({});
modelPatchUpdateCall.mockResolvedValue({});
modelDeleteCall.mockResolvedValue({});
});
it("creates an auto Fusion model with an ordinary model/new payload", async () => {
@ -137,6 +152,25 @@ describe("FusionModelsPanel", () => {
});
});
it("keeps the form open and shows the backend error when creation fails", async () => {
modelCreateCall.mockRejectedValue(new Error("backend unavailable"));
const user = userEvent.setup();
renderPanel();
await user.click(screen.getByRole("button", { name: "Add Fusion Model" }));
await user.type(screen.getByLabelText("Model name"), "fusion/coding");
await user.click(screen.getByRole("button", { name: "Choose panel models" }));
await user.click(screen.getByLabelText("Outer model"));
await user.click(await screen.findByRole("option", { name: "outer" }, { timeout: 5000 }));
await user.click(screen.getByRole("button", { name: "Load search tools" }));
await user.click(screen.getByRole("button", { name: "Create Fusion Model" }));
expect(await screen.findByText("backend unavailable")).toBeVisible();
expect(screen.getByRole("dialog", { name: "Add Fusion Model" })).toBeVisible();
expect(screen.getByRole("button", { name: "Create Fusion Model" })).toBeEnabled();
expect(invalidate).not.toHaveBeenCalled();
});
it("shows readable, full-width deliberation presets", async () => {
const user = userEvent.setup();
renderPanel();
@ -200,4 +234,32 @@ describe("FusionModelsPanel", () => {
"fusion-id",
);
});
it("deletes a stored Fusion model and refreshes the list", async () => {
useFusionRouters.mockReturnValue({ data: [existingDeployment], isLoading: false });
const user = userEvent.setup();
renderPanel();
await user.click(screen.getByRole("button", { name: "Delete fusion/existing" }));
expect(screen.getByRole("dialog", { name: "Delete Fusion Model" })).toHaveTextContent("fusion/existing");
await user.click(screen.getByRole("button", { name: "Confirm delete" }));
await waitFor(() => expect(modelDeleteCall).toHaveBeenCalledWith("token", "fusion-id"));
await waitFor(() => expect(invalidate).toHaveBeenCalled());
expect(toastSuccess).toHaveBeenCalledWith("Deleted Fusion model: fusion/existing");
});
it("keeps the delete dialog open when deletion fails", async () => {
useFusionRouters.mockReturnValue({ data: [existingDeployment], isLoading: false });
modelDeleteCall.mockRejectedValue(new Error("backend unavailable"));
const user = userEvent.setup();
renderPanel();
await user.click(screen.getByRole("button", { name: "Delete fusion/existing" }));
await user.click(screen.getByRole("button", { name: "Confirm delete" }));
await waitFor(() => expect(toastFromError).toHaveBeenCalled());
expect(screen.getByRole("dialog", { name: "Delete Fusion Model" })).toBeVisible();
expect(invalidate).not.toHaveBeenCalled();
});
});

View file

@ -74,6 +74,15 @@ describe("Fusion model configuration", () => {
).toMatchObject({ search_tool_name: "web-search", max_tool_calls: 4 });
});
it("rejects non-finite and fractional numeric settings before sending them", () => {
expect(fusionConfigError(validValue({ panel_timeout_seconds: Number.NaN }), false)).toMatch(/timeout/);
expect(fusionConfigError(validValue({ max_candidate_chars: 1000.5 }), false)).toMatch(/Candidate limit/);
expect(fusionConfigError(validValue({ temperature: Number.NaN }), false)).toMatch(/temperature/);
expect(fusionConfigError(validValue({ web_access_enabled: true, search_tool_name: " " }), false)).toMatch(
/Search Tool/,
);
});
it("includes team scope only when required", () => {
expect(fusionModelPayload(validValue({ team_id: "team-1" }), true).model_info).toEqual({ team_id: "team-1" });
expect(fusionConfigError(validValue(), true)).toBe("Select a team to continue.");

View file

@ -47,7 +47,13 @@ const numberOr = (value: unknown, fallback: number): number =>
const REASONING_EFFORTS = new Set<FusionReasoningEffort>(["none", "minimal", "low", "medium", "high", "xhigh"]);
const webAccessConfigError = (value: FusionFormValue): string | null =>
value.web_access_enabled && !value.search_tool_name ? "Select a Search Tool or turn Web access off." : null;
value.web_access_enabled && !value.search_tool_name.trim() ? "Select a Search Tool or turn Web access off." : null;
const isFiniteNumberInRange = (value: number, minimum: number, maximum: number): boolean =>
Number.isFinite(value) && value >= minimum && value <= maximum;
const isIntegerInRange = (value: number, minimum: number, maximum: number): boolean =>
Number.isInteger(value) && value >= minimum && value <= maximum;
export const parseFusionConfig = (value: unknown): FusionRouterConfigValue => {
const config = asRecord(value);
@ -77,26 +83,24 @@ export const parseFusionConfig = (value: unknown): FusionRouterConfigValue => {
export const fusionConfigError = (value: FusionFormValue, requiresTeamScope: boolean): string | null => {
if (!value.model_name.trim()) return "Fusion model name is required.";
if (requiresTeamScope && !value.team_id) return "Select a team to continue.";
if (!value.outer_model) return "Select the outer model.";
if (!value.outer_model.trim()) return "Select the outer model.";
if (value.panel_models.length < 1) return "Select at least one panel model.";
if (value.panel_models.length > 8) return "A Fusion panel can contain at most eight models.";
const webAccessError = webAccessConfigError(value);
if (webAccessError) return webAccessError;
if (value.panel_timeout_seconds <= 0 || value.panel_timeout_seconds > 600) {
if (!isFiniteNumberInRange(value.panel_timeout_seconds, Number.MIN_VALUE, 600)) {
return "Panel and analyst timeout must be between 1 and 600 seconds.";
}
if (value.max_candidate_chars < 1000 || value.max_candidate_chars > 50000) {
if (!isIntegerInRange(value.max_candidate_chars, 1000, 50000)) {
return "Candidate limit must be between 1,000 and 50,000 characters.";
}
if (
!Number.isInteger(value.max_completion_tokens) ||
value.max_completion_tokens < 1 ||
value.max_completion_tokens > 128000
) {
if (!isIntegerInRange(value.max_completion_tokens, 1, 128000)) {
return "Internal output tokens must be between 1 and 128,000.";
}
if (value.temperature < 0 || value.temperature > 2) return "Panel temperature must be between 0 and 2.";
if (!Number.isInteger(value.max_tool_calls) || value.max_tool_calls < 1 || value.max_tool_calls > 16) {
if (!isFiniteNumberInRange(value.temperature, 0, 2)) {
return "Panel temperature must be between 0 and 2.";
}
if (!isIntegerInRange(value.max_tool_calls, 1, 16)) {
return "Tool calls must be between 1 and 16.";
}
return null;