import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { persistSessionUsageUpdate } from "./session-usage.js"; async function seedSessionStore(params: { storePath: string; sessionKey: string; entry: Record; }) { await fs.mkdir(path.dirname(params.storePath), { recursive: true }); await fs.writeFile( params.storePath, JSON.stringify({ [params.sessionKey]: params.entry }, null, 2), "utf-8", ); } describe("persistSessionUsageUpdate", () => { it("uses lastCallUsage for totalTokens when provided", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-")); const storePath = path.join(tmp, "sessions.json"); const sessionKey = "main"; await seedSessionStore({ storePath, sessionKey, entry: { sessionId: "s1", updatedAt: Date.now(), totalTokens: 100_000 }, }); // Accumulated usage (sums all API calls) — inflated const accumulatedUsage = { input: 180_000, output: 10_000, total: 190_000 }; // Last individual API call's usage — actual context after compaction const lastCallUsage = { input: 12_000, output: 2_000, total: 14_000 }; await persistSessionUsageUpdate({ storePath, sessionKey, usage: accumulatedUsage, lastCallUsage, contextTokensUsed: 200_000, }); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); // totalTokens should reflect lastCallUsage (12_000 input), not accumulated (180_000) expect(stored[sessionKey].totalTokens).toBe(12_000); expect(stored[sessionKey].totalTokensFresh).toBe(true); // inputTokens/outputTokens still reflect accumulated usage for cost tracking expect(stored[sessionKey].inputTokens).toBe(180_000); expect(stored[sessionKey].outputTokens).toBe(10_000); }); it("marks totalTokens as unknown when no fresh context snapshot is available", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-")); const storePath = path.join(tmp, "sessions.json"); const sessionKey = "main"; await seedSessionStore({ storePath, sessionKey, entry: { sessionId: "s1", updatedAt: Date.now() }, }); await persistSessionUsageUpdate({ storePath, sessionKey, usage: { input: 50_000, output: 5_000, total: 55_000 }, contextTokensUsed: 200_000, }); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); expect(stored[sessionKey].totalTokens).toBeUndefined(); expect(stored[sessionKey].totalTokensFresh).toBe(false); }); it("uses promptTokens when available without lastCallUsage", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-")); const storePath = path.join(tmp, "sessions.json"); const sessionKey = "main"; await seedSessionStore({ storePath, sessionKey, entry: { sessionId: "s1", updatedAt: Date.now() }, }); await persistSessionUsageUpdate({ storePath, sessionKey, usage: { input: 50_000, output: 5_000, total: 55_000 }, promptTokens: 42_000, contextTokensUsed: 200_000, }); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); expect(stored[sessionKey].totalTokens).toBe(42_000); expect(stored[sessionKey].totalTokensFresh).toBe(true); }); it("keeps non-clamped lastCallUsage totalTokens when exceeding context window", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-")); const storePath = path.join(tmp, "sessions.json"); const sessionKey = "main"; await seedSessionStore({ storePath, sessionKey, entry: { sessionId: "s1", updatedAt: Date.now() }, }); await persistSessionUsageUpdate({ storePath, sessionKey, usage: { input: 300_000, output: 10_000, total: 310_000 }, lastCallUsage: { input: 250_000, output: 5_000, total: 255_000 }, contextTokensUsed: 200_000, }); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); expect(stored[sessionKey].totalTokens).toBe(250_000); expect(stored[sessionKey].totalTokensFresh).toBe(true); }); });