From ed86414ab5573a1efc61cf5c9b33675c0362e0a9 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 12 Aug 2026 14:48:39 -0700 Subject: [PATCH] fix(ui): reject garbled number input instead of silently clearing limits Switch the default team/user settings number fields from type=number to type=text with inputMode, so values the browser flags as badInput (e.g. 12e) reach zod validation and show an error instead of being read as empty and saved as null. Also add noValidate to the user settings form so zod messages render there too. --- .../DefaultTeamSettingsForm.test.tsx | 23 +++++++++++++++---- .../DefaultTeamSettingsForm.tsx | 6 ++--- .../DefaultUserSettingsForm.test.tsx | 18 +++++++++++++-- .../DefaultUserSettingsForm.tsx | 6 ++--- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx index a80eb4fbe1e..7f9e9b4c55b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -139,13 +139,28 @@ describe("DefaultTeamSettingsForm", () => { const { updateSettings } = renderForm(); await enterEditMode(user); - fireEvent.change(await screen.findByLabelText("TPM Limit"), { target: { value: "12.5" } }); + await user.clear(await screen.findByLabelText("TPM Limit")); + await user.type(screen.getByLabelText("TPM Limit"), "12.5"); await user.click(await saveButton()); expect(await screen.findByText("Must be a non-negative whole number")).toBeInTheDocument(); expect(updateSettings).not.toHaveBeenCalled(); }); + it("keeps number-ish junk visible and rejects it instead of silently clearing the limit", async () => { + const user = userEvent.setup(); + const { updateSettings } = renderForm(); + + await enterEditMode(user); + await user.clear(await screen.findByLabelText("TPM Limit")); + await user.type(screen.getByLabelText("TPM Limit"), "12e"); + await user.click(await saveButton()); + + expect(screen.getByLabelText("TPM Limit")).toHaveValue("12e"); + expect(await screen.findByText("Must be a non-negative whole number")).toBeInTheDocument(); + expect(updateSettings).not.toHaveBeenCalled(); + }); + it("sends the models selection through unchanged, sentinel values included", async () => { const user = userEvent.setup(); const { updateSettings } = renderForm(); @@ -223,7 +238,7 @@ describe("DefaultTeamSettingsForm", () => { expect(NotificationsManager.fromBackend).toHaveBeenCalledWith("Set `'STORE_MODEL_IN_DB='True'` in your env."), ); expect(await saveButton()).toBeEnabled(); - expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue(250); + expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue("250"); }); it("discards edits and returns to the read-only view when Cancel is pressed", async () => { @@ -240,7 +255,7 @@ describe("DefaultTeamSettingsForm", () => { expect(updateSettings).not.toHaveBeenCalled(); await enterEditMode(user); - expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue(100); + expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue("100"); expect(await saveButton()).toBeDisabled(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx index 56a7cc13844..43ec838dc59 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/_components/default-team-settings/DefaultTeamSettingsForm.tsx @@ -175,7 +175,7 @@ const SettingsForm = ({ initialValues, updateSettings, onCancel, onSaved }: Sett label="Max Budget (USD)" description="Default maximum budget for new teams" > - {({ ref, ...field }) => } + {({ ref, ...field }) => } - {({ ref, ...field }) => } + {({ ref, ...field }) => } - {({ ref, ...field }) => } + {({ ref, ...field }) => } { expect(updateSettings).toHaveBeenCalledWith({ ...SAVED_BODY, max_budget: null }); }); + it("keeps number-ish junk visible and rejects it instead of silently clearing the budget", async () => { + const user = userEvent.setup(); + const { updateSettings } = renderForm(); + + await enterEditMode(user); + await user.clear(await screen.findByLabelText("Max Budget (USD)")); + await user.type(screen.getByLabelText("Max Budget (USD)"), "12e"); + await user.click(await saveButton()); + + expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue("12e"); + expect(await screen.findByText("Must be a non-negative number")).toBeInTheDocument(); + expect(updateSettings).not.toHaveBeenCalled(); + }); + it("sends the models selection through unchanged, sentinel values included", async () => { const user = userEvent.setup(); const { updateSettings } = renderForm(); @@ -275,7 +289,7 @@ describe("DefaultUserSettingsForm", () => { expect(NotificationsManager.fromBackend).toHaveBeenCalledWith("Team(s) not found: team-alhpa."), ); expect(await saveButton()).toBeEnabled(); - expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue(250); + expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue("250"); }); it("discards edits and returns to the read-only view when Cancel is pressed", async () => { @@ -293,7 +307,7 @@ describe("DefaultUserSettingsForm", () => { expect(updateSettings).not.toHaveBeenCalled(); await enterEditMode(user); - expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue(100); + expect(screen.getByLabelText("Max Budget (USD)")).toHaveValue("100"); expect(await saveButton()).toBeDisabled(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx index 14520b71b31..c2fbcdb1112 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx @@ -124,7 +124,7 @@ const TeamsField = ({ control }: { control: SettingsControl }) => { {({ ref, ...budgetField }) => ( - + )} @@ -244,7 +244,7 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on const onSubmit = form.handleSubmit((values) => mutation.mutate(values)); return ( -
+ - {({ ref, ...field }) => } + {({ ref, ...field }) => }