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 <souzamiriamrodrigues790@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 03:29:09 -03:00
committed by GitHub
parent 0412b128c0
commit af32e0041c
3 changed files with 152 additions and 1 deletions

View File

@@ -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)
---

View File

@@ -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);

View File

@@ -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<typeof useModelVisibilityHandlers>;
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<typeof createRoot> | 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(<TestComponent onHook={(h) => (captured = h)} />);
});
}
/** Mock fetch: test-all returns the configured per-model status; the hide PATCH succeeds. */
function setFetch(perModel: Record<string, "ok" | "error">) {
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");
});
});