Refactor router config handling

This commit is contained in:
musistudio 2026-06-30 10:38:33 +08:00
parent 20691a8215
commit 1a8d7cb610
17 changed files with 754 additions and 53 deletions

View file

@ -8,26 +8,60 @@ import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const testHome = mkdtempSync(path.join(os.tmpdir(), "ccr-test-home-"));
const testSuites = ["main", "renderer"];
const requestedSuites = process.argv.slice(2);
const suites = requestedSuites.length === 0 ? testSuites : requestedSuites;
const child = spawn(electron, ["--test", "dist/tests/*.js"], {
cwd: projectRoot,
env: {
...process.env,
CCR_INTERNAL_APP_DATA_DIR: path.join(testHome, "app-data"),
CCR_INTERNAL_HOME_DIR: testHome,
CCR_INTERNAL_USER_DATA_DIR: path.join(testHome, "user-data"),
HOME: testHome,
ELECTRON_RUN_AS_NODE: "1"
},
shell: process.platform === "win32",
stdio: "inherit"
});
child.on("exit", (code, signal) => {
rmSync(testHome, { force: true, recursive: true });
if (signal) {
process.kill(process.pid, signal);
return;
for (const suite of suites) {
if (!testSuites.includes(suite)) {
cleanup();
throw new Error(`Unknown test suite: ${suite}`);
}
process.exit(code ?? 1);
});
}
try {
for (const suite of suites) {
await runSuite(suite);
}
} finally {
cleanup();
}
function runSuite(suite) {
console.log(`\nRunning ${suite} tests...`);
return new Promise((resolve, reject) => {
const child = spawn(electron, ["--test", `dist/tests/${suite}/*.js`], {
cwd: projectRoot,
env: {
...process.env,
CCR_INTERNAL_APP_DATA_DIR: path.join(testHome, "app-data"),
CCR_INTERNAL_HOME_DIR: testHome,
CCR_INTERNAL_USER_DATA_DIR: path.join(testHome, "user-data"),
HOME: testHome,
ELECTRON_RUN_AS_NODE: "1"
},
shell: process.platform === "win32",
stdio: "inherit"
});
child.on("error", reject);
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
if (code === 0) {
resolve();
return;
}
reject(new Error(`${suite} tests exited with code ${code ?? 1}`));
});
});
}
function cleanup() {
rmSync(testHome, { force: true, recursive: true });
}

View file

@ -1,44 +1,112 @@
import esbuild from "esbuild";
import { mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const outdir = path.join(projectRoot, "dist", "tests");
const testsOutDir = path.join(projectRoot, "dist", "tests");
const rendererRoot = path.join(projectRoot, "src", "renderer");
const testSuites = [
{ name: "main", testDir: path.join(projectRoot, "tests", "main") },
{ name: "renderer", testDir: path.join(projectRoot, "tests", "renderer") }
];
const requestedSuites = new Set(process.argv.slice(2));
const suiteNames = new Set(testSuites.map((suite) => suite.name));
const unknownSuites = [...requestedSuites].filter((suite) => !suiteNames.has(suite));
const selectedSuites = requestedSuites.size === 0
? testSuites
: testSuites.filter((suite) => requestedSuites.has(suite.name));
rmSync(outdir, { force: true, recursive: true });
mkdirSync(outdir, { recursive: true });
if (unknownSuites.length > 0) {
throw new Error(`Unknown test suite: ${unknownSuites.join(", ")}`);
}
const entryPoints = findTestFiles(path.join(projectRoot, "tests"));
rmSync(testsOutDir, { force: true, recursive: true });
await esbuild.build({
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints,
external: [
"better-sqlite3",
"electron"
],
format: "cjs",
legalComments: "none",
logLevel: "info",
outdir,
platform: "node",
target: "node22"
});
for (const suite of selectedSuites) {
const entryPoints = findTestFiles(suite.testDir);
if (entryPoints.length === 0) {
continue;
}
await esbuild.build({
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints,
external: [
"better-sqlite3",
"electron"
],
format: "cjs",
legalComments: "none",
loader: {
".gif": "file",
".ico": "file",
".jpg": "file",
".jpeg": "file",
".png": "file",
".svg": "file",
".webp": "file"
},
logLevel: "info",
outdir: path.join(testsOutDir, suite.name),
platform: "node",
plugins: [rendererAliasPlugin()],
target: "node22"
});
}
function findTestFiles(dir) {
if (!existsSync(dir)) {
return [];
}
const files = [];
for (const name of readdirSync(dir)) {
const file = path.join(dir, name);
const stat = statSync(file);
if (stat.isDirectory()) {
files.push(...findTestFiles(file));
} else if (name.endsWith(".test.mjs")) {
} else if (/\.(test|spec)\.(mjs|ts|tsx)$/.test(name)) {
files.push(file);
}
}
return files.sort();
}
function rendererAliasPlugin() {
return {
name: "renderer-test-alias",
setup(build) {
build.onResolve({ filter: /^@\// }, (args) => {
return { path: resolveRendererImport(args.path.slice(2)) };
});
}
};
}
function resolveRendererImport(importPath) {
const basePath = path.resolve(rendererRoot, importPath);
const candidates = [
basePath,
`${basePath}.tsx`,
`${basePath}.ts`,
`${basePath}.jsx`,
`${basePath}.js`,
`${basePath}.json`,
path.join(basePath, "index.tsx"),
path.join(basePath, "index.ts"),
path.join(basePath, "index.jsx"),
path.join(basePath, "index.js")
];
for (const candidate of candidates) {
if (existsSync(candidate) && statSync(candidate).isFile()) {
return candidate;
}
}
return basePath;
}

View file

@ -46,6 +46,8 @@
"prepublishOnly": "npm run typecheck",
"preview": "npm run build:assets && electron .",
"test": "node build/test.mjs && node build/run-tests.mjs",
"test:main": "node build/test.mjs main && node build/run-tests.mjs main",
"test:renderer": "node build/test.mjs renderer && node build/run-tests.mjs renderer",
"typecheck": "tsc --noEmit",
"rebuild:sqlite3": "electron-rebuild -f -w better-sqlite3"
},

View file

@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { botGatewayProfileEnv } from "../src/main/bot-gateway-env.ts";
import { botGatewayProfileEnv } from "../../src/main/bot-gateway-env.ts";
function botGateway(overrides = {}) {
return {

View file

@ -6,7 +6,7 @@ import {
parseProviderDeepLinkPayload,
parseProviderManifestDeepLinkPayload,
parseProviderManifestPayload
} from "../src/shared/deep-link.ts";
} from "../../src/shared/deep-link.ts";
function base64UrlJson(value) {
return Buffer.from(JSON.stringify(value), "utf8")

View file

@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { loadModelCatalogPayload, modelCatalogPathCandidates } from "../src/main/model-catalog-file.ts";
import { loadModelCatalogPayload, modelCatalogPathCandidates } from "../../src/main/model-catalog-file.ts";
test("modelCatalogPathCandidates prefers env paths and removes duplicates", () => {
const previousCatalogPath = process.env.CCR_MODEL_CATALOG_PATH;

View file

@ -10,7 +10,7 @@ import {
resolveClaudeCodeSettingsFile,
resolveCodexConfigFile,
resolveProfileOpenSurface
} from "../src/main/profile-launch-core.ts";
} from "../../src/main/profile-launch-core.ts";
const claudeProfile = {
agent: "claude-code",

View file

@ -7,7 +7,7 @@ import {
providerEndpointCanReceiveProviderApiKeyInList,
providerIdentitySafetyIssueInList,
providerPresetMatchesBaseUrl
} from "../src/shared/provider-preset-utils.ts";
} from "../../src/shared/provider-preset-utils.ts";
const openAiPreset = {
aliases: ["OpenAI", "ChatGPT"],

View file

@ -5,7 +5,7 @@ import {
parseProviderBaseUrl,
providerBaseUrlForProtocol,
providerUrlWithDefaultScheme
} from "../src/shared/provider-url.ts";
} from "../../src/shared/provider-url.ts";
test("provider URL parsing strips endpoint paths and unsafe URL parts", () => {
const parsed = parseProviderBaseUrl("https://user:secret@api.example.com/v1/chat/completions?token=secret#section");

View file

@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { RequestLogStore } from "../src/main/request-log-store.ts";
import { RequestLogStore } from "../../src/main/request-log-store.ts";
test("RequestLogStore keeps list rows lightweight and detail rows complete", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-test-"));

View file

@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { normalizeUsageInputTokens } from "../src/main/usage-normalization.ts";
import { normalizeUsageInputTokens } from "../../src/main/usage-normalization.ts";
test("normalizeUsageInputTokens subtracts cache tokens for OpenAI-compatible protocols", () => {
const usage = normalizeUsageInputTokens(

View file

@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { UsageStore } from "../src/main/usage-store.ts";
import { UsageStore } from "../../src/main/usage-store.ts";
test("UsageStore aggregates stats in SQLite without loading all events", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-test-"));

View file

@ -0,0 +1,79 @@
import assert from "node:assert/strict";
import test from "node:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { Badge } from "../../src/renderer/components/ui/badge.tsx";
import { Button } from "../../src/renderer/components/ui/button.tsx";
import { Card, CardContent, CardHeader, CardTitle } from "../../src/renderer/components/ui/card.tsx";
import { Switch } from "../../src/renderer/components/ui/switch.tsx";
test("Button renders default button semantics and variant classes", () => {
const html = renderToStaticMarkup(
<Button className="extra-action" size="iconSm" variant="outline">
Run
</Button>
);
assert.match(html, /^<button /);
assert.match(html, /type="button"/);
assert.match(html, /border-input/);
assert.match(html, /h-7/);
assert.match(html, /w-7/);
assert.match(html, /extra-action/);
assert.match(html, />Run<\/button>$/);
});
test("Button unstyled mode keeps caller supplied styling only", () => {
const html = renderToStaticMarkup(
<Button className="plain-button" unstyled>
Plain
</Button>
);
assert.match(html, /class="plain-button"/);
assert.match(html, /type="button"/);
assert.doesNotMatch(html, /inline-flex/);
});
test("Badge renders the selected visual variant", () => {
const html = renderToStaticMarkup(
<Badge className="status-badge" variant="warning">
Delayed
</Badge>
);
assert.match(html, /^<span /);
assert.match(html, /text-amber-700/);
assert.match(html, /bg-amber-50/);
assert.match(html, /status-badge/);
assert.match(html, />Delayed<\/span>$/);
});
test("Card primitives compose the expected document structure", () => {
const html = renderToStaticMarkup(
<Card className="settings-card">
<CardHeader>
<CardTitle>Provider settings</CardTitle>
</CardHeader>
<CardContent>Ready</CardContent>
</Card>
);
assert.match(html, /^<div /);
assert.match(html, /settings-card/);
assert.match(html, /<h2 class="[^"]*text-\[13px\][^"]*">Provider settings<\/h2>/);
assert.match(html, /<div class="p-4">Ready<\/div>/);
});
test("Switch renders accessible checked and disabled state", () => {
const html = renderToStaticMarkup(
<Switch aria-label="Enable provider" checked className="provider-switch" disabled />
);
assert.match(html, /provider-switch/);
assert.match(html, /role="switch"/);
assert.match(html, /aria-checked="true"/);
assert.match(html, /aria-label="Enable provider"/);
assert.match(html, /disabled=""/);
assert.match(html, /translate-x-\[24px\]/);
});

189
tests/renderer/fixtures.ts Normal file
View file

@ -0,0 +1,189 @@
import type {
ProviderAccountSnapshot,
UsageComparisonRow,
UsageStatsRange,
UsageStatsSnapshot,
UsageTotals
} from "../../src/shared/app.ts";
export function installBrowserGlobals() {
const localStorage = {
getItem: () => null,
removeItem: () => undefined,
setItem: () => undefined
};
const windowMock = {
addEventListener: () => undefined,
cancelAnimationFrame: () => undefined,
ccr: {
closeTray: () => undefined,
quitApp: () => undefined,
setTrayDetailOpen: () => undefined,
showMainWindow: () => undefined
},
clearInterval: () => undefined,
localStorage,
matchMedia: () => ({
addEventListener: () => undefined,
matches: false,
removeEventListener: () => undefined
}),
removeEventListener: () => undefined,
requestAnimationFrame: () => 0,
setInterval: () => 0
};
Object.defineProperty(globalThis, "window", {
configurable: true,
value: windowMock
});
Object.defineProperty(globalThis, "document", {
configurable: true,
value: {
body: {
classList: {
add: () => undefined,
remove: () => undefined
},
style: {}
},
documentElement: {
lang: "en"
}
}
});
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: {
language: "en-US",
languages: ["en-US"]
}
});
}
export function usageTotals(patch: Partial<UsageTotals> = {}): UsageTotals {
return {
avgDurationMs: 842,
cacheRatio: 0.24,
cacheTokens: 2400,
costUsd: 1.23,
errorCount: 2,
inputTokens: 6200,
outputTokens: 3400,
requestCount: 128,
successRate: 0.984,
totalTokens: 12000,
...patch
};
}
export function usageRow(key: string, label: string, patch: Partial<UsageComparisonRow> = {}): UsageComparisonRow {
return {
...usageTotals(),
caption: "Test usage row",
key,
label,
maxShare: 0.5,
...patch
};
}
export function usageStats(range: UsageStatsRange = "30d", patch: Partial<UsageStatsSnapshot> = {}): UsageStatsSnapshot {
const series = Array.from({ length: 8 }, (_, index) => {
const totalTokens = 1000 + index * 250;
const requestCount = 8 + index;
return {
...usageTotals({
cacheRatio: 0.1 + index * 0.02,
cacheTokens: 120 + index * 30,
errorCount: index % 4 === 0 ? 1 : 0,
inputTokens: 560 + index * 120,
outputTokens: 320 + index * 80,
requestCount,
successRate: index % 4 === 0 ? 0.92 : 1,
totalTokens
}),
bucket: `2026-06-${String(20 + index).padStart(2, "0")}T00:00:00.000Z`,
label: `6/${20 + index}`
};
});
const models = [
usageRow("model:gpt-4.1", "gpt-4.1", { maxShare: 0.62, model: "gpt-4.1", provider: "openai", totalTokens: 7200 }),
usageRow("model:claude-sonnet", "claude-sonnet", { maxShare: 0.38, model: "claude-sonnet", provider: "anthropic", totalTokens: 4800 })
];
return {
clientModels: [
usageRow("client:claude-code", "Claude Code", { client: "claude-code", model: "gpt-4.1", provider: "openai", totalTokens: 6800 }),
usageRow("client:codex", "Codex", { client: "codex", model: "claude-sonnet", provider: "anthropic", totalTokens: 5200 })
],
generatedAt: "2026-06-30T00:00:00.000Z",
models,
providerModels: [
usageRow("provider:openai", "OpenAI", { credentialId: "primary", model: "gpt-4.1", provider: "openai", totalTokens: 7200 }),
usageRow("provider:anthropic", "Anthropic", { credentialId: "secondary", model: "claude-sonnet", provider: "anthropic", totalTokens: 4800 })
],
range,
recentRequests: [],
series,
totals: usageTotals(),
...patch
};
}
export function accountSnapshots(): ProviderAccountSnapshot[] {
return [
{
credentialId: "primary",
credentialLabel: "Primary Key",
meters: [
{
id: "5h",
kind: "quota",
label: "5h quota",
limit: 100,
remaining: 42,
unit: "requests",
window: "5h"
},
{
id: "weekly",
kind: "quota",
label: "Weekly quota",
limit: 500,
remaining: 310,
unit: "requests",
window: "weekly"
},
{
id: "balance",
kind: "balance",
label: "Cash balance",
remaining: 12.34,
unit: "USD"
}
],
provider: "openai",
source: "standard",
status: "warning",
updatedAt: "2026-06-30T00:00:00.000Z"
},
{
credentialId: "secondary",
credentialLabel: "Secondary Key",
meters: [
{
id: "balance",
kind: "balance",
label: "Credit balance",
remaining: 88,
unit: "USD"
}
],
provider: "anthropic",
source: "standard",
status: "ok",
updatedAt: "2026-06-30T00:00:00.000Z"
}
];
}

View file

@ -0,0 +1,71 @@
import assert from "node:assert/strict";
import test from "node:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { OverviewView } from "../../src/renderer/pages/home/components/dashboard.tsx";
import type { OverviewWidgetConfig } from "../../src/shared/app.ts";
import { accountSnapshots, installBrowserGlobals, usageStats } from "./fixtures.ts";
installBrowserGlobals();
test("OverviewView renders every overview widget type", () => {
const widgets: OverviewWidgetConfig[] = [
{ enabled: true, id: "status", size: "4:1", type: "system-status", variant: "timeline" },
{ enabled: true, id: "account", size: "4:2", type: "account-balance", variant: "nested-rings" },
{ enabled: true, id: "metric-requests", metric: "requests", size: "1:1", type: "metric", variant: "card" },
{ enabled: true, id: "metric-cache", metric: "cache-ratio", size: "1:1", type: "metric", variant: "ring" },
{ enabled: true, id: "metric-errors", metric: "errors", size: "1:1", type: "metric", variant: "bar" },
{ enabled: true, id: "trend", size: "3:2", type: "usage-trend", variant: "composed" },
{ enabled: true, id: "activity", size: "4:2", type: "token-activity", variant: "heatmap" },
{ enabled: true, id: "token-mix", size: "2:2", type: "token-mix", variant: "stacked" },
{ enabled: true, id: "models", size: "2:2", type: "model-distribution", variant: "donut" },
{ enabled: true, id: "clients", size: "4:2", type: "client-analysis", variant: "table" },
{ enabled: true, id: "providers", size: "4:2", type: "provider-analysis", variant: "table" }
];
const html = renderToStaticMarkup(
<OverviewView
overviewWidgets={widgets}
providerAccounts={accountSnapshots()}
refreshProviderAccounts={() => undefined}
setUsageRange={() => undefined}
usageRange="30d"
usageStats={usageStats("30d")}
onWidgetsChange={() => undefined}
/>
);
assert.match(html, /<h2 class="[^"]*">Overview<\/h2>/);
assert.match(html, /aria-label="Edit widgets"/);
assert.match(html, /System status/);
assert.match(html, /API Service/);
assert.match(html, /openai \/ Primary Key/);
assert.match(html, /Requests/);
assert.match(html, /Cache ratio/);
assert.match(html, /Errors/);
assert.match(html, /Usage Trend/);
assert.match(html, /Activity/);
assert.match(html, /Token Mix/);
assert.match(html, /Model Distribution/);
assert.match(html, /Client Analysis/);
assert.match(html, /Provider Analysis/);
assert.match(html, /claude-code/);
assert.match(html, /openai/);
});
test("OverviewView renders the empty widget layout state", () => {
const html = renderToStaticMarkup(
<OverviewView
overviewWidgets={[]}
providerAccounts={[]}
setUsageRange={() => undefined}
usageRange="7d"
usageStats={usageStats("7d")}
onWidgetsChange={() => undefined}
/>
);
assert.match(html, /Overview/);
assert.match(html, /No widgets configured/);
assert.match(html, /aria-label="Edit widgets"/);
});

View file

@ -0,0 +1,258 @@
import assert from "node:assert/strict";
import test from "node:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import {
AccountSummaryPanel,
AnimatedUsageChart,
ChartShell,
ModelShareChart,
RadialMetric,
RangeSwitch,
RingMetrics,
SourceGrid,
StatsGrid,
TokenActivityPanel,
TokenMixPanel,
TrayStatusStrip,
UsageDetailPanel,
UsageOverviewPanel
} from "../../src/renderer/pages/tray/components/index.ts";
import { TrayApp } from "../../src/renderer/pages/tray/TrayApp.tsx";
import { TrayDetailApp } from "../../src/renderer/pages/tray/TrayDetailApp.tsx";
import { accountSnapshots, installBrowserGlobals, usageStats, usageTotals } from "./fixtures.ts";
installBrowserGlobals();
const componentVariants = {
account: "bar",
modelShare: "bars",
rings: "rings",
stats: "cards",
tokenFlow: "line",
tokenMix: "bars"
} as const;
test("UsageOverviewPanel renders every enabled overview tray module", () => {
const activeStats = usageStats("30d");
const html = renderToStaticMarkup(
<UsageOverviewPanel
accountRefreshing
accountSnapshots={accountSnapshots()}
activeStats={activeStats}
componentVariants={componentVariants}
loading
modules={new Set(["account", "token-flow", "activity", "stats", "token-mix", "rings", "model-share"])}
monthTotals={usageTotals({ totalTokens: 32000 })}
todayTotals={usageTotals({ requestCount: 12, totalTokens: 3400 })}
topModel={activeStats.models[0]}
weekTotals={usageTotals({ totalTokens: 8600 })}
/>
);
assert.match(html, /openai \/ Primary Key/);
assert.match(html, /30d Token Flow/);
assert.match(html, /Activity/);
assert.match(html, /Today tokens/);
assert.match(html, /Token Mix/);
assert.match(html, /Circular metrics/);
assert.match(html, /Model Share/);
assert.match(html, /Syncing usage\.\.\./);
});
test("UsageDetailPanel renders configured detail widgets and empty state", () => {
const activeStats = usageStats("7d");
const html = renderToStaticMarkup(
<UsageDetailPanel
accountSnapshots={accountSnapshots()}
activeStats={activeStats}
provider="openai"
range="7d"
widgets={[
{ id: "tabs", type: "source-tabs" },
{ id: "header", type: "header" },
{ id: "stats", type: "stats", variant: "compact" },
{ id: "account", type: "account", variant: "stacked" },
{ id: "flow", type: "token-flow", variant: "area" },
{ id: "activity", type: "activity" },
{ id: "mix", type: "token-mix", variant: "stacked" },
{ id: "rings", type: "rings", variant: "arcs" },
{ id: "share", type: "model-share", variant: "list" }
]}
onRangeChange={() => undefined}
/>
);
const emptyHtml = renderToStaticMarkup(
<UsageDetailPanel
accountSnapshots={[]}
activeStats={activeStats}
range="30d"
widgets={[{ id: "tabs", type: "source-tabs" }]}
onRangeChange={() => undefined}
/>
);
assert.match(html, /Usage Detail/);
assert.match(html, /7d - OpenAI/);
assert.match(html, /7d tokens/);
assert.match(html, /Token Flow/);
assert.match(html, /Token Mix/);
assert.match(html, /Circular metrics/);
assert.match(html, /Model Share/);
assert.match(emptyHtml, /No tray modules enabled/);
});
test("TrayStatusStrip renders open and quit actions", () => {
const html = renderToStaticMarkup(<TrayStatusStrip totalTokens={12500} />);
assert.match(html, /aria-label="Open CCR"/);
assert.match(html, /title="Open CCR"/);
assert.match(html, /12\.5K tokens/);
assert.match(html, /CCR/);
assert.match(html, /aria-label="Quit"/);
});
test("SourceGrid renders provider tabs with the selected state", () => {
const html = renderToStaticMarkup(
<SourceGrid
selectedProvider="openai"
tabs={[
{ id: "all", label: "All" },
{ id: "provider:openai", label: "OpenAI", provider: "openai" },
{ id: "provider:anthropic", label: "Anthropic", provider: "anthropic" }
]}
onSelect={() => undefined}
/>
);
assert.match(html, />All<\/button>/);
assert.match(html, /border-teal-300\/35 bg-teal-300\/16 text-teal-50/);
assert.match(html, />OpenAI<\/button>/);
assert.match(html, />Anthropic<\/button>/);
});
test("AccountSummaryPanel covers empty and metered account states", () => {
const emptyHtml = renderToStaticMarkup(<AccountSummaryPanel snapshots={[]} variant="bar" />);
const meteredHtml = renderToStaticMarkup(
<AccountSummaryPanel snapshots={accountSnapshots()} variant="stacked" onRefresh={() => undefined} />
);
assert.match(emptyHtml, /No account data configured/);
assert.match(meteredHtml, /openai \/ Primary Key/);
assert.match(meteredHtml, /5h quota/);
assert.match(meteredHtml, /42 requests/);
assert.match(meteredHtml, /style="\s*width:42%"/);
});
test("RangeSwitch renders every usage range option", () => {
const html = renderToStaticMarkup(<RangeSwitch range="7d" onChange={() => undefined} />);
assert.match(html, />Today<\/button>/);
assert.match(html, />24h<\/button>/);
assert.match(html, /bg-white\/14 text-slate-50/);
assert.match(html, />7d<\/button>/);
assert.match(html, />30d<\/button>/);
});
test("ChartShell renders title, meta, and children", () => {
const html = renderToStaticMarkup(
<ChartShell meta="gpt-4.1" title="Token Flow">
<span>chart body</span>
</ChartShell>
);
assert.match(html, /Token Flow/);
assert.match(html, /gpt-4\.1/);
assert.match(html, /chart body/);
});
test("StatsGrid renders cards, compact, and pill variants", () => {
const items = [
{ label: "Tokens", value: "12K" },
{ label: "Requests", value: "128" }
];
const cardsHtml = renderToStaticMarkup(<StatsGrid items={items} variant="cards" />);
const compactHtml = renderToStaticMarkup(<StatsGrid items={items} variant="compact" />);
const pillsHtml = renderToStaticMarkup(<StatsGrid items={items} variant="pills" />);
assert.match(cardsHtml, /grid-cols-2/);
assert.match(compactHtml, /py-0\.5/);
assert.match(pillsHtml, /rounded-full/);
assert.match(`${cardsHtml}${compactHtml}${pillsHtml}`, /Tokens/);
assert.match(`${cardsHtml}${compactHtml}${pillsHtml}`, /12K/);
});
test("AnimatedUsageChart renders line, area, bar, and sparkline output", () => {
const series = usageStats().series;
const lineHtml = renderToStaticMarkup(<AnimatedUsageChart chartId="line-chart" series={series} variant="line" />);
const areaHtml = renderToStaticMarkup(<AnimatedUsageChart chartId="area-chart" series={series} variant="area" />);
const barHtml = renderToStaticMarkup(<AnimatedUsageChart chartId="bar-chart" series={series} variant="bar" />);
const sparkHtml = renderToStaticMarkup(<AnimatedUsageChart chartId="spark-chart" series={series} variant="sparkline" />);
assert.match(lineHtml, /aria-label="Usage chart"/);
assert.match(lineHtml, /line-chart-glow/);
assert.match(areaHtml, /fill="rgba\(45,212,191,.18\)"/);
assert.match(barHtml, /<rect /);
assert.match(sparkHtml, /stroke-width="3"/);
});
test("TokenActivityPanel renders summary, grid, and legend", () => {
const html = renderToStaticMarkup(<TokenActivityPanel series={usageStats().series} />);
assert.match(html, /Activity/);
assert.match(html, /Longest streak/);
assert.match(html, /aria-label="Activity Tokens"/);
assert.match(html, /Less/);
assert.match(html, /More/);
});
test("TokenMixPanel renders bars, stacked, and share chart variants", () => {
const totals = usageTotals();
const barsHtml = renderToStaticMarkup(<TokenMixPanel totals={totals} variant="bars" />);
const stackedHtml = renderToStaticMarkup(<TokenMixPanel totals={totals} variant="stacked" />);
const donutHtml = renderToStaticMarkup(<TokenMixPanel totals={totals} variant="donut" />);
assert.match(barsHtml, /Token Mix/);
assert.match(barsHtml, /Input/);
assert.match(stackedHtml, /bg-blue-400/);
assert.match(donutHtml, /aria-label="Share chart"/);
});
test("RingMetrics and RadialMetric render accessible radial charts", () => {
const ringsHtml = renderToStaticMarkup(<RingMetrics totals={usageTotals()} variant="gauges" />);
const radialHtml = renderToStaticMarkup(
<RadialMetric centerUnit="tokens" centerValue="12K" color="rgb(45,212,191)" label="Cache 24%" value={0.24} variant="ring" />
);
assert.match(ringsHtml, /Circular metrics/);
assert.match(ringsHtml, /Success/);
assert.match(ringsHtml, /Cache/);
assert.match(radialHtml, /aria-label="Cache 24%"/);
assert.match(radialHtml, /12K/);
assert.match(radialHtml, /tokens/);
});
test("ModelShareChart renders populated variants and empty state", () => {
const rows = usageStats().models;
const barsHtml = renderToStaticMarkup(<ModelShareChart rows={rows} variant="bars" />);
const listHtml = renderToStaticMarkup(<ModelShareChart rows={rows} variant="list" />);
const donutHtml = renderToStaticMarkup(<ModelShareChart rows={rows} variant="donut" />);
const emptyHtml = renderToStaticMarkup(<ModelShareChart rows={[]} variant="bars" />);
assert.match(barsHtml, /Model Share/);
assert.match(barsHtml, /gpt-4\.1/);
assert.match(listHtml, /1\. gpt-4\.1/);
assert.match(listHtml, /62%/);
assert.match(donutHtml, /aria-label="Share chart"/);
assert.match(emptyHtml, /No usage captured yet/);
});
test("TrayApp and TrayDetailApp render their shell components without browser runtime", () => {
const trayHtml = renderToStaticMarkup(<TrayApp />);
const detailHtml = renderToStaticMarkup(<TrayDetailApp provider="openai" />);
assert.match(trayHtml, /Usage Overview/);
assert.match(trayHtml, /Open CCR/);
assert.match(detailHtml, /Usage Detail/);
assert.match(detailHtml, /OpenAI/);
});

View file

@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { buildTokenActivity, activityDateKey } from "../src/renderer/lib/usage-activity.ts";
import { buildTokenActivity, activityDateKey } from "../../src/renderer/lib/usage-activity.ts";
test("buildTokenActivity summarizes observed token days and streaks", () => {
const summary = buildTokenActivity(