fix(dashboard): pass nodeMap into Runtime QuotaGroup (#12868)

Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

O `nodeMap` lido do closure de `RuntimePageClient` por uma função de nível de módulo é uma bomba-relógio silenciosa: só explode quando um monitor entra em error/exhausted/alerting, e o teste existente só alimentava listas vazias. Tirar o arquivo do exclude do vitest vale tanto quanto o fix — confirmei aqui que `tests/unit/ui/runtime-page-client.test.tsx` agora roda na `test:vitest:ui` e passa.

A anotação sobre o "内部服务器错误" ser o catálogo RSC da página, e não o crash, poupou o próximo a caçar fantasma.
This commit is contained in:
Bob.Hou
2026-09-07 07:56:49 -04:00
committed by GitHub
parent 25bc16d87e
commit d4d2e68a1f
5 changed files with 89 additions and 7 deletions

View File

@@ -0,0 +1 @@
- **fix(dashboard):** pass `nodeMap` into Runtime `QuotaGroup` so a quota monitor in error/exhausted/alerting no longer throws `ReferenceError: nodeMap is not defined`. ([#12868](https://github.com/diegosouzapw/OmniRoute/pull/12868))

View File

@@ -1,4 +1,5 @@
{
"_rebaseline_2026_09_06_runtime_quotagroup_nodemap": "Own growth: src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx 1201->1222 (+21, check-file-size split-newline). QuotaGroup is a module-level sibling and was reading nodeMap from RuntimePageClient's closure; that identifier is not in scope, so a quota monitor with status error/exhausted/alerting throws ReferenceError. Fix threads nodeMap as a prop (3 call sites + parameter + ProviderNodeEntry import). Prettier wraps the long import and the three QuotaGroup JSX tags. Covered by tests/unit/ui/runtime-page-client.test.tsx (empty monitors stay green; error+exhausted fixtures mount QuotaGroup).",
"_rebaseline_2026_09_03_reset_aware_model_family": "Own growth: open-sse/services/combo.ts 4036->4041 (+5). buildAutoCandidates now keys the reset-aware quota cache by getQuotaFetchScope and spreads requestedModel onto the connection so Gemini windows stay off a Claude-empty Antigravity account. Irreducible wiring at the existing fetchResetAwareQuotaWithCache call site; the family helper itself lives in antigravityQuotaFamily.ts. Covered by tests/unit/reset-aware-request-scope-12600.test.ts.",
"_rebaseline_2026_09_03_overloaded_not_provider_breaker": "fix/overloaded-not-provider-breaker own growth: open-sse/services/combo.ts 4036->4075 (check-file-size split-newline, +39). Circuit-open pre-skip now records the breaker retryAfter and, when every target was skipped that way, waits the short reset via resolveCircuitOpenWaitDecision (new leaf in comboCooldownRetry.ts) instead of crystallizing ALL_TARGETS_SKIPPED in ~43ms. skippedForCircuitOpen / earliestCircuitOpenRetryMs reset each setTry so a later iteration cannot inherit a stale retryAfter. Irreducible at the existing ALL_TARGETS_SKIPPED chokepoint (same pattern as #7301/#8213 cooldown-wait). Predicate itself lives in circuitBreaker.ts / comboPredicates.ts / chatPredicates.ts, all under cap. Covered by tests/unit/overloaded-not-provider-breaker.test.ts + combo-cooldown-retry.test.ts.",
"_rebaseline_2026_09_03_12649_free_tier_reaudit_gateways": "PR #12649 (fix/free-tier-quota-reaudit) own growth: src/shared/constants/providers/apikey/gateways.ts 1459->1462 (+3 = the nara authHint rewritten for the re-audited 7M/day plan now wraps to two lines, plus the Prettier reflow of two pre-existing >100-col authHint lines (oneminai, freebuff) that lint-staged enforces on any touch of the file; additive text at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines: #11786 seekai, #10987 logfare, #10531 freebuff). Covered by tests/unit/free-tier-reaudit-2026-09.test.ts and tests/unit/free-providers-batch-2026-07.test.ts.",
@@ -439,7 +440,7 @@
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2491,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1631,
"src/app/(dashboard)/dashboard/providers/page.tsx": 2025,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1222,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1475,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1271,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1607,
@@ -575,7 +576,7 @@
"src/app/(dashboard)/dashboard/health/page.tsx": "1165",
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": "1324",
"src/app/(dashboard)/dashboard/providers/page.tsx": "1944",
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": "1201",
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": "1222",
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": "1019",
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": "1470",
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": "1123",

View File

@@ -6,7 +6,11 @@ import Link from "next/link";
import Card from "@/shared/components/Card";
import ProviderIcon from "@/shared/components/ProviderIcon";
import ModelCooldownsCard from "./components/ModelCooldownsCard";
import { useProviderNodeMap, resolveProviderName } from "@/lib/display/useProviderNodeMap";
import {
useProviderNodeMap,
resolveProviderName,
type ProviderNodeEntry,
} from "@/lib/display/useProviderNodeMap";
type KnownBreakerState = "CLOSED" | "OPEN" | "HALF_OPEN" | "DEGRADED";
type BreakerState = KnownBreakerState | (string & {});
@@ -1002,13 +1006,28 @@ export default function RuntimePageClient() {
return (
<div className="mt-2 flex flex-col gap-3">
{exhausted.length > 0 && (
<QuotaGroup tone="red" label={t("statusExhausted")} items={exhausted} />
<QuotaGroup
tone="red"
label={t("statusExhausted")}
items={exhausted}
nodeMap={nodeMap}
/>
)}
{alerting.length > 0 && (
<QuotaGroup tone="amber" label={t("statusAlerting")} items={alerting} />
<QuotaGroup
tone="amber"
label={t("statusAlerting")}
items={alerting}
nodeMap={nodeMap}
/>
)}
{errors.length > 0 && (
<QuotaGroup tone="orange" label={t("statusError")} items={errors} />
<QuotaGroup
tone="orange"
label={t("statusError")}
items={errors}
nodeMap={nodeMap}
/>
)}
</div>
);
@@ -1148,10 +1167,12 @@ function QuotaGroup({
tone,
label,
items,
nodeMap,
}: {
tone: "red" | "amber" | "orange";
label: string;
items: QuotaMonitor[];
nodeMap: Map<string, ProviderNodeEntry>;
}) {
const t = useTranslations("runtime");
const toneMap = {

View File

@@ -133,6 +133,9 @@ const runtimePageClientMessages: Record<string, string> = {
openQuota: "Open Quota",
allQuotasHealthy: "All quotas healthy",
moreSuffix: "+{count} more",
statusExhausted: "EXHAUSTED",
statusAlerting: "ALERTING",
statusError: "ERROR",
};
function runtimePageClientTranslate(key: string, values?: Record<string, unknown>): string {
@@ -196,6 +199,63 @@ describe("RuntimePageClient", () => {
await waitForText("future-provider");
await waitForText("DEG");
await waitForText("UNK");
await waitForText("All quotas healthy");
expect(document.body.textContent).not.toContain("Internal Server Error");
});
it("renders error and exhausted quota monitors without a nodeMap ReferenceError", async () => {
fetchMock.mockImplementation((input: RequestInfo | URL) => {
const path = getRequestPath(input);
if (path === "/api/monitoring/health") {
return Promise.resolve(
jsonResponse({
providerBreakers: [],
lockouts: {},
sessions: { activeCount: 0, stickyBoundCount: 0, byApiKey: {}, top: [] },
quotaMonitor: {
active: 2,
alerting: 0,
exhausted: 1,
errors: 1,
monitors: [
{
accountId: "acct-error",
provider: "agy",
window: "5h",
status: "error",
remainingPercent: 3,
},
{
accountId: "acct-exhausted",
provider: "volcengine-coding-plan",
window: "5h",
status: "exhausted",
remainingPercent: 0,
},
],
},
})
);
}
if (path === "/api/providers/client") {
return Promise.resolve(jsonResponse({ connections: [] }));
}
if (path === "/api/provider-nodes") {
return Promise.resolve(jsonResponse({ nodes: [] }));
}
return Promise.resolve(jsonResponse({}));
});
await renderRuntimePage();
await waitForText("Runtime");
await waitForText("acct-error");
await waitForText("acct-exhausted");
await waitForText("ERROR");
await waitForText("EXHAUSTED");
expect(document.body.textContent).toContain("agy");
expect(document.body.textContent).toContain("volcengine-coding-plan");
expect(document.body.textContent).not.toContain("Internal Server Error");
expect(document.body.textContent).not.toContain("All quotas healthy");
});
});

View File

@@ -58,7 +58,6 @@ export default defineConfig({
"tests/unit/ui/livePage.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
"tests/unit/ui/diffPane.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
"tests/unit/ui/CliCodePage.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
"tests/unit/ui/runtime-page-client.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
"tests/unit/ui/engineConfigPage.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
"open-sse/services/autoCombo/__tests__/autoCombo.test.ts", // #8618 — pre-existing failure; remove this exclusion when fixed
"tests/unit/ui/agent-card-risk-modal.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed