From af32e0041c754ca482b808afd967bbf7635e5a4c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:29:09 -0300 Subject: [PATCH] fix(dashboard): switch to visible filter after auto-hiding failed models in test-all (#4887) (#4991) * fix(dashboard): switch to visible filter after auto-hiding failed models in OAuth provider test-all (#4887) * test(dashboard): move #4887 test into tests/unit/ui so a CI runner collects it --------- Co-authored-by: Diego Rodrigues de Sa e Souza --- CHANGELOG.md | 2 +- .../[id]/hooks/useModelVisibilityHandlers.ts | 9 ++ ...odel-visibility-handlers-test-all.test.tsx | 142 ++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 tests/unit/ui/use-model-visibility-handlers-test-all.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e18c7bd1b..3e444a0a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ _In development — bullets added per PR; finalized at release._ ### 🔧 Bug Fixes -- **fix(sse):** auto-combo now soft-penalizes exhausted provider connections so dead providers stop winning. A connection in a terminal/transient status (`credits_exhausted` / `rate_limited` / `banned` / `expired` / future-dated `unavailable`) with no numeric quota fetcher used to keep `quotaRemaining=100` and score identically to a healthy provider — routing kept picking it. With the quota-preflight HARD cutoff OFF (the default), such a candidate is no longer hard-blocked (which would surface a misleading "below quota cutoff" 429); instead its auto-combo score is multiplied by `STATUS_SOFT_DEPRIORITIZE_FACTOR` (default 0.5) so it ranks strictly below an otherwise-identical healthy candidate. The existing opt-in hard-cutoff path is unchanged. (#4540) - **fix(dashboard):** show custom provider given-name instead of internal id across dashboard pages — cache, combo health, compression analytics, cost overview, health/autopilot, provider stats, route explainability, provider utilization, runtime. Adds shared `resolveProviderName` resolver and `useProviderNodeMap` hook. (#4603) +- **fix(dashboard):** on OAuth providers (e.g. GLM Coding), "Test all models" with auto-hide-failed now switches the model list to the "visible" filter after the run, so just-hidden failed models actually disappear on-screen — parity with the passthrough-provider path (#3610). Previously they were hidden in the DB but stayed visible under the "All" filter, so it looked like nothing was hidden. (#4887) --- diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts index b2ba946b20..e9283d1b77 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts @@ -26,6 +26,7 @@ import { providerText, testAllResultsText, evaluateTestAllEntry, + shouldSwitchToVisibleFilter, type ProviderMessageTranslator, type CompatByProtocolMap, } from "../providerPageHelpers"; @@ -391,6 +392,14 @@ export function useModelVisibilityHandlers({ notify.info(testAllResultsText(t, ok, ok + error)); if (hiddenCount > 0) { notify.info(providerText(t, "testAllFailedHidden", "{count} hidden", { count: hiddenCount })); + // Bug #4887: switch to "visible" so the models we just auto-hid disappear + // on-screen — parity with PassthroughModelsSection (#3610). Without this, + // failed models were hidden in the DB but stayed visible under the "All" + // filter, so on GLM (and other OAuth providers using this hook's handleTestAll) + // it looked like nothing was hidden. + if (shouldSwitchToVisibleFilter({ autoHideFailed, hiddenCount })) { + setVisibilityFilter("visible"); + } } setTestingAll(false); setTestProgress(null); diff --git a/tests/unit/ui/use-model-visibility-handlers-test-all.test.tsx b/tests/unit/ui/use-model-visibility-handlers-test-all.test.tsx new file mode 100644 index 0000000000..61ae9babb7 --- /dev/null +++ b/tests/unit/ui/use-model-visibility-handlers-test-all.test.tsx @@ -0,0 +1,142 @@ +// @vitest-environment jsdom +// +// Regression for #4887: on OAuth providers (e.g. GLM Coding), "Test all models" +// with auto-hide-failed enabled hid failed models in the DB but they STAYED on +// screen, so it looked like nothing happened. The passthrough path (#3610, +// PassthroughModelsSection) already switches the filter to "visible" after a run +// that hid models, via shouldSwitchToVisibleFilter(...) → setVisibilityFilter. +// The OAuth path (useModelVisibilityHandlers.handleTestAll — consumed by +// ProviderModelsSection, which GLM renders) DID NOT, so just-hidden models stayed +// visible under the "All" filter. +// +// This drives the real hook: a failing model + autoHideFailed on must leave the +// hook's visibilityFilter === "visible" so the just-hidden model disappears. +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mockFetch = vi.fn(); +vi.stubGlobal("fetch", mockFetch); + +const { + useModelVisibilityHandlers, +} = await import( + "../../../src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers" +); + +type Hook = ReturnType; + +const t = ((key: string) => key) as any; +const notify = { error: vi.fn(), success: vi.fn(), info: vi.fn() } as any; + +const baseParams = { + providerId: "glm", + modelAliases: {}, + customMap: new Map(), + providerStorageAlias: "glm", + fetchProviderModelMeta: async () => {}, + fetchAliases: async () => {}, + notify, + t, + selectedConnection: { id: "conn-1", provider: "glm" }, + providerNode: { id: "glm" }, +} as any; + +let container: HTMLDivElement | null = null; +let root: ReturnType | null = null; +let captured: Hook | null = null; + +function TestComponent({ onHook }: { onHook: (h: Hook) => void }) { + const hook = useModelVisibilityHandlers(baseParams); + const { useEffect } = React; + useEffect(() => { + onHook(hook); + }); + return null; +} + +async function renderHook() { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement("div"); + document.body.appendChild(container); + captured = null; + await act(async () => { + root = createRoot(container!); + root.render( (captured = h)} />); + }); +} + +/** Mock fetch: test-all returns the configured per-model status; the hide PATCH succeeds. */ +function setFetch(perModel: Record) { + mockFetch.mockImplementation(async (url: string, init?: RequestInit) => { + if (typeof url === "string" && url.startsWith("/api/models/test-all")) { + const body = JSON.parse((init?.body as string) || "{}"); + const full = body.modelIds?.[0] as string; + return { + ok: true, + json: async () => ({ results: { [full]: { status: perModel[full] ?? "error" } } }), + }; + } + return { ok: true, text: async () => "", json: async () => ({}) }; + }); +} + +describe("useModelVisibilityHandlers.handleTestAll — #4887 visible-filter parity", () => { + beforeEach(() => { + vi.clearAllMocks(); + captured = null; + }); + afterEach(() => { + if (root && container) act(() => root!.unmount()); + container?.remove(); + container = null; + root = null; + captured = null; + }); + + it("switches visibilityFilter to 'visible' when autoHideFailed is on and a model was hidden", async () => { + setFetch({ "glm/model-ok": "ok", "glm/model-bad": "error" }); + await renderHook(); + + expect(captured!.visibilityFilter).toBe("all"); + + act(() => captured!.setAutoHideFailed(true)); + expect(captured!.autoHideFailed).toBe(true); + + await act(async () => { + await captured!.handleTestAll([ + { modelId: "model-ok", fullModel: "glm/model-ok" }, + { modelId: "model-bad", fullModel: "glm/model-bad" }, + ]); + }); + + // The failed model was hidden — the filter must flip so it disappears on-screen. + expect(captured!.visibilityFilter).toBe("visible"); + }); + + it("does NOT switch the filter when autoHideFailed is off (nothing hidden)", async () => { + setFetch({ "glm/model-bad": "error" }); + await renderHook(); + expect(captured!.autoHideFailed).toBe(false); + + await act(async () => { + await captured!.handleTestAll([{ modelId: "model-bad", fullModel: "glm/model-bad" }]); + }); + + expect(captured!.visibilityFilter).toBe("all"); + }); + + it("does NOT switch the filter when all models pass (nothing hidden)", async () => { + setFetch({ "glm/model-ok": "ok" }); + await renderHook(); + + act(() => captured!.setAutoHideFailed(true)); + + await act(async () => { + await captured!.handleTestAll([{ modelId: "model-ok", fullModel: "glm/model-ok" }]); + }); + + expect(captured!.visibilityFilter).toBe("all"); + }); +});