vps-health-api/__tests__/agent-claude-usage.test.js
le king fu c888f7c633
Some checks are pending
CI / verification (pull_request) Successful in 28s
CI / verification (push) Waiting to run
fix(agent): take the scan threshold from the open days, key maps without prototype
The threshold came from the most recent frozen key: a frozen day in the
future, or a gap in the frozen run, pushed it past open days whose files
were then never read, and the agent would have frozen them empty for good.
It is now the oldest open day of the window (the same day as before for the
agent's contiguous frozen prefix).

Project, model and day maps are now Object.create(null): a project named
__proto__ or constructor wrote NaN counters onto Object.prototype and
dropped out of the top.

Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:25:43 -04:00

755 lines
28 KiB
JavaScript

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
aggregate,
mapProject,
torontoDay,
shiftDay,
windowDays,
capModels,
WINDOW_DAYS,
MAX_MODELS_PER_DAY,
OTHER_MODEL,
OTHER_PROJECTS,
} = require("../agent/claude-usage/aggregate.js");
// Coverage for the pure aggregation of the claude-usage agent (issue #49).
//
// Every transcript here is synthetic (__tests__/fixtures/claude-usage/ or
// written inline): no real ~/.claude content ever enters the repo. Fixtures
// are copied into a temp dir and their mtimes set with utimesSync, so the
// scan threshold is exercised on explicit instants, never on the wall clock.
const FIXTURES = path.join(__dirname, "fixtures", "claude-usage", "projects");
const HOME = "/home/tester";
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-usage-test-"));
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function setMtime(file, iso) {
const t = new Date(iso);
fs.utimesSync(file, t, t);
}
// Write a transcript under tmpDir. `lines` are objects (serialized) or raw
// strings (written verbatim, for broken lines).
function writeTranscript(rel, lines, mtimeIso) {
const file = path.join(tmpDir, rel);
fs.mkdirSync(path.dirname(file), { recursive: true });
const body = lines.map((l) => (typeof l === "string" ? l : JSON.stringify(l))).join("\n");
fs.writeFileSync(file, `${body}\n`);
if (mtimeIso) setMtime(file, mtimeIso);
return file;
}
let seq = 0;
function line({
ts,
model = "claude-opus-5-5",
cwd = `${HOME}/claude-code/alpha`,
id,
req,
input = 1,
output = 1,
cacheCreation = 0,
cacheRead = 0,
version = "2.1.283",
type = "assistant",
}) {
seq += 1;
return {
type,
message: {
id: id || `msg_${seq}`,
model,
usage: {
input_tokens: input,
output_tokens: output,
cache_creation_input_tokens: cacheCreation,
cache_read_input_tokens: cacheRead,
},
},
requestId: req || `req_${seq}`,
cwd,
timestamp: ts,
version,
};
}
function dayOf(result, date) {
return result.days.find((d) => d.date === date);
}
function sumWork(projects) {
return projects.reduce((s, p) => s + p.work, 0);
}
// --- Dates ------------------------------------------------------------------
describe("Toronto dates", () => {
it("23:59 and 00:01 Toronto time fall on two different days (EDT)", () => {
expect(torontoDay(Date.parse("2026-10-03T03:59:00Z"))).toBe("2026-10-02");
expect(torontoDay(Date.parse("2026-10-03T04:01:00Z"))).toBe("2026-10-03");
});
it("splits days correctly on both sides of the 2026-11-01 DST change", () => {
// Midnight Oct 31 -> Nov 1 is still EDT (UTC-4).
expect(torontoDay(Date.parse("2026-11-01T03:59:00Z"))).toBe("2026-10-31");
expect(torontoDay(Date.parse("2026-11-01T04:01:00Z"))).toBe("2026-11-01");
// Midnight Nov 1 -> Nov 2 is EST (UTC-5): a fixed -4 offset would be wrong.
expect(torontoDay(Date.parse("2026-11-02T04:30:00Z"))).toBe("2026-11-01");
expect(torontoDay(Date.parse("2026-11-02T04:59:00Z"))).toBe("2026-11-01");
expect(torontoDay(Date.parse("2026-11-02T05:01:00Z"))).toBe("2026-11-02");
});
it("shifts dates as calendar strings across month, year and DST boundaries", () => {
expect(shiftDay("2026-11-02", -1)).toBe("2026-11-01");
expect(shiftDay("2026-11-01", 1)).toBe("2026-11-02");
expect(shiftDay("2026-03-01", -1)).toBe("2026-02-28");
expect(shiftDay("2027-01-01", -1)).toBe("2026-12-31");
expect(shiftDay("2027-03-14", 1)).toBe("2027-03-15");
});
it.each(["2026-11-15", "2027-03-20", "2026-10-04", "2027-01-05"])(
"windowDays(%s) is 30 consecutive unique dates ending today",
(today) => {
const days = windowDays(today);
expect(days).toHaveLength(WINDOW_DAYS);
expect(new Set(days).size).toBe(WINDOW_DAYS);
expect(days[days.length - 1]).toBe(today);
for (let i = 1; i < days.length; i++) expect(shiftDay(days[i - 1], 1)).toBe(days[i]);
},
);
it("aggregate puts 23:59 and 00:01 lines on two days around 2026-11-01", async () => {
writeTranscript(
"p/s.jsonl",
[
line({ ts: "2026-11-01T03:59:00Z", output: 1 }), // Oct 31 23:59 EDT
line({ ts: "2026-11-01T04:01:00Z", output: 10 }), // Nov 1 00:01 EDT
line({ ts: "2026-11-02T04:59:00Z", output: 100 }), // Nov 1 23:59 EST
line({ ts: "2026-11-02T05:01:00Z", output: 1000 }), // Nov 2 00:01 EST
],
"2026-11-02T12:00:00Z",
);
const r = await aggregate({ root: tmpDir, today: "2026-11-02", homedir: HOME });
expect(dayOf(r, "2026-10-31").models["claude-opus-5-5"].output).toBe(1);
expect(dayOf(r, "2026-11-01").models["claude-opus-5-5"].output).toBe(110);
expect(dayOf(r, "2026-11-02").models["claude-opus-5-5"].output).toBe(1000);
expect(dayOf(r, "2026-11-01").models["claude-opus-5-5"].requests).toBe(2);
});
it("rejects a malformed today", async () => {
await expect(aggregate({ root: tmpDir, today: "2026-13-01" })).rejects.toThrow(/today/);
await expect(aggregate({ root: tmpDir, today: "04/10/2026" })).rejects.toThrow(/today/);
});
it("defaults today to the current Toronto day", async () => {
const r = await aggregate({ root: tmpDir, homedir: HOME });
expect(r.days[r.days.length - 1].date).toBe(torontoDay(Date.now()));
});
});
// --- Project mapping ----------------------------------------------------------
describe("mapProject", () => {
it("rule 1: <home>/claude-code/<a>/... -> <a>", () => {
expect(mapProject(`${HOME}/claude-code/simpl-resultat`, HOME)).toBe("simpl-resultat");
expect(mapProject(`${HOME}/claude-code/simpl-resultat/src/lib`, HOME)).toBe(
"simpl-resultat",
);
expect(
mapProject(`${HOME}/claude-code/vps-health-api/.claude/worktrees/agent-x`, HOME),
).toBe("vps-health-api");
// A doubled slash never yields an empty project name.
expect(mapProject(`${HOME}/claude-code//simpl-liste/app`, HOME)).toBe("simpl-liste");
expect(mapProject(`${HOME}//claude-code`, HOME)).toBe("(monorepo)");
});
it("rule 1: .spikes and .archive keep their second segment", () => {
expect(mapProject(`${HOME}/claude-code/.spikes/idea/sub`, HOME)).toBe(".spikes/idea");
expect(mapProject(`${HOME}/claude-code/.archive/medic`, HOME)).toBe(".archive/medic");
});
it("rule 2: <home>/claude-code exactly -> (monorepo)", () => {
expect(mapProject(`${HOME}/claude-code`, HOME)).toBe("(monorepo)");
expect(mapProject(`${HOME}/claude-code/`, HOME)).toBe("(monorepo)");
});
it("rule 3: /tmp/spike-defenseur-auto-* -> defenseurs", () => {
expect(mapProject("/tmp/spike-defenseur-auto-20260927", HOME)).toBe("defenseurs");
expect(mapProject("/tmp/spike-defenseur-auto-abc/repo", HOME)).toBe("defenseurs");
});
it("rule 4: /tmp/claude-<uid>/<home slug>-claude-code-<p>/... -> <p>", () => {
expect(
mapProject("/tmp/claude-1000/-home-tester-claude-code-vps-health-api/sess/scratchpad", HOME),
).toBe("vps-health-api");
expect(mapProject("/tmp/claude-1000/-home-tester-claude-code-roadtrip", HOME)).toBe(
"roadtrip",
);
});
it("rule 5: anything else -> (autre)", () => {
expect(mapProject("/home/tester/other", HOME)).toBe("(autre)");
expect(mapProject("/home/tester/claude-codex/x", HOME)).toBe("(autre)");
expect(mapProject("/tmp/claude-1000/-home-other-claude-code-x", HOME)).toBe("(autre)");
expect(mapProject("/tmp/spike-other", HOME)).toBe("(autre)");
expect(mapProject(undefined, HOME)).toBe("(autre)");
expect(mapProject("", HOME)).toBe("(autre)");
});
it("uses the injected homedir, not the real one", () => {
expect(mapProject("/home/max/claude-code/alpha", "/home/max")).toBe("alpha");
expect(mapProject("/home/max/claude-code/alpha", HOME)).toBe("(autre)");
});
});
// --- Fixture scan ---------------------------------------------------------------
describe("aggregate on the fixture tree", () => {
async function runFixture(extra = {}) {
fs.cpSync(FIXTURES, tmpDir, { recursive: true });
const session = path.join(tmpDir, "-home-tester-claude-code-alpha", "sess-1.jsonl");
const sub = path.join(
tmpDir,
"-home-tester-claude-code-alpha",
"sess-1",
"subagents",
"agent-a.jsonl",
);
setMtime(session, "2026-10-04T12:00:00Z");
setMtime(sub, "2026-10-04T12:00:00Z");
return aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME, ...extra });
}
it("counts a request duplicated across session and subagents/ once, at its largest output", async () => {
const r = await runFixture();
expect(dayOf(r, "2026-10-03").models).toEqual({
"claude-opus-5-5": {
input: 10,
output: 2048,
cacheCreation: 100,
cacheRead: 1000,
requests: 1,
},
});
});
it("ignores <synthetic>, user lines and truncated JSON without throwing", async () => {
const r = await runFixture();
const all = r.days.flatMap((d) => Object.keys(d.models));
expect(all).not.toContain("<synthetic>");
const outputs = r.days.flatMap((d) => Object.values(d.models).map((m) => m.output));
expect(outputs).not.toContain(7777); // synthetic
expect(outputs).not.toContain(999); // user line carrying a usage object
expect(dayOf(r, "2026-10-04").models).toEqual({
"claude-sonnet-5": { input: 5, output: 50, cacheCreation: 0, cacheRead: 200, requests: 1 },
});
expect(dayOf(r, "2026-10-02").models["claude-opus-5-5"].requests).toBe(1);
});
it("aggregates projects with work = input + output + cacheCreation", async () => {
const r = await runFixture();
expect(r.projects).toEqual([
{ name: "alpha", work: 10 + 2048 + 100 + 5 + 50, cacheRead: 1200, requests: 2 },
{ name: ".spikes/idea", work: 33, cacheRead: 0, requests: 1 },
]);
});
it("reports claudeCodeVersion from the most recent timestamp", async () => {
const r = await runFixture();
expect(r.claudeCodeVersion).toBe("2.1.283");
});
it("returns every window day as open when nothing is frozen", async () => {
const r = await runFixture();
expect(Object.keys(r.openDays)).toEqual(windowDays("2026-10-04"));
expect(r.openDays["2026-10-03"].projects).toEqual({
alpha: { work: 2158, cacheRead: 1000, requests: 1 },
});
expect(r.openDays["2026-09-20"]).toEqual({ models: {}, projects: {} });
});
});
// --- Series -------------------------------------------------------------------
describe("30-day series", () => {
it("always has 30 entries, empty days included, newest = today", async () => {
const r = await aggregate({ root: path.join(tmpDir, "missing"), today: "2026-10-04", homedir: HOME });
expect(r.days).toHaveLength(30);
expect(r.days[29].date).toBe("2026-10-04");
expect(r.days[0].date).toBe("2026-09-05");
for (const d of r.days) expect(d.models).toEqual({});
expect(r.projects).toEqual([]);
expect(r.claudeCodeVersion).toBeNull();
});
it("drops lines outside the window (older than 30 days, or after today)", async () => {
writeTranscript(
"p/s.jsonl",
[
line({ ts: "2026-09-05T04:00:00Z", output: 1 }), // Sep 5 00:00 Toronto, first day
line({ ts: "2026-09-05T03:59:00Z", output: 2 }), // Sep 4, outside
line({ ts: "2026-10-05T04:01:00Z", output: 4 }), // Oct 5, after today
line({ ts: "not a date", output: 8 }),
],
"2026-10-04T12:00:00Z",
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
const total = r.days.reduce(
(s, d) => s + Object.values(d.models).reduce((t, m) => t + m.output, 0),
0,
);
expect(total).toBe(1);
expect(dayOf(r, "2026-09-05").models["claude-opus-5-5"].output).toBe(1);
});
it("keeps 30 entries when the window straddles the 2026-11-01 DST change", async () => {
const r = await aggregate({ root: tmpDir, today: "2026-11-15", homedir: HOME });
expect(r.days.map((d) => d.date)).toEqual(windowDays("2026-11-15"));
expect(r.days.filter((d) => d.date === "2026-11-01")).toHaveLength(1);
});
});
// --- Top projects ---------------------------------------------------------------
describe("top 8 projects + (autres)", () => {
it("keeps the top 8 by work and aggregates the rest; sums match the window total", async () => {
const lines = [];
for (let i = 1; i <= 11; i++) {
lines.push(
line({
ts: "2026-10-03T15:00:00Z",
cwd: `${HOME}/claude-code/p${String(i).padStart(2, "0")}`,
input: i * 100,
output: i,
cacheCreation: 7,
cacheRead: 3,
}),
);
}
writeTranscript("p/s.jsonl", lines, "2026-10-04T12:00:00Z");
const frozenDays = {
"2026-10-01": {
models: { "claude-opus-5-5": { input: 50, output: 0, cacheCreation: 0, cacheRead: 9, requests: 1 } },
projects: { p01: { work: 50, cacheRead: 9, requests: 1 } },
},
};
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME, frozenDays });
const expectedTotal = 50 + [...Array(11)].reduce((s, _, k) => s + (k + 1) * 101 + 7, 0);
expect(r.projects).toHaveLength(9);
expect(r.projects[8].name).toBe(OTHER_PROJECTS);
expect(r.projects.slice(0, 8).map((p) => p.name)).toEqual([
"p11", "p10", "p09", "p08", "p07", "p06", "p05", "p04",
]);
// p01 = 101 + 7 + 50 frozen, p02 = 209, p03 = 310 -> (autres)
expect(r.projects[8]).toEqual({
name: OTHER_PROJECTS,
work: 158 + 209 + 310,
cacheRead: 3 * 3 + 9,
requests: 4,
});
expect(sumWork(r.projects)).toBe(expectedTotal);
const seriesWork = r.days.reduce(
(s, d) =>
s + Object.values(d.models).reduce((t, m) => t + m.input + m.output + m.cacheCreation, 0),
0,
);
expect(sumWork(r.projects)).toBe(seriesWork);
});
it("omits (autres) when 8 projects or fewer have volume", async () => {
const lines = [];
for (let i = 1; i <= 8; i++) {
lines.push(line({ ts: "2026-10-03T15:00:00Z", cwd: `${HOME}/claude-code/q${i}`, input: i }));
}
writeTranscript("p/s.jsonl", lines, "2026-10-04T12:00:00Z");
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(r.projects).toHaveLength(8);
expect(r.projects.map((p) => p.name)).not.toContain(OTHER_PROJECTS);
});
});
// --- Model cap ------------------------------------------------------------------
describe("at most 12 models per day", () => {
it("merges the least consuming models into `other`, totals preserved", async () => {
const lines = [];
for (let i = 1; i <= 14; i++) {
lines.push(
line({
ts: "2026-10-03T15:00:00Z",
model: `model-${String(i).padStart(2, "0")}`,
input: i * 10,
cacheRead: 1,
}),
);
}
writeTranscript("p/s.jsonl", lines, "2026-10-04T12:00:00Z");
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
const models = dayOf(r, "2026-10-03").models;
expect(Object.keys(models)).toHaveLength(MAX_MODELS_PER_DAY);
expect(models[OTHER_MODEL]).toEqual({
input: 10 + 20 + 30,
output: 3,
cacheCreation: 0,
cacheRead: 3,
requests: 3,
});
expect(models["model-03"]).toBeUndefined();
expect(models["model-04"]).toBeDefined();
// The open day keeps the raw truth: the cap is applied to the series only.
expect(Object.keys(r.openDays["2026-10-03"].models)).toHaveLength(14);
});
it("is idempotent and leaves a small day untouched", () => {
const small = { a: { input: 1, output: 0, cacheCreation: 0, cacheRead: 0, requests: 1 } };
expect(capModels(small)).toBe(small);
const big = {};
for (let i = 0; i < 20; i++) {
big[`m${i}`] = { input: i, output: 0, cacheCreation: 0, cacheRead: 0, requests: 1 };
}
const once = capModels(big);
expect(Object.keys(once)).toHaveLength(12);
expect(capModels(once)).toBe(once);
expect(Object.values(once).reduce((s, m) => s + m.requests, 0)).toBe(20);
});
});
// --- Frozen days and the scan threshold ----------------------------------------
describe("frozen days", () => {
// What the agent hands over: a contiguous frozen run from the window start
// through 2026-10-01, empty days included.
const FROZEN = {};
for (const day of windowDays("2026-10-04")) {
if (day > "2026-10-01") break;
FROZEN[day] = { models: {}, projects: {} };
}
FROZEN["2026-10-01"] = {
models: { "claude-opus-5-5": { input: 7, output: 7, cacheCreation: 7, cacheRead: 7, requests: 7 } },
projects: { alpha: { work: 21, cacheRead: 7, requests: 7 } },
};
it("opens no file whose mtime day is before the threshold (spy on file opening)", async () => {
// Threshold = oldest open day = day after the frozen run = 2026-10-02.
const old = writeTranscript(
"a/old.jsonl",
[line({ ts: "2026-10-02T15:00:00Z", output: 5 })], // would count if opened
"2026-09-20T12:00:00Z",
);
const justBefore = writeTranscript(
"a/sub/just-before.jsonl",
[line({ ts: "2026-10-02T15:00:00Z", output: 5 })],
"2026-10-02T03:30:00Z", // Oct 1 23:30 Toronto
);
const atThreshold = writeTranscript(
"a/at-threshold.jsonl",
[line({ ts: "2026-10-02T03:00:00Z", output: 1 })], // Oct 1 23:00 -> frozen, ignored
"2026-10-02T04:30:00Z", // Oct 2 00:30 Toronto
);
const recent = writeTranscript(
"b/subagents/recent.jsonl",
[line({ ts: "2026-10-03T15:00:00Z", output: 100 })],
"2026-10-04T12:00:00Z",
);
const opened = [];
const openFile = (file, options) => {
opened.push(file);
return fs.createReadStream(file, options);
};
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
frozenDays: FROZEN,
openFile,
});
expect(opened.sort()).toEqual([atThreshold, recent].sort());
expect(opened).not.toContain(old);
expect(opened).not.toContain(justBefore);
expect(dayOf(r, "2026-10-02").models).toEqual({});
expect(dayOf(r, "2026-10-03").models["claude-opus-5-5"].output).toBe(100);
});
it("the default opener goes through fs.createReadStream, and skips old files too", async () => {
const old = writeTranscript(
"a/old.jsonl",
[line({ ts: "2026-10-02T15:00:00Z" })],
"2026-09-20T12:00:00Z",
);
const recent = writeTranscript(
"a/recent.jsonl",
[line({ ts: "2026-10-03T15:00:00Z" })],
"2026-10-04T12:00:00Z",
);
const spy = vi.spyOn(fs, "createReadStream");
await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME, frozenDays: FROZEN });
const paths = spy.mock.calls.map((c) => c[0]);
expect(paths).toContain(recent);
expect(paths).not.toContain(old);
});
it("without frozen days, still skips files last written before the window", async () => {
const ancient = writeTranscript(
"a/ancient.jsonl",
[line({ ts: "2026-08-01T15:00:00Z" })],
"2026-08-01T16:00:00Z",
);
const inWindow = writeTranscript(
"a/in-window.jsonl",
[line({ ts: "2026-09-05T15:00:00Z" })],
"2026-09-05T16:00:00Z",
);
const opened = [];
await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
openFile: (f, o) => {
opened.push(f);
return fs.createReadStream(f, o);
},
});
expect(opened).toEqual([inWindow]);
expect(opened).not.toContain(ancient);
});
it("serves frozen days verbatim, never recomputes them, and leaves them out of openDays", async () => {
writeTranscript(
"a/recent.jsonl",
[
line({ ts: "2026-10-01T15:00:00Z", output: 999 }), // frozen day: ignored
line({ ts: "2026-10-03T15:00:00Z", output: 3 }),
],
"2026-10-04T12:00:00Z",
);
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
frozenDays: FROZEN,
});
expect(dayOf(r, "2026-10-01").models).toEqual(FROZEN["2026-10-01"].models);
expect(dayOf(r, "2026-09-30").models).toEqual({});
expect(r.openDays["2026-10-01"]).toBeUndefined();
expect(r.openDays["2026-09-30"]).toBeUndefined();
expect(Object.keys(r.openDays)).toEqual(["2026-10-02", "2026-10-03", "2026-10-04"]);
expect(r.projects).toEqual([{ name: "alpha", work: 21 + 1 + 3, cacheRead: 7, requests: 8 }]);
});
it("a frozen day in the future never hides an open day's files", async () => {
const file = writeTranscript(
"a/open.jsonl",
[line({ ts: "2026-10-03T15:00:00Z", output: 3 })],
"2026-10-03T16:00:00Z",
);
const opened = [];
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
frozenDays: { "2026-11-15": { models: {}, projects: {} } },
openFile: (f, o) => {
opened.push(f);
return fs.createReadStream(f, o);
},
});
expect(opened).toEqual([file]);
expect(r.openDays["2026-10-03"].models["claude-opus-5-5"].output).toBe(3);
});
it("a gap in the frozen run keeps the open day before the gap scanned", async () => {
// 10-03 open, 10-04 (today) frozen: the oldest open day sets the threshold.
const file = writeTranscript(
"a/open.jsonl",
[line({ ts: "2026-10-03T15:00:00Z", output: 3 })],
"2026-10-03T16:00:00Z",
);
const frozenDays = {};
for (const day of windowDays("2026-10-04")) {
if (day !== "2026-10-03") frozenDays[day] = { models: {}, projects: {} };
}
const opened = [];
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
frozenDays,
openFile: (f, o) => {
opened.push(f);
return fs.createReadStream(f, o);
},
});
expect(opened).toEqual([file]);
expect(Object.keys(r.openDays)).toEqual(["2026-10-03"]);
expect(r.openDays["2026-10-03"].models["claude-opus-5-5"].output).toBe(3);
});
it("every day frozen: no file is opened", async () => {
writeTranscript("a/x.jsonl", [line({ ts: "2026-10-04T15:00:00Z" })], "2026-10-04T16:00:00Z");
const frozenDays = {};
for (const day of windowDays("2026-10-04")) frozenDays[day] = { models: {}, projects: {} };
const opened = [];
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
frozenDays,
openFile: (f, o) => {
opened.push(f);
return fs.createReadStream(f, o);
},
});
expect(opened).toEqual([]);
expect(Object.keys(r.openDays)).toEqual([]);
expect(r.days).toHaveLength(WINDOW_DAYS);
});
it("re-reads frozen counters defensively (bad numbers -> 0, junk ignored)", async () => {
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
frozenDays: {
"2026-10-01": {
models: {
"claude-opus-5-5": { input: -1, output: 1.5, cacheCreation: "9", cacheRead: 4, requests: 1 },
"Bad Key!": { input: 2, output: 0, cacheCreation: 0, cacheRead: 0, requests: 1 },
junk: null,
},
projects: { alpha: { work: Number.MAX_SAFE_INTEGER + 2, cacheRead: 1, requests: 1 } },
},
"not-a-day": { models: { x: { input: 1 } } },
},
});
expect(dayOf(r, "2026-10-01").models).toEqual({
"claude-opus-5-5": { input: 0, output: 0, cacheCreation: 0, cacheRead: 4, requests: 1 },
other: { input: 2, output: 0, cacheCreation: 0, cacheRead: 0, requests: 1 },
});
expect(r.projects).toEqual([{ name: "alpha", work: 0, cacheRead: 1, requests: 1 }]);
expect(Object.keys(r.openDays)).toHaveLength(29);
});
});
// --- Robustness -----------------------------------------------------------------
describe("robustness", () => {
it("counts a line missing its identifiers as its own request", async () => {
const a = line({ ts: "2026-10-03T15:00:00Z", output: 2 });
delete a.requestId;
const b = JSON.parse(JSON.stringify(a));
writeTranscript("p/s.jsonl", [a, b], "2026-10-04T12:00:00Z");
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(dayOf(r, "2026-10-03").models["claude-opus-5-5"].requests).toBe(2);
});
it("ignores non-.jsonl files and survives an unreadable transcript", async () => {
writeTranscript("p/notes.txt", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
writeTranscript(
"p/good.jsonl",
[line({ ts: "2026-10-03T15:00:00Z", output: 4 })],
"2026-10-04T12:00:00Z",
);
const bad = writeTranscript("p/bad.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
// The bad transcript's stream fails with ENOENT, as when Claude Code
// removes a file between the directory walk and the open.
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
openFile: (f, o) =>
fs.createReadStream(f === bad ? path.join(tmpDir, "does-not-exist") : f, o),
});
expect(dayOf(r, "2026-10-03").models["claude-opus-5-5"]).toEqual({
input: 1,
output: 4,
cacheCreation: 0,
cacheRead: 0,
requests: 1,
});
});
it("folds a model name outside the server key pattern into `other`", async () => {
writeTranscript(
"p/s.jsonl",
[line({ ts: "2026-10-03T15:00:00Z", model: "Weird Model/1" })],
"2026-10-04T12:00:00Z",
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(Object.keys(dayOf(r, "2026-10-03").models)).toEqual([OTHER_MODEL]);
});
it("reads no network module and writes nothing", () => {
const src = fs.readFileSync(
path.join(__dirname, "..", "agent", "claude-usage", "aggregate.js"),
"utf-8",
);
const requires = [...src.matchAll(/require\("([^"]+)"\)/g)].map((m) => m[1]).sort();
expect(requires).toEqual(["node:fs", "node:os", "node:path", "node:readline"]);
expect(src).not.toMatch(
/fs\.(?:promises\.)?(?:write|append|unlink|rename|mkdir|rm|copy|cp|symlink|chmod|chown|utimes|truncate|createWriteStream)/,
);
});
});
// --- Names that collide with Object.prototype ------------------------------------
describe("prototype-shaped names", () => {
const PROTO_KEYS = ["work", "cacheRead", "requests", "input", "output", "cacheCreation"];
function prototypeIsClean() {
for (const k of PROTO_KEYS) {
expect(Object.prototype.hasOwnProperty.call(Object.prototype, k)).toBe(false);
expect(Object.prototype.hasOwnProperty.call(Object, k)).toBe(false);
}
}
it("projects named __proto__ and constructor are counted, Object.prototype untouched", async () => {
writeTranscript(
"a/x.jsonl",
[
line({ ts: "2026-10-03T15:00:00Z", cwd: `${HOME}/claude-code/__proto__`, output: 5 }),
line({ ts: "2026-10-03T16:00:00Z", cwd: `${HOME}/claude-code/constructor`, output: 7 }),
line({ ts: "2026-10-03T17:00:00Z", model: "constructor", output: 2 }),
],
"2026-10-04T12:00:00Z",
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
prototypeIsClean();
const byName = Object.fromEntries(r.projects.map((p) => [p.name, p]));
expect(Object.keys(byName).sort()).toEqual(["__proto__", "alpha", "constructor"].sort());
expect(Object.getOwnPropertyDescriptor(byName, "__proto__").value.work).toBe(1 + 5);
expect(byName.constructor.work).toBe(1 + 7);
expect(dayOf(r, "2026-10-03").models.constructor.output).toBe(2);
expect(JSON.parse(JSON.stringify(r.days)).length).toBe(WINDOW_DAYS);
});
it("a __proto__ key in a frozen day stays a plain project", async () => {
const frozenDays = JSON.parse(
'{"2026-10-01":{"models":{},"projects":{"__proto__":{"work":4,"cacheRead":0,"requests":1},"constructor":{"work":6,"cacheRead":0,"requests":1}}}}',
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME, frozenDays });
prototypeIsClean();
expect(r.projects.map((p) => [p.name, p.work]).sort()).toEqual(
[
["__proto__", 4],
["constructor", 6],
].sort(),
);
});
});