Files
OmniRoute/tests/unit/ui/request-logger-cache-tokens.test.tsx
Diego Rodrigues de Sa e Souza f6ccd3cf9f fix(quality): green release/v3.8.50 base-reds round 2 (#9985) (#10131)
* 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>
2026-08-12 16:09:34 -03:00

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");
});
});