Files
AgentHub/server/__tests__/token-baseline.test.js
T
nntrivi2001 a9c71cd905 feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
Usage panel (status bar):
- every account shows its plan (Claude subscriptionType, Codex
  chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier)
- new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity
  (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs
  agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's
  own file, sent only to its own host while valid, never refreshed
- in-flight quota reads are shared between tabs

AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added;
Gemini CLI, Hermes and OpenClaw removed.

Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat
on an editor selection, Settings in three groups, JetBrains-style Changes
panel (ticked files/hunks, ignore, blame, file history), terminal font.

--no-verify: :3000 is taken by another dev server (lane-detect tests);
both suites run by hand — server 2636/2639 (the 3 are :3000), client
1131/1131.
2026-10-08 12:22:14 +07:00

160 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Tests for replaceTokenUsage's compaction baseline semantics, and for
* the long-context subsets written beside it (high-water mark, exact reset).
*
* The effective total for a bucket is `live + baseline`. It must behave as a
* monotonic HIGH-WATER MARK: never decrease (so a compaction that shrinks the
* transcript doesn't lose usage), but never inflate past the largest value ever
* seen (so two writers hitting the same bucket with different scopes — the live
* hook writer stores main-only tokens, importSession stores main+subagents —
* can't ratchet the baseline upward on every downward fluctuation).
*
* Regression guard for the runaway that inflated one 26-day session's baseline
* to ~11× its real usage (dashboard total $22.5k vs true ~$15.2k).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const fs = require("fs");
const os = require("os");
const TEST_DB = path.join(os.tmpdir(), `dashboard-token-baseline-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const dbModule = require("../db");
const { db, stmts } = dbModule;
after(() => {
if (db) db.close();
for (const suffix of ["", "-wal", "-shm"]) {
try {
fs.unlinkSync(TEST_DB + suffix);
} catch {
/* ignore */
}
}
});
// Write a cache_read value for a fixed bucket; return effective (live+baseline).
function writeCacheRead(sessionId, cacheRead) {
stmts.replaceTokenUsage.run(
sessionId,
"claude-opus-4-8",
"standard",
"global",
"standard",
0, // input
0, // output
cacheRead,
0, // cache_write
0, // cache_write_1h
0, // web_search
0, // web_fetch
0 // code_execution
);
}
function effectiveCacheRead(sessionId) {
const r = stmts.getTokensBySession.all(sessionId).find((x) => x.model === "claude-opus-4-8");
return r ? r.cache_read_tokens : 0;
}
function rawRow(sessionId) {
return db
.prepare(
"SELECT cache_read_tokens, baseline_cache_read FROM token_usage WHERE session_id = ? AND model = 'claude-opus-4-8'"
)
.get(sessionId);
}
describe("replaceTokenUsage — high-water-mark baseline", () => {
before(() => {
for (const id of ["hw-grow", "hw-idem", "hw-shrink", "hw-altern", "hw-recover"]) {
try {
stmts.insertSession.run(id, "t", "active", "/tmp", null, null);
} catch {
/* exists */
}
}
});
it("monotonic growth: effective follows the latest, no baseline", () => {
writeCacheRead("hw-grow", 100);
assert.equal(effectiveCacheRead("hw-grow"), 100);
writeCacheRead("hw-grow", 300);
assert.equal(effectiveCacheRead("hw-grow"), 300);
assert.equal(rawRow("hw-grow").baseline_cache_read, 0);
});
it("idempotent: rewriting the same value never changes effective", () => {
writeCacheRead("hw-idem", 500);
writeCacheRead("hw-idem", 500);
writeCacheRead("hw-idem", 500);
assert.equal(effectiveCacheRead("hw-idem"), 500);
assert.equal(rawRow("hw-idem").baseline_cache_read, 0);
});
it("decrease preserves the max (compaction never loses usage)", () => {
writeCacheRead("hw-shrink", 300);
writeCacheRead("hw-shrink", 100); // transcript shrank
assert.equal(effectiveCacheRead("hw-shrink"), 300, "effective must not drop below the peak");
const row = rawRow("hw-shrink");
assert.equal(row.cache_read_tokens, 100);
assert.equal(row.baseline_cache_read, 200); // 100 live + 200 baseline = 300
});
it("writer alternation does NOT run away (the bug)", () => {
// Two writers: 'big' (main+subagents) and 'small' (main-only) on one bucket.
for (let i = 0; i < 20; i++) {
writeCacheRead("hw-altern", 800); // big writer
writeCacheRead("hw-altern", 700); // small writer
}
// Effective must stay pinned at the true max (800), NOT accumulate.
assert.equal(effectiveCacheRead("hw-altern"), 800, "effective must stay at the peak, not grow");
assert.equal(rawRow("hw-altern").baseline_cache_read, 100); // 700 + 100 = 800
});
it("recovers upward: a new higher value zeroes stale baseline", () => {
writeCacheRead("hw-recover", 300);
writeCacheRead("hw-recover", 100); // baseline=200, eff=300
writeCacheRead("hw-recover", 500); // new peak
assert.equal(effectiveCacheRead("hw-recover"), 500);
assert.equal(rawRow("hw-recover").baseline_cache_read, 0);
});
});
describe("long-context subsets", () => {
const { writeSessionTokens } = require("../../scripts/import-history");
const { emptyBucket, accumulateBucket, extractUsageFields } = require("../lib/token-usage");
const longRow = (sid) =>
db
.prepare("SELECT input_tokens, long_input_tokens FROM token_usage WHERE session_id = ?")
.get(sid);
const bucketOf = (...usages) => {
const b = emptyBucket("claude-haiku-5-5", "standard", "global", "standard");
for (const u of usages) accumulateBucket(b, extractUsageFields(u));
return { k: b };
};
before(() => {
db.prepare(
"INSERT OR IGNORE INTO sessions (id, name, status) VALUES ('long-1', 'x', 'active')"
).run();
});
it("stores the long part as a subset of the same row and keeps its high-water mark", () => {
const short = { input_tokens: 1_000 };
const long = { input_tokens: 150_000 };
writeSessionTokens(dbModule, "long-1", bucketOf(short, long));
assert.deepEqual(longRow("long-1"), { input_tokens: 151_000, long_input_tokens: 150_000 });
// A smaller-scope writer never takes the subset back.
writeSessionTokens(dbModule, "long-1", bucketOf(short));
assert.equal(longRow("long-1").long_input_tokens, 150_000);
});
it("lets the exact reconcile path clear a stale subset", () => {
writeSessionTokens(dbModule, "long-1", bucketOf({ input_tokens: 1_000 }), { exact: true });
assert.deepEqual(longRow("long-1"), { input_tokens: 1_000, long_input_tokens: 0 });
});
});