mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-11-05 01:01:29 +08:00
256 lines
7.3 KiB
TypeScript
256 lines
7.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import * as React from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import type { AgentAnalysisSessionRow, AgentAnalysisTraceRun, RequestLogEntry, RequestLogPage } from "@ccr/core/contracts/app.ts";
|
|
import { AgentAnalysisView } from "@ccr/ui/pages/home/components/dashboard.tsx";
|
|
import { LogsView } from "@ccr/ui/pages/home/components/network-logs.tsx";
|
|
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
|
|
import { createEmptyAgentAnalysis } from "@ccr/ui/pages/home/shared/usage.ts";
|
|
|
|
const emptyLogPage: RequestLogPage = {
|
|
generatedAt: "2026-07-23T00:00:00.000Z",
|
|
items: [],
|
|
options: {
|
|
credentials: [],
|
|
models: [],
|
|
providers: []
|
|
},
|
|
page: 1,
|
|
pageSize: 25,
|
|
total: 0,
|
|
totalPages: 1
|
|
};
|
|
|
|
const sampleRequestLogEntry: RequestLogEntry = {
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
client: "claude-code",
|
|
costUsd: 0.01,
|
|
createdAt: "2026-07-23T00:00:00.000Z",
|
|
credentialChain: [],
|
|
credentialSaturated: false,
|
|
durationMs: 120,
|
|
id: 1,
|
|
inputTokens: 100,
|
|
isStream: true,
|
|
method: "POST",
|
|
model: "claude-sonnet-4",
|
|
ok: true,
|
|
outputTokens: 50,
|
|
path: "/v1/messages",
|
|
provider: "anthropic",
|
|
reasoningTokens: 0,
|
|
requestBody: { encoding: "utf8", sizeBytes: 2, text: "{}", truncated: false },
|
|
requestHeaders: {},
|
|
requestId: "req-token-copy",
|
|
routeAttemptCount: 1,
|
|
routeHopCount: 1,
|
|
routeTraceTruncated: false,
|
|
retryAttempts: [],
|
|
responseBody: { encoding: "utf8", sizeBytes: 2, text: "{}", truncated: false },
|
|
responseHeaders: {},
|
|
statusCode: 200,
|
|
totalTokens: 150,
|
|
url: "/v1/messages"
|
|
};
|
|
|
|
test("LogsView keeps disabled request logs discoverable with an enable action", () => {
|
|
const html = renderToStaticMarkup(
|
|
<AppI18nContext.Provider value={appCopy.zh}>
|
|
<LogsView
|
|
enabled={false}
|
|
error=""
|
|
filter={{ page: 1, pageSize: 25, status: "all" }}
|
|
loading={false}
|
|
onEnable={() => undefined}
|
|
page={emptyLogPage}
|
|
refreshLogs={() => undefined}
|
|
updateFilter={() => undefined}
|
|
/>
|
|
</AppI18nContext.Provider>
|
|
);
|
|
|
|
assert.match(html, /请求日志已关闭/);
|
|
assert.match(html, /启用请求日志/);
|
|
});
|
|
|
|
test("LogsView explains filtered empty results and translates page sizes", () => {
|
|
const html = renderToStaticMarkup(
|
|
<AppI18nContext.Provider value={appCopy.en}>
|
|
<LogsView
|
|
error=""
|
|
filter={{ page: 1, pageSize: 25, query: "missing", status: "all" }}
|
|
loading={false}
|
|
page={emptyLogPage}
|
|
refreshLogs={() => undefined}
|
|
updateFilter={() => undefined}
|
|
/>
|
|
</AppI18nContext.Provider>
|
|
);
|
|
|
|
assert.match(html, /No request logs match the current filters\./);
|
|
assert.match(html, /Clear filters/);
|
|
assert.match(html, /25 \/ page/);
|
|
assert.doesNotMatch(html, /\/ 页/);
|
|
});
|
|
|
|
test("LogsView keeps Chinese token column copy as Token", () => {
|
|
const html = renderToStaticMarkup(
|
|
<AppI18nContext.Provider value={appCopy.zh}>
|
|
<LogsView
|
|
error=""
|
|
filter={{ page: 1, pageSize: 25, status: "all" }}
|
|
loading={false}
|
|
page={{ ...emptyLogPage, items: [sampleRequestLogEntry], total: 1 }}
|
|
refreshLogs={() => undefined}
|
|
updateFilter={() => undefined}
|
|
/>
|
|
</AppI18nContext.Provider>
|
|
);
|
|
|
|
assert.match(html, /Token/);
|
|
assert.doesNotMatch(html, /令牌/);
|
|
});
|
|
|
|
test("AgentAnalysisView keeps session headings horizontal and shows cache rate and cost", () => {
|
|
const session: AgentAnalysisSessionRow = {
|
|
agent: "claude-code",
|
|
avgDurationMs: 420,
|
|
cacheRatio: 0.375,
|
|
cacheReadTokens: 300,
|
|
cacheTokens: 300,
|
|
cacheWriteTokens: 100,
|
|
client: "claude-code",
|
|
costUsd: 1.25,
|
|
durationMs: 900,
|
|
errorCount: 0,
|
|
id: "session-cache-cost",
|
|
inputTokens: 500,
|
|
lastSeenAt: "2026-07-23T00:01:00.000Z",
|
|
maxConcurrentRequests: 1,
|
|
maxDurationMs: 500,
|
|
models: ["claude-sonnet-4"],
|
|
outputTokens: 100,
|
|
p50DurationMs: 420,
|
|
p95DurationMs: 500,
|
|
p99DurationMs: 500,
|
|
providers: ["anthropic"],
|
|
requestCount: 2,
|
|
sessionCount: 1,
|
|
startedAt: "2026-07-23T00:00:00.000Z",
|
|
subagentCallCount: 0,
|
|
successRate: 1,
|
|
toolCallCount: 1,
|
|
topTools: [{ count: 1, name: "Read" }],
|
|
totalTokens: 1000
|
|
};
|
|
const snapshot = {
|
|
...createEmptyAgentAnalysis("24h"),
|
|
scannedRequestCount: 2,
|
|
selectedSession: {
|
|
endpoints: [],
|
|
errors: [],
|
|
models: [],
|
|
requests: [{
|
|
agent: session.agent,
|
|
cacheReadTokens: 300,
|
|
cacheWriteTokens: 100,
|
|
client: session.client,
|
|
concurrentRequests: 1,
|
|
costUsd: 0.25,
|
|
createdAt: session.startedAt,
|
|
durationMs: 420,
|
|
id: 42,
|
|
inputTokens: 500,
|
|
method: "POST",
|
|
model: "claude-sonnet-4",
|
|
ok: true,
|
|
outputTokens: 100,
|
|
path: "/v1/messages",
|
|
provider: "anthropic",
|
|
requestId: "request-with-cost",
|
|
routeReason: "default",
|
|
sessionId: session.id,
|
|
statusCode: 200,
|
|
toolCallCount: 1,
|
|
tools: ["Read"],
|
|
totalTokens: 1000
|
|
}],
|
|
routes: [],
|
|
session,
|
|
statusCodes: [],
|
|
subagents: [],
|
|
tools: [],
|
|
totals: session,
|
|
trace: {
|
|
agent: session.agent,
|
|
durationMs: session.durationMs,
|
|
endedAt: session.lastSeenAt,
|
|
errorCount: 1,
|
|
id: `${session.agent}:${session.id}`,
|
|
llmRunCount: 0,
|
|
maxDepth: 0,
|
|
rootRunId: `agent:${session.agent}:${session.id}`,
|
|
runCount: 1,
|
|
runs: [{
|
|
agent: session.agent,
|
|
cacheReadTokens: session.cacheReadTokens,
|
|
cacheWriteTokens: session.cacheWriteTokens,
|
|
concurrentRequests: session.maxConcurrentRequests,
|
|
costUsd: 0.75,
|
|
depth: 0,
|
|
durationMs: session.durationMs,
|
|
endedAt: session.lastSeenAt,
|
|
id: `agent:${session.agent}:${session.id}`,
|
|
inputTokens: session.inputTokens,
|
|
kind: "agent",
|
|
name: "Claude Code session",
|
|
offsetMs: 0,
|
|
outputTokens: session.outputTokens,
|
|
sessionId: session.id,
|
|
startedAt: session.startedAt,
|
|
status: "partial",
|
|
totalTokens: session.totalTokens
|
|
} satisfies AgentAnalysisTraceRun],
|
|
sessionId: session.id,
|
|
startedAt: session.startedAt,
|
|
subagentRunCount: 0,
|
|
toolRunCount: 0
|
|
}
|
|
},
|
|
sessions: [session]
|
|
};
|
|
|
|
const html = renderToStaticMarkup(
|
|
<AppI18nContext.Provider value={appCopy.zh}>
|
|
<AgentAnalysisView
|
|
agentFilter="all"
|
|
error=""
|
|
loading={false}
|
|
range="24h"
|
|
refreshAnalysis={() => undefined}
|
|
setAgentFilter={() => undefined}
|
|
setRange={() => undefined}
|
|
setSelectedSession={() => undefined}
|
|
snapshot={snapshot}
|
|
/>
|
|
</AppI18nContext.Provider>
|
|
);
|
|
|
|
assert.match(html, /持续时间/);
|
|
assert.match(html, /子代理/);
|
|
assert.match(html, /缓存率/);
|
|
assert.match(html, /38%/);
|
|
assert.match(html, /成本/);
|
|
assert.match(html, /Token/);
|
|
assert.doesNotMatch(html, /令牌/);
|
|
assert.match(html, /\$1\.25/);
|
|
assert.match(html, /\$0\.25/);
|
|
assert.match(html, /\$0\.75/);
|
|
assert.match(html, /部分失败/);
|
|
assert.match(html, /border-amber-200/);
|
|
assert.match(html, /min-w-\[64px\]/);
|
|
assert.match(html, /whitespace-nowrap/);
|
|
});
|