fix(ui): stop treating a failed background model refetch as unverifiable

react-query keeps the last successful list around when a later refetch fails: data stays populated, but isError flips true. presetAvailability read isError alone, so once a caller had a good cached list, any subsequent refetch hiccup (window refocus, a manual retry that itself fails, etc.) made every preset unverifiable again, wrongly blocked an already-selected preset at the new submit-time check, and showed a "models are no longer available" toast for models that were, per the cache, still there. Only treat the state as unverifiable when there has never been a successful fetch (data is still undefined); otherwise keep trusting the cached list, matching how react-query itself treats stale-but-valid data.
This commit is contained in:
Tin Chi Lo 2026-08-03 17:48:41 -07:00
parent aa4843cb02
commit b140c02a91
2 changed files with 34 additions and 4 deletions

View file

@ -1,5 +1,5 @@
import { renderWithProviders, screen, waitFor, testQueryClient } from "../../../tests/test-utils";
import { fireEvent } from "@testing-library/react";
import { act, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import AddAutoRouterTab from "./add_auto_router_tab";
@ -200,6 +200,31 @@ describe("AddAutoRouterTab", () => {
expect(mockFetchAvailableModels).toHaveBeenCalledTimes(2);
});
// react-query keeps the last successful list when a later refetch fails, so a background
// refetch error must not treat an already-verified, still-cached preset as unverifiable: the
// caller never sees a stale error message, and a preset they already picked stays submittable.
it("keeps a selected preset submit-reachable when a background refetch fails but cached models remain valid", async () => {
const user = userEvent.setup();
mockFetchAvailableModels.mockResolvedValueOnce(ALL_FAMILY_MODELS).mockRejectedValue(new Error("boom"));
renderWithProviders(<Harness />);
openTemplateDropdown();
await waitFor(() => expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(false));
fireEvent.click(optionByLabel("Anthropic Family")!);
await act(async () => {
await testQueryClient.refetchQueries({ queryKey: ["availableModels", "autoRouter", "token"] });
});
await user.type(screen.getByPlaceholderText(/smart_router/i), "resilient-router");
await user.click(screen.getByRole("button", { name: /add auto router/i }));
await waitFor(() => expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalled());
expect(NotificationManager.fromBackend).not.toHaveBeenCalledWith(
"This template's models are no longer available. Please reselect a template or switch to Custom.",
);
});
// The headline behavior: selecting a preset must pre-fill the tier config so the created
// router carries the preset's models. Real tier validation runs here (getMissingTiersError is
// not stubbed), so if selection stopped pre-filling, the empty tiers would either block the

View file

@ -106,7 +106,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
}, [accessToken]);
const {
data: modelInfo = [],
data,
isLoading: modelsLoading,
isError: modelsError,
refetch: refetchModels,
@ -115,6 +115,11 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
queryFn: () => fetchAvailableModels(accessToken),
enabled: Boolean(accessToken),
});
const modelInfo = React.useMemo(() => data ?? [], [data]);
// react-query keeps the last successful list around when a later refetch fails, so isError
// alone can't tell "never loaded" apart from "loaded, then a background refetch errored" - only
// the former leaves us with nothing trustworthy to verify a preset's models against.
const modelsUnverifiable = modelsError && data === undefined;
const isAdmin = all_admin_roles.includes(userRole);
@ -133,7 +138,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
// makes the load-race (pick during loading, then discover a missing model) unrepresentable.
const presetAvailability = (preset: AutoRouterPreset): PresetAvailability => {
if (modelsLoading) return { kind: "loading" };
if (modelsError) return { kind: "unverifiable" };
if (modelsUnverifiable) return { kind: "unverifiable" };
const missing = getMissingModelsInPreset(preset, availableModelSet);
return missing.length > 0 ? { kind: "missing_models", models: missing } : { kind: "available" };
};
@ -372,7 +377,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
{showValidationErrors && !selectedPreset && (
<div className="text-xs mt-1 text-red-500">Please select a template</div>
)}
{modelsError && (
{modelsUnverifiable && (
<div className="text-xs mt-1 text-red-500">
Could not load available models.{" "}
<button type="button" className="underline" onClick={() => refetchModels()}>