mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(usage): skip empty timeseries scans
This commit is contained in:
parent
373ef81e83
commit
a8bc1716dd
4 changed files with 144 additions and 87 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
37
test/scripts/limit-edge-case-live-proof.test.ts
Normal file
37
test/scripts/limit-edge-case-live-proof.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue