fix(ui): revalidate the selected preset's models at submit time

handlePresetChange only ever applies a preset that was verified available at selection time, but that guarantee could go stale by submit time if the caller's model access narrowed in between (a token change re-keys the model query without clearing the selection, since clearing it would erase in-progress Custom edits too). Re-run the same presetAvailability check at the submit boundary instead of trusting state gathered earlier, so a stale preset can no longer create a router referencing models the current caller doesn't have.
This commit is contained in:
Tin Chi Lo 2026-08-03 17:31:47 -07:00
parent e185362f8f
commit aa4843cb02
2 changed files with 43 additions and 0 deletions

View file

@ -306,6 +306,35 @@ describe("AddAutoRouterTab", () => {
expect(isOptionDisabled(optionByLabel("OpenAI Family")!)).toBe(true);
});
// handlePresetChange only ever applies an available preset, but that guarantee can go stale by
// submit time: select under a caller with the full family, then switch to a caller missing one
// of its models. Nothing clears the selection (that would erase in-progress Custom edits too),
// so submit itself must re-verify against the current caller's list before creating the router.
it("blocks submit when the selected preset's models are no longer available for the current caller", async () => {
const user = userEvent.setup();
mockFetchAvailableModels
.mockResolvedValueOnce(ALL_FAMILY_MODELS)
.mockResolvedValueOnce(ALL_FAMILY_MODELS.filter((m) => m.model_group !== "o3"));
const { rerender } = renderWithProviders(
<AddAutoRouterTab handleOk={vi.fn()} accessToken="caller-a" userRole="Admin" />,
);
openTemplateDropdown();
await waitFor(() => expect(isOptionDisabled(optionByLabel("OpenAI Family")!)).toBe(false));
fireEvent.click(optionByLabel("OpenAI Family")!);
rerender(<AddAutoRouterTab handleOk={vi.fn()} accessToken="caller-b" userRole="Admin" />);
await waitFor(() => expect(mockFetchAvailableModels).toHaveBeenCalledTimes(2));
await user.type(screen.getByPlaceholderText(/smart_router/i), "stale-preset-router");
await user.click(screen.getByRole("button", { name: /add auto router/i }));
expect(NotificationManager.fromBackend).toHaveBeenCalledWith(
"This template's models are no longer available. Please reselect a template or switch to Custom.",
);
expect(mockHandleAddAutoRouterSubmit).not.toHaveBeenCalled();
});
// Prefill must preserve a preset's deliberately-falsy fields (a 0 match threshold, an empty
// escalation list). Using `||` instead of `??` would swap the 0 for the create-form default and
// re-enable escalation the preset meant to turn off, so this asserts the exact submitted values.

View file

@ -189,6 +189,20 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
return;
}
// handlePresetChange only ever applies a preset that was available at selection time; that
// guarantee can go stale by submit time (e.g. the caller's model access narrowed since), so
// re-verify here rather than trust state gathered earlier.
if (selectedPreset !== "custom") {
const preset = getPresetByKey(selectedPreset);
if (!preset || presetAvailability(preset).kind !== "available") {
setShowValidationErrors(true);
NotificationManager.fromBackend(
"This template's models are no longer available. Please reselect a template or switch to Custom.",
);
return;
}
}
const {
tiers,
classifier_type: classifierType,