185 lines
5.8 KiB
JavaScript
185 lines
5.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { RequestLogStore } from "../../packages/core/src/observability/request-log-store.ts";
|
|
import { UsageStore } from "../../packages/core/src/usage/store.ts";
|
|
|
|
test("UsageStore aggregates stats in SQLite without loading all events", async () => {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-test-"));
|
|
try {
|
|
const store = new UsageStore(path.join(dir, "usage.sqlite"));
|
|
const now = new Date();
|
|
const earlier = new Date(now.getTime() - 60_000);
|
|
|
|
await store.record({
|
|
createdAt: earlier.toISOString(),
|
|
durationMs: 120,
|
|
method: "POST",
|
|
model: "alpha-model",
|
|
path: "/v1/messages",
|
|
provider: "alpha",
|
|
requestId: "req-1",
|
|
statusCode: 200,
|
|
usage: {
|
|
cacheReadTokens: 2,
|
|
inputTokens: 10,
|
|
outputTokens: 5,
|
|
totalTokens: 17
|
|
}
|
|
});
|
|
await store.record({
|
|
createdAt: now.toISOString(),
|
|
durationMs: 80,
|
|
method: "POST",
|
|
model: "beta-model",
|
|
path: "/v1/messages",
|
|
provider: "beta",
|
|
requestId: "req-2",
|
|
statusCode: 500,
|
|
usage: {
|
|
inputTokens: 4,
|
|
outputTokens: 6
|
|
}
|
|
});
|
|
|
|
const stats = await store.getStats("30d", { includeProxy: true });
|
|
assert.equal(stats.totals.requestCount, 2);
|
|
assert.equal(stats.totals.errorCount, 1);
|
|
assert.equal(stats.totals.totalTokens, 27);
|
|
assert.equal(stats.totals.inputTokens, 14);
|
|
assert.equal(stats.totals.outputTokens, 11);
|
|
assert.equal(stats.recentRequests.length, 2);
|
|
assert.equal(stats.models[0]?.requestCount, 1);
|
|
} finally {
|
|
rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
test("UsageStore excludes proxy rows by default and includes them on request", async () => {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-proxy-test-"));
|
|
try {
|
|
const store = new UsageStore(path.join(dir, "usage.sqlite"));
|
|
const createdAt = new Date().toISOString();
|
|
|
|
await store.record({
|
|
createdAt,
|
|
durationMs: 10,
|
|
method: "POST",
|
|
model: "direct/model-a",
|
|
path: "/v1/messages",
|
|
requestId: "direct-1",
|
|
statusCode: 200,
|
|
usage: {
|
|
inputTokens: 5,
|
|
outputTokens: 7
|
|
}
|
|
});
|
|
await store.record({
|
|
createdAt,
|
|
durationMs: 10,
|
|
method: "POST",
|
|
model: "proxy-model",
|
|
path: "/v1/messages",
|
|
provider: "proxy",
|
|
requestId: "proxy-1",
|
|
statusCode: 200,
|
|
usage: {
|
|
inputTokens: 100,
|
|
outputTokens: 200
|
|
}
|
|
});
|
|
|
|
const defaultStats = await store.getStats("30d");
|
|
assert.equal(defaultStats.totals.requestCount, 1);
|
|
assert.equal(defaultStats.totals.totalTokens, 12);
|
|
assert.equal(defaultStats.providerModels[0]?.provider, "direct");
|
|
assert.equal(defaultStats.providerModels[0]?.model, "model-a");
|
|
|
|
const withProxy = await store.getStats("30d", { includeProxy: true });
|
|
assert.equal(withProxy.totals.requestCount, 2);
|
|
assert.equal(withProxy.totals.totalTokens, 312);
|
|
} finally {
|
|
rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
test("UsageStore treats null web RPC usage filters as empty filters", async () => {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-null-filter-test-"));
|
|
try {
|
|
const store = new UsageStore(path.join(dir, "usage.sqlite"));
|
|
|
|
await store.record({
|
|
createdAt: new Date().toISOString(),
|
|
durationMs: 10,
|
|
method: "POST",
|
|
model: "alpha-model",
|
|
path: "/v1/messages",
|
|
provider: "alpha",
|
|
requestId: "req-null-filter",
|
|
statusCode: 200,
|
|
usage: {
|
|
inputTokens: 3,
|
|
outputTokens: 4
|
|
}
|
|
});
|
|
|
|
const stats = await store.getStats("7d", null);
|
|
assert.equal(stats.range, "7d");
|
|
assert.equal(stats.totals.requestCount, 1);
|
|
|
|
const defaultRangeStats = await store.getStats(null, null);
|
|
assert.equal(defaultRangeStats.range, "7d");
|
|
assert.equal(defaultRangeStats.totals.requestCount, 1);
|
|
} finally {
|
|
rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
test("UsageStore backfills missing events from request logs", async () => {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "ccr-usage-request-log-backfill-test-"));
|
|
try {
|
|
const requestLogDbFile = path.join(dir, "request-logs.sqlite");
|
|
const requestLogStore = new RequestLogStore(requestLogDbFile);
|
|
const usageStore = new UsageStore(path.join(dir, "usage.sqlite"), { requestLogDbFile });
|
|
const createdAt = new Date().toISOString();
|
|
|
|
await requestLogStore.record({
|
|
client: "Claude Code",
|
|
completedAt: createdAt,
|
|
durationMs: 25,
|
|
method: "POST",
|
|
path: "/v1/messages",
|
|
providerName: "alpha",
|
|
requestBody: Buffer.from(JSON.stringify({ model: "alpha-model" })),
|
|
requestHeaders: { "content-type": "application/json" },
|
|
requestId: "req-backfill-1",
|
|
responseBodyText: JSON.stringify({
|
|
model: "alpha-model",
|
|
usage: {
|
|
input_tokens: 12,
|
|
output_tokens: 5,
|
|
total_tokens: 17
|
|
}
|
|
}),
|
|
responseHeaders: new Headers({ "content-type": "application/json" }),
|
|
startedAt: createdAt,
|
|
statusCode: 200,
|
|
url: "http://127.0.0.1:3456/v1/messages"
|
|
});
|
|
|
|
const stats = await usageStore.getStats("today", { includeProxy: true });
|
|
assert.equal(stats.totals.requestCount, 1);
|
|
assert.equal(stats.totals.totalTokens, 17);
|
|
assert.equal(stats.providerModels[0]?.provider, "alpha");
|
|
assert.equal(stats.providerModels[0]?.model, "alpha-model");
|
|
|
|
const reread = await usageStore.getStats("today", { includeProxy: true });
|
|
assert.equal(reread.totals.requestCount, 1);
|
|
assert.equal(reread.totals.totalTokens, 17);
|
|
} finally {
|
|
rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|