mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 21:52:21 +03:00
* fix(quality): green release/v3.8.50 base-reds round 2 — gateways/conol/deepai corruption, migrations, docs, ratchets, dashboard-typecheck Base-red fix for issue #9985 after the 2026-08-11 merge storm (99 PRs). Real defects fixed: - gateways.ts: close regolo entry (was swallowing naga-ac + chatanywhere from #9421), drop stale duplicate chatanywhere entry (#9594) - conol-web + deepai registry: correct ../shared import depth + deepai executor:default - modelSelectModalHelpers: close isProviderModelHidden (#9011) - driverFactory.test.ts: restore eaten test-closing brace (#9173) - usageTracking: remove duplicate cache_* props - modelCapability{Overrides,ResolutionSnapshot,Capabilities}: max_token -> max_output_tokens (#9199 vs #8908) + test align - videoGeneration: drop duplicate handleFalVideoGeneration import (mediaGeneration/fal canonical, #9982) - responseSanitizer: cast input_tokens_details before .cached_tokens access - EditConnectionModal: missing alibaba code fields, hoist validationPsd, providerPageHelpers Badge variant union - FreeBudgetCard: t() -> labels.noApiKey - peerRouting + cliRuntime: ProcessEnv typing - image-combo.test.ts: type any -> unknown - fal.test.ts: moved to tests/unit/services (collected path) 14 tests green - remove duplicate 143_job_registry.sql (146 canonical), KNOWN_GAPS fix Docs/ratchets (owner-authorized rebaselines, annotated): - CHANGELOG 3.8.50 living section restored + 42 i18n mirrors - MCP-SERVER.md 104->105 tools + i18n - ENVIRONMENT.md/.env.example: ADOBE_FIREFLY_CHROME_HEADED + DEBUG_CLAUDE_NONSTREAM - fabricated-docs allowlist: TELEGRAM proposal env vars - file-size: 5 grown files + proxyFetch 1207->1220 - dead-code 230->248, codeql 2->9 (drift from merged PRs, not this PR) - untrack _tasks symlink; agent-skills-sync --apply (config-codex-cli) * fix(changelog): reformat two feature fragments to the bullet convention (#9239, #9490) * fix(quality): prune stale ESLint suppressions (base-red) * fix(quality): resolve open-sse type errors + catalog/build regressions (base-red round 3) Storm-merge splices repaired in the base-fix PR #10131: - doctor.ts: AppConfig missing brokerSocketPath - conol-web.ts: Buffer not assignable to BodyInit (Uint8Array) - tinycms.ts: TinyCmsExecutor.execute return matches BaseExecutor (response/url/transformedBody) - tinycmsSigner.ts: encodeInto never-narrowing guard + dead wasm URL fallback (Turbopack) - virtualFactory.ts: options slot for resolutionSnapshot - bottleneckPatch.ts: insufficient-overlap casts (as unknown as) - imageCombo.ts: narrow handleImageGeneration union result - browser-worker.ts: AppConfig + turn.capabilities splice - conolDiscovery.ts: getProviderOutboundGuard from Policy module - catalog.ts: drop removed SWR hooks (getCatalogStaleWhileRevalidateMs + accessors), CatalogCachePolicy -> inline settings, resolve 4-arg call - catalogCache.ts: remove dead inFlight/promise refs - chat.ts: add isProviderBreakerFailureStatus import - model-catalog-cache-swr-8728.test.ts: align to #9199 new API (policy injection removed) * fix(quality): align UI test fixtures to current component contracts (base-red vitest) - setup-wizard: provide required serverState prop (component gained it in a merged PR) - grok-device-oauth-modal: next-intl stub resolves grok flow keys to EN labels - provider-quota-widget: label now inline (PR #8916 removed AutoRefreshButtonLabel extraction) — test the widget - use-provider-connections-cursor-refresh + phase1f: match /api/providers?provider=<id> query form; hoist heavy dynamic imports to module scope (timeout flake) - home-topology: mock next/navigation useRouter (component added node-click navigation) - cooling/lobe/AutoComboCatalog: raise cold-import describe timeouts to 30-60s - request-logger-*: align to current detail-view contract * fix(search): guard params.token undefined in serper headers (typecheck base-red) * fix(search): guard token headers + non-null providerConfig (typecheck base-red) * fix(changelog): restore base CHANGELOGs eaten by merge auto-resolve (43 files) --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: backryun <bakryun0718@proton.me>
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import React, { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("next-intl", () => {
|
|
const cacheLabels: Record<string, string> = {
|
|
cachedTokensCol: "Cache Read",
|
|
cacheCreation: "Cache Write",
|
|
};
|
|
const detailLabels: Record<string, string> = {
|
|
totalIn: "Total In: {value}",
|
|
cacheRead: "Cache Read: {value}",
|
|
cacheWrite: "Cache Write: {value}",
|
|
compressed: "Compressed: {percent}%",
|
|
totalOut: "Total Out: {value}",
|
|
reasoning: "Reasoning: {value}",
|
|
notAvailable: "N/A",
|
|
};
|
|
const interpolate = (template: string, params: Record<string, string> = {}) =>
|
|
template.replace(/\{(\w+)\}/g, (_, k) => (k in params ? String(params[k]) : `{${k}}`));
|
|
return {
|
|
useLocale: () => "en",
|
|
useTranslations: (namespace?: string) => (key: string, params?: Record<string, string>) =>
|
|
namespace === "cache"
|
|
? interpolate(cacheLabels[key] ?? key, params)
|
|
: namespace === "requestLogger.detail"
|
|
? interpolate(detailLabels[key] ?? key, params)
|
|
: interpolate(key, params),
|
|
};
|
|
});
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), refresh: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock("@/store/emailPrivacyStore", () => ({
|
|
default: () => ({ emailsVisible: true }),
|
|
}));
|
|
|
|
const RequestLoggerV2 = (await import("@/shared/components/RequestLoggerV2")).default;
|
|
const RequestLoggerDetail = (await import("@/shared/components/RequestLoggerDetail")).default;
|
|
|
|
let container: HTMLElement;
|
|
let root: Root;
|
|
|
|
const populatedLog = {
|
|
id: "log-cache",
|
|
status: 200,
|
|
method: "POST",
|
|
path: "/v1/chat/completions",
|
|
model: "gpt-cache",
|
|
provider: "openai",
|
|
timestamp: "2026-08-10T12:00:00.000Z",
|
|
duration: 1_000,
|
|
tokens: {
|
|
in: 1_000,
|
|
out: 250,
|
|
cacheRead: 800,
|
|
cacheWrite: 120,
|
|
reasoning: 50,
|
|
compressed: 20,
|
|
},
|
|
};
|
|
|
|
const noop = () => {};
|
|
|
|
async function render(component: React.ReactNode) {
|
|
await act(async () => {
|
|
root.render(component);
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await act(async () => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("request log cache token metrics (#9620)", () => {
|
|
it("renders cache read/write beside the existing row token metrics", async () => {
|
|
const emptyCacheLog = {
|
|
...populatedLog,
|
|
id: "log-no-cache",
|
|
model: "gpt-no-cache",
|
|
timestamp: "2026-08-10T11:59:00.000Z",
|
|
tokens: { ...populatedLog.tokens, cacheRead: null, cacheWrite: 0 },
|
|
};
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
if (url.startsWith("/api/usage/call-logs")) {
|
|
return Response.json([populatedLog, emptyCacheLog]);
|
|
}
|
|
if (url.startsWith("/api/provider-nodes")) return Response.json({ nodes: [] });
|
|
if (url.startsWith("/api/logs/detail")) return Response.json({ enabled: false });
|
|
return Response.json({});
|
|
})
|
|
);
|
|
|
|
await render(<RequestLoggerV2 />);
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const row = Array.from(container.querySelectorAll("tbody tr")).find((candidate) =>
|
|
candidate.textContent?.includes("gpt-cache")
|
|
);
|
|
expect(row?.textContent).toContain("TI: 1,000");
|
|
expect(row?.textContent).toContain("TO: 250");
|
|
expect(row?.textContent).toContain("CR: 800");
|
|
expect(row?.textContent).toContain("CW: 120");
|
|
expect(row?.textContent).toContain("↓20");
|
|
|
|
const emptyRow = Array.from(container.querySelectorAll("tbody tr")).find((candidate) =>
|
|
candidate.textContent?.includes("gpt-no-cache")
|
|
);
|
|
expect(emptyRow?.textContent).toContain("TI: 1,000");
|
|
expect(emptyRow?.textContent).toContain("TO: 250");
|
|
expect(emptyRow?.textContent).not.toContain("CR:");
|
|
expect(emptyRow?.textContent).not.toContain("CW:");
|
|
});
|
|
|
|
it("distinguishes cache read from cache write in the detail view", async () => {
|
|
await render(
|
|
<RequestLoggerDetail
|
|
log={populatedLog}
|
|
detail={populatedLog}
|
|
loading={false}
|
|
debugEnabled={false}
|
|
onClose={noop}
|
|
onCopy={async () => true}
|
|
/>
|
|
);
|
|
|
|
const inputGroup = container.querySelector('[data-testid="token-group-input"]');
|
|
const outputGroup = container.querySelector('[data-testid="token-group-output"]');
|
|
expect(inputGroup?.textContent).toContain("Total In: 1,000");
|
|
expect(inputGroup?.textContent).toContain("Cache Read: 800");
|
|
expect(inputGroup?.textContent).toContain("Cache Write: 120");
|
|
expect(inputGroup?.textContent).toContain("Compressed:");
|
|
expect(outputGroup?.textContent).toContain("Total Out: 250");
|
|
expect(outputGroup?.textContent).toContain("Reasoning: 50");
|
|
});
|
|
|
|
it("handles historical null and zero cache values without inventing usage", async () => {
|
|
const emptyCacheLog = {
|
|
...populatedLog,
|
|
id: "log-no-cache",
|
|
tokens: { ...populatedLog.tokens, cacheRead: null, cacheWrite: 0 },
|
|
};
|
|
|
|
await render(
|
|
<RequestLoggerDetail
|
|
log={emptyCacheLog}
|
|
detail={emptyCacheLog}
|
|
loading={false}
|
|
debugEnabled={false}
|
|
onClose={noop}
|
|
onCopy={async () => true}
|
|
/>
|
|
);
|
|
|
|
const inputGroup = container.querySelector('[data-testid="token-group-input"]');
|
|
expect(inputGroup?.textContent).toContain("Cache Read: N/A");
|
|
expect(inputGroup?.textContent).toContain("Cache Write: 0");
|
|
expect(inputGroup?.textContent).toContain("Total In: 1,000");
|
|
});
|
|
});
|