mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 11:22:15 +03:00
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.
262 lines
8.7 KiB
TypeScript
262 lines
8.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import React from "react";
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import RuntimePageClient from "../../../src/app/(dashboard)/dashboard/runtime/RuntimePageClient";
|
|
|
|
function jsonResponse(data: unknown) {
|
|
return {
|
|
ok: true,
|
|
json: async () => data,
|
|
} as Response;
|
|
}
|
|
|
|
function getRequestPath(input: RequestInfo | URL) {
|
|
return typeof input === "string" ? input : input instanceof URL ? input.pathname : input.url;
|
|
}
|
|
|
|
const cleanupCallbacks: Array<() => void> = [];
|
|
|
|
async function waitForText(text: string, timeoutMs = 3000) {
|
|
const startedAt = Date.now();
|
|
while (!document.body.textContent?.includes(text)) {
|
|
if (Date.now() - startedAt > timeoutMs) {
|
|
throw new Error(`Timed out waiting for text: ${text}`);
|
|
}
|
|
await act(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
});
|
|
}
|
|
}
|
|
|
|
async function renderRuntimePage() {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
const root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root.render(<RuntimePageClient />);
|
|
});
|
|
|
|
cleanupCallbacks.push(() => {
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
});
|
|
}
|
|
|
|
vi.mock("next/link", () => ({
|
|
default: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
|
<a href={String(href)} {...props}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/shared/components/Card", () => ({
|
|
default: ({ children }: { children: React.ReactNode }) => <section>{children}</section>,
|
|
}));
|
|
|
|
vi.mock("@/shared/components/ProviderIcon", () => ({
|
|
default: ({ providerId }: { providerId: string }) => (
|
|
<span data-testid={`provider-icon-${providerId}`} />
|
|
),
|
|
}));
|
|
|
|
// Real next-intl's `useTranslations()` returns a REFERENTIALLY STABLE function across
|
|
// re-renders. RuntimePageClient depends on that (`t` sits in a `useCallback`/`useEffect`
|
|
// dependency array); building `messages`/the returned function fresh inside the arrow
|
|
// below — as this mock used to — hands back a NEW closure on every call, so the effect
|
|
// sees a "new" `t` every render and never stops re-firing (a hidden infinite render
|
|
// loop that manifests as the whole test file hanging past any timeout instead of
|
|
// failing fast). Build `messages` and `t` once, outside `useTranslations`, so every
|
|
// call returns the same reference — mirrors the fix applied to the global mock in
|
|
// tests/_setup/vitestUiPolyfills.ts.
|
|
const runtimePageClientMessages: Record<string, string> = {
|
|
title: "Runtime",
|
|
description: "Realtime observability",
|
|
pause: "Pause",
|
|
resume: "Resume",
|
|
refreshNow: "Refresh now",
|
|
kpiSessions: "Sessions",
|
|
kpiCircuits: "Circuits",
|
|
kpiCooldowns: "Cooldowns",
|
|
kpiLockouts: "Lockouts",
|
|
hintStickyBound: "{count} sticky-bound",
|
|
hintRecovering: "{count} recovering",
|
|
hintAllHealthy: "all healthy",
|
|
hintOpen: "open",
|
|
hintConnsCooling: "connections cooling",
|
|
hintModelsBlocked: "models blocked",
|
|
resilienceTitle: "3-Layer Resilience",
|
|
resilienceSubtitle: "Mirrors the documented resilience model",
|
|
providersHealthy: "{percent}% providers healthy",
|
|
layer: "Layer {n}",
|
|
layer1Title: "Provider Circuit Breakers",
|
|
layer1Desc: "Stop traffic to providers failing at the upstream level",
|
|
layer2Title: "Connection Cooldowns",
|
|
layer2Desc: "Skip one bad account/key",
|
|
layer3Title: "Model Lockouts",
|
|
layer3Desc: "Per-model rate-limit locks",
|
|
badgeAffectedOf: "{affected} of {total} affected",
|
|
badgeCooling: "{count} cooling",
|
|
badgeLocked: "{count} locked",
|
|
emptyCircuits: "No circuit breakers active yet",
|
|
emptyCooldowns: "No connection cooldowns active",
|
|
emptyLockouts: "No model lockouts",
|
|
feedTitle: "Live Feed",
|
|
feedSubtitle: "Last {count} events",
|
|
feedFilterAll: "All",
|
|
feedFilterCircuits: "Circuits",
|
|
feedFilterCooldowns: "Cooldowns",
|
|
feedFilterLockouts: "Lockouts",
|
|
feedFilterSessions: "Sessions",
|
|
feedFilterQuotas: "Quotas",
|
|
feedClear: "Clear",
|
|
feedEmptyWaiting: "Waiting for events...",
|
|
feedEmptyFiltered: "No events match this filter",
|
|
sessionsTitle: "Active Sessions",
|
|
sessionsSubtitle: "Sticky-bound request fingerprints",
|
|
sessionsActive: "{count} active",
|
|
sessionsEmptyTitle: "No active sessions",
|
|
sessionsEmptyHint: "Sessions appear as requests flow through the proxy",
|
|
tblSession: "Session",
|
|
tblAge: "Age",
|
|
tblIdle: "Idle",
|
|
tblReqs: "Reqs",
|
|
tblBoundTo: "Bound to",
|
|
topApiKeys: "Top API keys",
|
|
quotaMonitorsTitle: "Quota Monitors",
|
|
quotaMonitorsSubtitle: "Live quota state per account window",
|
|
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 {
|
|
let message = runtimePageClientMessages[key] ?? key;
|
|
if (values) {
|
|
for (const [name, value] of Object.entries(values)) {
|
|
message = message.replace(`{${name}}`, String(value));
|
|
}
|
|
}
|
|
return message;
|
|
}
|
|
|
|
vi.mock("next-intl", () => ({
|
|
useTranslations: () => runtimePageClientTranslate,
|
|
}));
|
|
|
|
describe("RuntimePageClient", () => {
|
|
const fetchMock = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
(
|
|
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
fetchMock.mockReset();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
});
|
|
|
|
afterEach(() => {
|
|
while (cleanupCallbacks.length > 0) {
|
|
cleanupCallbacks.pop()?.();
|
|
}
|
|
document.body.innerHTML = "";
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("renders degraded and unknown provider breaker states without crashing", async () => {
|
|
fetchMock.mockImplementation((input: RequestInfo | URL) => {
|
|
const path = getRequestPath(input);
|
|
if (path === "/api/monitoring/health") {
|
|
return Promise.resolve(
|
|
jsonResponse({
|
|
providerBreakers: [
|
|
{ provider: "free3", state: "DEGRADED", failureCount: 7, retryAfterMs: 0 },
|
|
{ provider: "future-provider", state: "SUSPENDED", failureCount: 1, retryAfterMs: 0 },
|
|
],
|
|
lockouts: {},
|
|
sessions: { activeCount: 0, stickyBoundCount: 0, byApiKey: {}, top: [] },
|
|
quotaMonitor: { active: 0, alerting: 0, exhausted: 0, errors: 0, monitors: [] },
|
|
})
|
|
);
|
|
}
|
|
if (path === "/api/providers/client") {
|
|
return Promise.resolve(jsonResponse({ connections: [] }));
|
|
}
|
|
return Promise.resolve(jsonResponse({}));
|
|
});
|
|
|
|
await renderRuntimePage();
|
|
|
|
await waitForText("free3");
|
|
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");
|
|
});
|
|
});
|