fix(usage): skip empty timeseries scans

This commit is contained in:
Vincent Koc 2026-06-01 18:20:47 +02:00
parent 373ef81e83
commit a8bc1716dd
No known key found for this signature in database
4 changed files with 144 additions and 87 deletions

View file

@ -6,6 +6,7 @@ import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { testing as voiceCallCliTesting } from "../../extensions/voice-call/src/cli.ts";
import { loadSessionLogs, loadSessionUsageTimeSeries } from "../../src/infra/session-cost-usage.ts";
import {
@ -14,6 +15,15 @@ import {
resetDiagnosticPhasesForTest,
} from "../../src/logging/diagnostic-phase.ts";
export async function withProofTempRoot(callback) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-proof-"));
try {
return await callback(root);
} finally {
fs.rmSync(root, { force: true, recursive: true });
}
}
async function main() {
resetDiagnosticPhasesForTest();
recordDiagnosticPhase({
@ -40,77 +50,80 @@ async function main() {
assert.equal(zeroPhases.length, 0);
console.log("getRecentDiagnosticPhases(0).length =", zeroPhases.length);
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-proof-"));
const sessionFile = path.join(root, "s.jsonl");
fs.writeFileSync(
sessionFile,
[
JSON.stringify({
type: "message",
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "a" },
}),
JSON.stringify({
type: "message",
timestamp: "2026-01-01T00:01:00.000Z",
message: {
role: "assistant",
content: "b",
provider: "openai",
model: "gpt-5.5",
usage: {
input: 1,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 3,
cost: { total: 0.001 },
await withProofTempRoot(async (root) => {
const sessionFile = path.join(root, "s.jsonl");
fs.writeFileSync(
sessionFile,
[
JSON.stringify({
type: "message",
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "a" },
}),
JSON.stringify({
type: "message",
timestamp: "2026-01-01T00:01:00.000Z",
message: {
role: "assistant",
content: "b",
provider: "openai",
model: "gpt-5.5",
usage: {
input: 1,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 3,
cost: { total: 0.001 },
},
},
},
}),
JSON.stringify({
type: "message",
timestamp: "2026-01-01T00:02:00.000Z",
message: {
role: "assistant",
content: "c",
provider: "openai",
model: "gpt-5.5",
usage: {
input: 3,
output: 4,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 7,
cost: { total: 0.002 },
}),
JSON.stringify({
type: "message",
timestamp: "2026-01-01T00:02:00.000Z",
message: {
role: "assistant",
content: "c",
provider: "openai",
model: "gpt-5.5",
usage: {
input: 3,
output: 4,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 7,
cost: { total: 0.002 },
},
},
},
}),
].join("\n"),
);
}),
].join("\n"),
);
const logs = await loadSessionLogs({ sessionFile, limit: 0 });
const series = await loadSessionUsageTimeSeries({ sessionFile, maxPoints: 0 });
const positiveLogs = await loadSessionLogs({ sessionFile, limit: 10 });
const positiveSeries = await loadSessionUsageTimeSeries({ sessionFile, maxPoints: 10 });
assert.equal(logs?.length, 0);
assert.equal(series.points.length, 0);
assert.equal(positiveLogs?.length, 3);
assert.equal(positiveSeries.points.length, 2);
console.log("loadSessionLogs({ limit: 0 }).length =", logs?.length);
console.log(
"loadSessionUsageTimeSeries({ maxPoints: 0 }).points.length =",
series?.points.length,
);
const logs = await loadSessionLogs({ sessionFile, limit: 0 });
const series = await loadSessionUsageTimeSeries({ sessionFile, maxPoints: 0 });
const positiveLogs = await loadSessionLogs({ sessionFile, limit: 10 });
const positiveSeries = await loadSessionUsageTimeSeries({ sessionFile, maxPoints: 10 });
assert.equal(logs?.length, 0);
assert.equal(series.points.length, 0);
assert.equal(positiveLogs?.length, 3);
assert.equal(positiveSeries.points.length, 2);
console.log("loadSessionLogs({ limit: 0 }).length =", logs?.length);
console.log(
"loadSessionUsageTimeSeries({ maxPoints: 0 }).points.length =",
series?.points.length,
);
try {
voiceCallCliTesting.parseVoiceCallIntOption("nope", "--port", { min: 1 });
assert.fail("expected invalid voicecall --port value to throw");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
assert.equal(message, "Invalid numeric value for --port: nope");
console.log("parseVoiceCallIntOption('nope', '--port') error:", message);
}
try {
voiceCallCliTesting.parseVoiceCallIntOption("nope", "--port", { min: 1 });
assert.fail("expected invalid voicecall --port value to throw");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
assert.equal(message, "Invalid numeric value for --port: nope");
console.log("parseVoiceCallIntOption('nope', '--port') error:", message);
}
});
}
await main();
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}

View file

@ -2523,20 +2523,26 @@ example
"utf-8",
);
await expect(loadSessionUsageTimeSeries({ sessionFile, maxPoints: 0 })).resolves.toEqual({
sessionId: undefined,
points: [],
});
await expect(loadSessionUsageTimeSeries({ sessionFile, maxPoints: -1 })).resolves.toEqual({
sessionId: undefined,
points: [],
});
await expect(
loadSessionUsageTimeSeries({ sessionFile, maxPoints: Number.NaN }),
).resolves.toEqual({ sessionId: undefined, points: [] });
await expect(
loadSessionUsageTimeSeries({ sessionFile, maxPoints: Number.POSITIVE_INFINITY }),
).resolves.toEqual({ sessionId: undefined, points: [] });
const createReadStream = vi.spyOn(nodeFs, "createReadStream");
try {
await expect(loadSessionUsageTimeSeries({ sessionFile, maxPoints: 0 })).resolves.toEqual({
sessionId: undefined,
points: [],
});
await expect(loadSessionUsageTimeSeries({ sessionFile, maxPoints: -1 })).resolves.toEqual({
sessionId: undefined,
points: [],
});
await expect(
loadSessionUsageTimeSeries({ sessionFile, maxPoints: Number.NaN }),
).resolves.toEqual({ sessionId: undefined, points: [] });
await expect(
loadSessionUsageTimeSeries({ sessionFile, maxPoints: Number.POSITIVE_INFINITY }),
).resolves.toEqual({ sessionId: undefined, points: [] });
expect(createReadStream).not.toHaveBeenCalled();
} finally {
createReadStream.mockRestore();
}
});
it("returns empty logs for zero, negative, and non-finite limits", async () => {

View file

@ -2300,6 +2300,12 @@ export async function loadSessionUsageTimeSeries(params: {
return null;
}
if (params.maxPoints !== undefined && params.maxPoints !== null) {
if (!Number.isFinite(params.maxPoints) || params.maxPoints <= 0) {
return { sessionId: params.sessionId, points: [] };
}
}
const points: SessionUsageTimePoint[] = [];
let cumulativeTokens = 0;
let cumulativeCost = 0;
@ -2341,11 +2347,6 @@ export async function loadSessionUsageTimeSeries(params: {
const sortedPoints = points.toSorted((a, b) => a.timestamp - b.timestamp);
// Optionally downsample if too many points
if (params.maxPoints !== undefined && params.maxPoints !== null) {
if (!Number.isFinite(params.maxPoints) || params.maxPoints <= 0) {
return { sessionId: params.sessionId, points: [] };
}
}
const maxPoints = params.maxPoints ?? 100;
if (sortedPoints.length > maxPoints) {
const step = Math.ceil(sortedPoints.length / maxPoints);

View file

@ -0,0 +1,37 @@
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withProofTempRoot } from "../../scripts/repro/limit-edge-case-live-proof.mjs";
describe("limit-edge-case live proof", () => {
it("cleans the generated session-log temp root", async () => {
const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-limit-proof-test-"));
try {
const originalTmpdir = process.env.TMPDIR;
process.env.TMPDIR = tempRoot;
let proofRoot = "";
try {
await withProofTempRoot(async (root) => {
proofRoot = root;
writeFileSync(path.join(root, "s.jsonl"), "{}\n");
expect(existsSync(root)).toBe(true);
});
} finally {
if (originalTmpdir === undefined) {
delete process.env.TMPDIR;
} else {
process.env.TMPDIR = originalTmpdir;
}
}
expect(proofRoot).not.toBe("");
expect(existsSync(proofRoot)).toBe(false);
expect(readdirSync(tempRoot).filter((entry) => entry.startsWith("openclaw-proof-"))).toEqual(
[],
);
} finally {
rmSync(tempRoot, { force: true, recursive: true });
}
});
});