mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
test(ui): cover fusion mutations and validation
This commit is contained in:
parent
a486537fe6
commit
a01c230990
3 changed files with 88 additions and 13 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue