vps-health-api/__tests__/agent-claude-usage.test.js
le king fu f694677a1a
All checks were successful
CI / verification (pull_request) Successful in 29s
CI / verification (push) Successful in 30s
test(agent): pin ENOENT as a read error below the root, note the subagent-copy limit
Review of #56: making ENOENT an empty scan on every directory, not only the
root, left the suite green. A vanished subdirectory is now a pinned read
error. The header also names the other known limit of the midnight fix:
the mtime threshold may skip a subagent transcript holding the only copy
of a straddling request's earlier line.

Refs #54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:53:27 -04:00

1955 lines
81 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);
});
// #54: a request whose lines straddle midnight. The day freezes at D+2, so
// the beat that froze it saw every line of the request and gave it to the
// day of its best line. Later beats must make the same choice, not count
// the request again on the open day once the frozen line is filtered out.
// Toronto is UTC-4 here: 03:59Z is Oct 1 23:59, 04:01Z is Oct 2 00:01.
it("a request straddling midnight whose best line sits on a frozen day is not counted again on the open day", async () => {
writeTranscript(
"a/straddle.jsonl",
[
line({ ts: "2026-10-02T03:59:00Z", id: "msg_mid", req: "req_mid", output: 5 }),
line({ ts: "2026-10-02T04:01:00Z", id: "msg_mid", req: "req_mid", output: 5 }),
],
"2026-10-02T12:00:00Z",
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME, frozenDays: FROZEN });
expect(dayOf(r, "2026-10-02").models).toEqual({});
expect(r.openDays["2026-10-02"].models).toEqual({});
// The frozen day is served as it was counted, untouched.
expect(dayOf(r, "2026-10-01").models["claude-opus-5-5"].requests).toBe(7);
});
it("a request straddling midnight whose best line sits on the open day is counted once there", async () => {
writeTranscript(
"a/straddle.jsonl",
[
line({ ts: "2026-10-02T03:59:00Z", id: "msg_mid", req: "req_mid", output: 1 }),
line({ ts: "2026-10-02T04:01:00Z", id: "msg_mid", req: "req_mid", output: 5 }),
],
"2026-10-02T12:00:00Z",
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME, frozenDays: FROZEN });
expect(dayOf(r, "2026-10-02").models["claude-opus-5-5"]).toEqual({
input: 1,
output: 5,
cacheCreation: 0,
cacheRead: 0,
requests: 1,
});
});
it("on equal output the earliest line wins, whatever the order the files are read in", async () => {
// Sorted read order puts the later line first. The beat that freezes a day
// and the beats after it do not read the same files (the threshold moves),
// so the winner must not depend on the read order.
const later = writeTranscript(
"a/a-first.jsonl",
[line({ ts: "2026-10-02T04:01:00Z", id: "msg_tie", req: "req_tie", output: 5 })],
"2026-10-02T12:00:00Z",
);
const earlier = writeTranscript(
"a/b-second.jsonl",
[line({ ts: "2026-10-02T03:59:00Z", id: "msg_tie", req: "req_tie", output: 5 })],
"2026-10-02T12:00:00Z",
);
const opened = [];
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
openFile: (f, o) => {
opened.push(f);
return fs.createReadStream(f, o);
},
});
expect(opened).toEqual([later, earlier]);
expect(dayOf(r, "2026-10-01").models["claude-opus-5-5"].requests).toBe(1);
expect(dayOf(r, "2026-10-02").models).toEqual({});
});
});
// --- 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,
});
});
// #54: an unreadable transcript, directory or stat is counted, so that the
// agent freezes nothing on that beat.
it("counts a stream error as a read error", async () => {
writeTranscript("p/good.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
const bad = writeTranscript("p/bad.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
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(r.readErrors).toBe(1);
});
it("counts an opener that throws as a read error", async () => {
writeTranscript("p/bad.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
const r = await aggregate({
root: tmpDir,
today: "2026-10-04",
homedir: HOME,
openFile: () => {
throw Object.assign(new Error("denied"), { code: "EACCES" });
},
});
expect(r.readErrors).toBe(1);
});
it("counts an unreadable subdirectory as a read error, and skips its files", async () => {
writeTranscript("p/good.jsonl", [line({ ts: "2026-10-03T15:00:00Z", output: 4 })], "2026-10-04T12:00:00Z");
writeTranscript("q/hidden.jsonl", [line({ ts: "2026-10-03T15:00:00Z", output: 9 })], "2026-10-04T12:00:00Z");
const badDir = path.join(tmpDir, "q");
// A spy rather than chmod 000: the CI runs as root, which reads anyway.
const realReaddir = fs.promises.readdir;
vi.spyOn(fs.promises, "readdir").mockImplementation((dir, opts) =>
dir === badDir
? Promise.reject(Object.assign(new Error("denied"), { code: "EACCES" }))
: realReaddir.call(fs.promises, dir, opts),
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(r.readErrors).toBe(1);
expect(dayOf(r, "2026-10-03").models["claude-opus-5-5"].output).toBe(4);
});
it("counts a subdirectory gone between the walk and its listing (ENOENT) as a read error", async () => {
// ENOENT is only an empty scan on the root itself: a project directory that
// vanishes mid-walk may have held lines of a day about to be frozen.
writeTranscript("p/good.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
writeTranscript("q/gone.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
const goneDir = path.join(tmpDir, "q");
const realReaddir = fs.promises.readdir;
vi.spyOn(fs.promises, "readdir").mockImplementation((dir, opts) =>
dir === goneDir
? Promise.reject(Object.assign(new Error("gone"), { code: "ENOENT" }))
: realReaddir.call(fs.promises, dir, opts),
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(r.readErrors).toBe(1);
});
it("counts a failed stat as a read error, ENOENT included", async () => {
writeTranscript("p/good.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
const gone = writeTranscript("p/gone.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
const realStat = fs.promises.stat;
vi.spyOn(fs.promises, "stat").mockImplementation((file, opts) =>
file === gone
? Promise.reject(Object.assign(new Error("gone"), { code: "ENOENT" }))
: realStat.call(fs.promises, file, opts),
);
const r = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(r.readErrors).toBe(1);
});
it("a missing root is an empty scan, not a read error; a clean scan has none", async () => {
const missing = await aggregate({ root: path.join(tmpDir, "absent"), today: "2026-10-04", homedir: HOME });
expect(missing.readErrors).toBe(0);
writeTranscript("p/good.jsonl", [line({ ts: "2026-10-03T15:00:00Z" })], "2026-10-04T12:00:00Z");
const clean = await aggregate({ root: tmpDir, today: "2026-10-04", homedir: HOME });
expect(clean.readErrors).toBe(0);
});
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(),
);
});
});
// ============================================================================
// The agent around the aggregation (issue #50): plan limits, local cache,
// snapshot assembly and delivery, cron wrapper.
//
// Nothing here touches the real ~/.claude or ~/.local, nor the network: every
// path (home, credentials, state dir, transcripts root) and `fetch` itself are
// injected, and the spawned processes run with HOME pointed at a temp dir.
// The two tokens below are checked, fragment by fragment, against every output
// that would reach `logger -t claude-usage`, as in agent.test.js.
// ============================================================================
const { execFile, spawn } = require("node:child_process");
const { promisify } = require("node:util");
const execFileAsync = promisify(execFile);
const {
fetchLimits,
parseUsage,
USAGE_URL,
DETAILS,
} = require("../agent/claude-usage/limits.js");
const {
loadState,
saveState,
nextState,
limitsForSnapshot,
emptyState,
stateFilePath,
defaultStateDir,
RETENTION_DAYS,
} = require("../agent/claude-usage/state.js");
const {
main: agentMain,
readConfig: agentReadConfig,
MAX_BODY_BYTES,
EXIT_OK,
EXIT_CONFIG,
EXIT_PUSH,
} = require("../agent/claude-usage/push-claude-usage.js");
const { sanitizeClaudeUsage } = require("../claude-usage.js");
const AGENT_DIR = path.join(__dirname, "..", "agent", "claude-usage");
const PUSH_PATH = path.join(AGENT_DIR, "push-claude-usage.js");
const WRAPPER_PATH = path.join(AGENT_DIR, "run-claude-usage.sh");
const USAGE_FIXTURE = path.join(__dirname, "fixtures", "claude-usage", "oauth-usage.json");
// Mixed letters and digits, no run of five digits, so neither a port nor a
// timestamp can ever look like a leak.
const OAUTH_TOKEN = "sk9ant8oat2q7w4e1r6t3y5u";
const INGEST_TOKEN = "c4u9g2i7n3g8e5s1t6k0z";
const API_URL = "https://health.example.test";
const NOW = Date.parse("2026-10-04T16:00:00.000Z"); // Toronto 2026-10-04 12:00
const FUTURE = NOW + 3600 * 1000;
const LIMIT_STATUS_ENUM = ["ok", "token_expired", "http_error", "unavailable", "disabled"];
const DETAIL_ENUM = Object.values(DETAILS);
function usageFixture() {
return JSON.parse(fs.readFileSync(USAGE_FIXTURE, "utf-8"));
}
function fragments(token, min = 5) {
const out = [];
for (let len = min; len <= token.length; len++) {
for (let i = 0; i + len <= token.length; i++) out.push(token.slice(i, i + len));
}
return out;
}
function expectNoLeak(text) {
const haystack = String(text);
for (const token of [OAUTH_TOKEN, INGEST_TOKEN]) {
const leaked = fragments(token).filter((f) => haystack.includes(f));
const worst = leaked.reduce((a, b) => (b.length > a.length ? b : a), "");
expect(leaked.length, `token leaked — longest fragment found: "${worst}"`).toBe(0);
}
}
function expectDetailInEnum(limits) {
expect(LIMIT_STATUS_ENUM).toContain(limits.status);
if (limits.detail !== undefined) {
const ok = DETAIL_ENUM.includes(limits.detail) || /^http_[1-5]\d\d$/.test(limits.detail);
expect(ok, `detail outside the enum: ${limits.detail}`).toBe(true);
}
}
// A fetch double that records every call. `handler(url, init)` returns a
// Response (or a promise of one) or throws.
function makeFetch(handler) {
const calls = [];
const fn = async (url, init = {}) => {
calls.push({ url: String(url), init });
return handler(String(url), init);
};
fn.calls = calls;
return fn;
}
function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
// Anthropic answers with `usageBody`, the health API with `pushStatus`.
function routedFetch({ usage = () => jsonResponse(usageFixture()), push = () => new Response(null, { status: 204 }) } = {}) {
return makeFetch((url, init) => (url === USAGE_URL ? usage(url, init) : push(url, init)));
}
// A fake home: <tmp>/home with .claude/projects (optionally the fixture tree)
// and .claude/.credentials.json (optionally).
function makeHome({ fixtures = false, credentials } = {}) {
const home = path.join(tmpDir, "home");
fs.mkdirSync(path.join(home, ".claude", "projects"), { recursive: true });
if (fixtures) {
fs.cpSync(FIXTURES, path.join(home, ".claude", "projects"), { recursive: true });
}
const credsPath = path.join(home, ".claude", ".credentials.json");
if (credentials !== undefined) {
fs.writeFileSync(
credsPath,
typeof credentials === "string" ? credentials : JSON.stringify(credentials),
{ mode: 0o600 },
);
}
return { home, credsPath, stateFile: stateFilePath(defaultStateDir(home)) };
}
function goodCredentials(expiresAt = FUTURE) {
return {
claudeAiOauth: {
accessToken: OAUTH_TOKEN,
refreshToken: "rt9single8use7only6",
expiresAt,
scopes: ["user:inference"],
},
};
}
function agentEnv(extra = {}) {
return { CLAUDE_USAGE_API_URL: API_URL, CLAUDE_USAGE_INGEST_TOKEN: INGEST_TOKEN, ...extra };
}
// Runs main() in process with everything injected; collects the output.
async function runAgent({ home, fetch, env = agentEnv(), argv = [], now = NOW, ...deps }) {
const out = [];
const err = [];
const code = await agentMain(argv, env, {
home,
fetch,
now,
log: (l) => out.push(l),
logError: (l) => err.push(l),
...deps,
});
return { code, stdout: out.join("\n"), stderr: err.join("\n") };
}
function pushCalls(fetch) {
return fetch.calls.filter((c) => c.url !== USAGE_URL);
}
// --- limits: parsing ----------------------------------------------------------
describe("limits — the endpoint's answer", () => {
const limitsOpts = (extra = {}) => ({
env: {},
credentialsPath: "/unused",
readFile: () => JSON.stringify(goodCredentials()),
claudeCodeVersion: "2.1.283",
now: NOW,
...extra,
});
it("maps the anonymized limits[] to items, weekly_scoped carrying model Fable", async () => {
const fetch = makeFetch(() => jsonResponse(usageFixture()));
const r = await fetchLimits(limitsOpts({ fetch }));
expect(r).toEqual({
status: "ok",
fetchedAt: new Date(NOW).toISOString(),
items: [
{ kind: "session", group: "session", percent: 1, severity: "normal", resetsAt: "2026-09-27T21:30:00Z", model: null },
{ kind: "weekly_all", group: "weekly", percent: 50, severity: "normal", resetsAt: "2026-10-01T07:00:00Z", model: null },
{ kind: "weekly_scoped", group: "weekly", percent: 63, severity: "normal", resetsAt: "2026-10-01T07:00:00Z", model: "Fable" },
],
});
expect(fetch.calls).toHaveLength(1);
});
it.each([
["absent", (b) => delete b.limits],
["empty", (b) => (b.limits = [])],
["made of junk entries", (b) => (b.limits = [null, 3, "x", { percent: 9 }])],
])("falls back to five_hour / seven_day when limits[] is %s", (_label, mutate) => {
const body = usageFixture();
mutate(body);
expect(parseUsage(body)).toEqual([
{ kind: "session", group: "session", percent: 1, resetsAt: "2026-09-27T21:30:00.512345+00:00", model: null },
{ kind: "weekly_all", group: "weekly", percent: 50, resetsAt: "2026-10-01T07:00:00.512345+00:00", model: null },
]);
});
it("an all-null answer is unavailable/bad_response", async () => {
const body = Object.fromEntries(Object.keys(usageFixture()).map((k) => [k, null]));
const fetch = makeFetch(() => jsonResponse(body));
const r = await fetchLimits(limitsOpts({ fetch }));
expect(r).toEqual({ status: "unavailable", detail: "bad_response", fetchedAt: null, items: [] });
});
it("rounds and clamps percent to an integer 0-100, drops a non-number, normalizes resetsAt", () => {
const items = parseUsage({
limits: [
{ kind: "a", group: "g", percent: 62.6, resets_at: 1790000000 },
{ kind: "b", group: "g", percent: 130, resets_at: "not a date" },
{ kind: "c", group: "g", percent: -3, resets_at: "Thu, 01 Oct 2026 07:00:00 GMT" },
{ kind: "d", percent: "50", scope: { model: { display_name: 42 } } },
],
});
expect(items.map((i) => i.percent)).toEqual([63, 100, 0, undefined]);
expect("percent" in items[3]).toBe(false);
expect(items[3].group).toBe("d");
expect(items[3].model).toBeNull();
expect(items[0].resetsAt).toBe(new Date(1790000000 * 1000).toISOString());
expect(items[1].resetsAt).toBeNull();
expect(items[2].resetsAt).toBe("2026-10-01T07:00:00.000Z");
});
it("keeps at most 12 items and no upstream key outside the whitelist", () => {
const limits = Array.from({ length: 20 }, (_, i) => ({
kind: `k${i}`, group: "g", percent: 1, severity: "normal", resets_at: null,
scope: null, is_active: true, __proto__x: "junk", token: OAUTH_TOKEN,
}));
const items = parseUsage({ limits });
expect(items).toHaveLength(12);
for (const it of items) {
expect(Object.keys(it).sort()).toEqual(["group", "kind", "model", "percent", "resetsAt", "severity"]);
}
expectNoLeak(JSON.stringify(items));
});
});
describe("limits — the response size is counted in bytes, while reading (#54)", () => {
const MAX = 256 * 1024;
const limitsOpts = (fetch) => ({
env: {},
credentialsPath: "/unused",
readFile: () => JSON.stringify(goodCredentials()),
claudeCodeVersion: "2.1.283",
now: NOW,
fetch,
});
// A valid answer padded to exactly `bytes` UTF-8 bytes with `pad`.
function padded(bytes, pad) {
const base = JSON.stringify({ ...usageFixture(), pad: "" });
let room = bytes - Buffer.byteLength(base);
const unit = Buffer.byteLength(pad);
let fill = "a".repeat(room % unit);
fill += pad.repeat(Math.floor(room / unit));
const body = JSON.stringify({ ...usageFixture(), pad: fill });
expect(Buffer.byteLength(body)).toBe(bytes);
return body;
}
it("accepts a body of exactly 256 KiB", async () => {
const body = padded(MAX, "a");
const r = await fetchLimits(limitsOpts(makeFetch(() => new Response(body, { status: 200 }))));
expect(r.status).toBe("ok");
});
it("refuses 256 KiB + 1 byte of two-byte characters, fewer than 256 Ki characters", async () => {
const body = padded(MAX + 1, "\u00e9");
expect(body.length).toBeLessThan(MAX);
const r = await fetchLimits(limitsOpts(makeFetch(() => new Response(body, { status: 200 }))));
expect(r).toEqual({ status: "unavailable", detail: DETAILS.BAD_RESPONSE, fetchedAt: null, items: [] });
});
it("stops reading an endless body and cancels it", async () => {
let pulls = 0;
let cancelled = false;
const chunk = new Uint8Array(64 * 1024).fill(0x20);
const stream = new ReadableStream({
pull(controller) {
pulls += 1;
controller.enqueue(chunk);
},
cancel() {
cancelled = true;
},
});
const r = await fetchLimits(limitsOpts(makeFetch(() => new Response(stream, { status: 200 }))));
expect(r).toEqual({ status: "unavailable", detail: DETAILS.BAD_RESPONSE, fetchedAt: null, items: [] });
expect(cancelled).toBe(true);
expect(pulls).toBeLessThan(10); // 5 chunks cross the ceiling
});
});
// --- limits: when no call is made ----------------------------------------------
describe("limits — cases that never call the endpoint", () => {
it.each([
["CLAUDE_USAGE_LIMITS=off", { env: { CLAUDE_USAGE_LIMITS: "off" } }, { status: "disabled" }],
["CLAUDE_USAGE_LIMITS=OFF", { env: { CLAUDE_USAGE_LIMITS: " OFF " } }, { status: "disabled" }],
["an expired token", { creds: JSON.stringify(goodCredentials(NOW - 1)) }, { status: "token_expired" }],
["truncated credentials (rewrite in progress)", { creds: '{"claudeAiOauth":{"accessToken":"sk9ant8o' }, { status: "unavailable", detail: "parse_error" }],
["absent credentials", { missing: true }, { status: "unavailable", detail: "no_credentials" }],
["credentials without an access token", { creds: '{"claudeAiOauth":{}}' }, { status: "unavailable", detail: "no_credentials" }],
["an unreadable credentials file", { unreadable: true }, { status: "unavailable", detail: "parse_error" }],
["no Claude Code version", { version: null }, { status: "unavailable", detail: "no_version" }],
])("%s", async (_label, c, expected) => {
const fetch = makeFetch(() => jsonResponse(usageFixture()));
const readFile = () => {
if (c.missing) throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
if (c.unreadable) throw Object.assign(new Error("EACCES"), { code: "EACCES" });
return c.creds || JSON.stringify(goodCredentials());
};
const r = await fetchLimits({
env: c.env || {},
credentialsPath: "/unused",
readFile,
fetch,
claudeCodeVersion: c.version === null ? null : "2.1.283",
now: NOW,
});
expect(fetch.calls).toHaveLength(0);
expect(r).toEqual({ ...expected, fetchedAt: null, items: [] });
expectNoLeak(JSON.stringify(r));
});
it("CLAUDE_USAGE_LIMITS=off does not even read the credentials", async () => {
const readFile = vi.fn(() => JSON.stringify(goodCredentials()));
const fetch = makeFetch(() => jsonResponse(usageFixture()));
await fetchLimits({ env: { CLAUDE_USAGE_LIMITS: "off" }, readFile, fetch, claudeCodeVersion: "2.1.283" });
expect(readFile).not.toHaveBeenCalled();
expect(fetch.calls).toHaveLength(0);
});
});
// --- limits: the request and its failures --------------------------------------
describe("limits — the request", () => {
const base = (fetch, env = {}) => ({
env,
credentialsPath: "/unused",
readFile: () => JSON.stringify(goodCredentials()),
fetch,
claudeCodeVersion: "2.1.283",
now: NOW,
});
it("sends the OAuth bearer to the pinned Anthropic URL only, redirects refused, whatever the environment says", async () => {
expect(USAGE_URL).toBe("https://api.anthropic.com/api/oauth/usage");
const fetch = makeFetch(() => jsonResponse(usageFixture()));
await fetchLimits(
base(fetch, {
ANTHROPIC_BASE_URL: "https://evil.example",
ANTHROPIC_API_URL: "https://evil.example",
CLAUDE_USAGE_API_URL: "https://evil.example",
CLAUDE_USAGE_LIMITS_URL: "https://evil.example",
CLAUDE_USAGE_LIMITS: "on",
}),
);
expect(fetch.calls).toHaveLength(1);
const { url, init } = fetch.calls[0];
expect(url).toBe("https://api.anthropic.com/api/oauth/usage");
expect(init.method).toBe("GET");
expect(init.redirect).toBe("error");
expect(init.signal).toBeInstanceOf(AbortSignal);
expect(init.headers).toMatchObject({
Authorization: `Bearer ${OAUTH_TOKEN}`,
"anthropic-beta": "oauth-2025-04-20",
"User-Agent": "claude-code/2.1.283",
});
});
it("limits.js reads no environment variable but CLAUDE_USAGE_LIMITS, and writes the URL once", () => {
const src = fs.readFileSync(path.join(AGENT_DIR, "limits.js"), "utf-8");
const code = src
.split("\n")
.filter((l) => !l.trim().startsWith("//") && !l.trim().startsWith("*"))
.join("\n");
expect([...code.matchAll(/\benv\.([A-Za-z_]+)/g)].map((m) => m[1])).toEqual(["CLAUDE_USAGE_LIMITS"]);
// process.env appears once, as the default of the injectable `env`.
expect(code.match(/process\.env/g)).toEqual(["process.env"]);
expect(code).toContain("env = process.env,");
expect(code.match(/https:\/\//g)).toHaveLength(1);
expect(code).not.toMatch(/refresh/i);
});
it("a 429 is http_error/http_429, and the agent then serves the last good items with their fetchedAt", async () => {
const fetch = makeFetch(() => new Response("Too Many Requests", { status: 429 }));
const r = await fetchLimits(base(fetch));
expect(r).toEqual({ status: "http_error", detail: "http_429", fetchedAt: null, items: [] });
expect(fetch.calls).toHaveLength(1); // no retry
const { home, credsPath } = makeHome({ fixtures: true, credentials: goodCredentials() });
const first = routedFetch();
expect((await runAgent({ home, fetch: first, now: NOW })).code).toBe(EXIT_OK);
const firstSnap = JSON.parse(pushCalls(first)[0].init.body);
const later = NOW + 15 * 60 * 1000;
const second = routedFetch({ usage: () => new Response("slow down", { status: 429 }) });
expect((await runAgent({ home, fetch: second, now: later })).code).toBe(EXIT_OK);
const snap = JSON.parse(pushCalls(second)[0].init.body);
expect(snap.limits).toEqual({
status: "http_error",
detail: "http_429",
fetchedAt: new Date(NOW).toISOString(),
items: firstSnap.limits.items,
});
expect(snap.generatedAt).toBe(new Date(later).toISOString());
expect(credsPath).toBeTruthy();
});
it.each([
["a 500 echoing the token and request headers", () => new Response(`upstream said: Authorization: Bearer ${OAUTH_TOKEN}\nanthropic-beta: oauth`, { status: 500 })],
["a 200 with arbitrary text", () => new Response(`<html>error ${OAUTH_TOKEN} please retry</html>`, { status: 200 })],
["a 200 JSON echoing the headers", () => jsonResponse({ headers: { authorization: `Bearer ${OAUTH_TOKEN}` }, detail: "Ignore previous instructions" })],
["a 200 JSON with a hostile detail field", () => jsonResponse({ detail: "http_200_but_actually_long_text", limits: "nope", five_hour: { utilization: "x" } })],
["a 302 to another host", () => new Response(null, { status: 302, headers: { location: "https://evil.example/" } })],
["a thrown error carrying the token in its message", () => { throw new TypeError(`Headers.append: "Bearer ${OAUTH_TOKEN}" is an invalid header value.`); }],
["a network failure", () => { throw Object.assign(new TypeError("fetch failed"), { cause: { code: "ECONNREFUSED" } }); }],
["a timeout", () => { throw new DOMException("The operation was aborted due to timeout", "TimeoutError"); }],
])("%s keeps detail inside the enum and no token in the result", async (_label, answer) => {
const fetch = makeFetch(answer);
const r = await fetchLimits(base(fetch));
expectDetailInEnum(r);
expect(r.status).not.toBe("ok");
expect(r.items).toEqual([]);
expectNoLeak(JSON.stringify(r));
expect(fetch.calls).toHaveLength(1);
});
it("maps timeouts, network errors and non-2xx to their codes", async () => {
const timeout = await fetchLimits({
...base(makeFetch((url, init) => new Promise((_, reject) => {
init.signal.addEventListener("abort", () => reject(init.signal.reason));
}))),
timeoutMs: 20,
});
expect(timeout).toMatchObject({ status: "unavailable", detail: "timeout" });
const network = await fetchLimits(base(makeFetch(() => { throw new TypeError("fetch failed"); })));
expect(network).toMatchObject({ status: "unavailable", detail: "network" });
const unauthorized = await fetchLimits(base(makeFetch(() => new Response("{}", { status: 401 }))));
expect(unauthorized).toMatchObject({ status: "http_error", detail: "http_401" });
});
});
// --- the credentials file ------------------------------------------------------
describe("the credentials file is never opened for writing", () => {
it("a full run only ever reads it, through fs.readFileSync without a flag", async () => {
const { home, credsPath } = makeHome({ fixtures: true, credentials: goodCredentials() });
const before = fs.readFileSync(credsPath, "utf-8");
const beforeStat = fs.statSync(credsPath);
const writers = [
"writeFileSync", "writeFile", "appendFileSync", "appendFile", "createWriteStream",
"renameSync", "rename", "copyFileSync", "copyFile", "cpSync", "rmSync", "rm",
"unlinkSync", "unlink", "truncateSync", "truncate", "chmodSync", "chmod",
"chownSync", "utimesSync", "symlinkSync", "linkSync", "openSync", "open",
];
const spies = writers.map((name) => vi.spyOn(fs, name));
const promiseWriters = ["writeFile", "appendFile", "open", "rename", "copyFile", "rm", "unlink", "truncate", "chmod"];
const promiseSpies = promiseWriters.map((name) => vi.spyOn(fs.promises, name));
const readSpy = vi.spyOn(fs, "readFileSync");
const fetch = routedFetch();
const { code } = await runAgent({ home, fetch });
expect(code).toBe(EXIT_OK);
expect(fetch.calls.filter((c) => c.url === USAGE_URL)).toHaveLength(1);
const credReads = readSpy.mock.calls.filter((c) => c[0] === credsPath);
expect(credReads).toHaveLength(1);
expect(credReads[0][1]).toBe("utf-8");
for (const spy of [...spies, ...promiseSpies]) {
for (const args of spy.mock.calls) {
const touches = args.some((a) => String(a) === credsPath || String(a).startsWith(`${credsPath}.`));
expect(touches, `${spy.getMockName()} touched the credentials file`).toBe(false);
}
}
// The state file, on the other hand, was written: the spies were live.
expect(spies[0].mock.calls.length + spies[6].mock.calls.length).toBeGreaterThan(0);
expect(fs.readFileSync(credsPath, "utf-8")).toBe(before);
expect(fs.statSync(credsPath).mtimeMs).toBe(beforeStat.mtimeMs);
});
});
// --- state -----------------------------------------------------------------------
describe("state — local cache", () => {
it.each([
["missing", null],
["truncated", '{"version":1,"frozenDays":{"2026-09-'],
["not an object", "[1,2,3]"],
["binary junk", "\u0000\u0001ÿ"],
])("a %s state.json gives the empty state", (_label, content) => {
const file = path.join(tmpDir, "state.json");
if (content !== null) fs.writeFileSync(file, content);
expect(loadState(file)).toEqual(emptyState());
});
it("keeps only well-formed frozen days and last good limits on reload", () => {
const file = path.join(tmpDir, "state.json");
fs.writeFileSync(
file,
JSON.stringify({
frozenDays: {
"2026-10-01": { models: { m: { input: 1 } }, projects: {} },
"2026-02-30": { models: {}, projects: {} },
"not-a-day": { models: {}, projects: {} },
"2026-09-30": "junk",
"2026-09-29": { models: [], projects: null },
},
lastGoodLimits: { fetchedAt: "garbage", items: [] },
}),
);
const s = loadState(file);
expect(Object.keys(s.frozenDays).sort()).toEqual(["2026-09-29", "2026-10-01"]);
expect(s.frozenDays["2026-09-29"]).toEqual({ models: {}, projects: {} });
expect(s.lastGoodLimits).toBeNull();
});
it("freezes every open day strictly before yesterday, empty ones included; yesterday and today stay open", () => {
const openDays = {
"2026-09-05": { models: {}, projects: {} },
"2026-10-01": { models: { m: { input: 1 } }, projects: { p: { work: 1 } } },
"2026-10-02": { models: {}, projects: {} },
"2026-10-03": { models: { m: { input: 2 } }, projects: {} },
"2026-10-04": { models: {}, projects: {} },
};
const s = nextState(emptyState(), { openDays, today: "2026-10-04", limits: { status: "disabled" } });
expect(Object.keys(s.frozenDays).sort()).toEqual(["2026-09-05", "2026-10-01", "2026-10-02"]);
expect(s.frozenDays["2026-10-02"]).toEqual({ models: {}, projects: {} });
expect(s.frozenDays["2026-10-01"]).toEqual(openDays["2026-10-01"]);
});
it(`prunes frozen days older than ${RETENTION_DAYS} days`, () => {
const state = {
...emptyState(),
frozenDays: {
"2026-08-29": { models: {}, projects: {} }, // today - 36
"2026-08-30": { models: {}, projects: {} }, // today - 35
"2026-09-20": { models: {}, projects: {} },
},
};
const s = nextState(state, { openDays: {}, today: "2026-10-04", limits: { status: "disabled" } });
expect(Object.keys(s.frozenDays).sort()).toEqual(["2026-08-30", "2026-09-20"]);
});
it("with freeze: false, keeps the frozen days (pruned) and freezes no new one", () => {
const state = {
...emptyState(),
frozenDays: {
"2026-08-29": { models: {}, projects: {} }, // pruned all the same
"2026-09-20": { models: {}, projects: {} },
},
};
const openDays = {
"2026-10-01": { models: { m: { input: 1 } }, projects: {} },
"2026-10-03": { models: {}, projects: {} },
};
const ok = { status: "ok", fetchedAt: "2026-10-04T16:00:00.000Z", items: [{ kind: "session" }] };
const s = nextState(state, { openDays, today: "2026-10-04", limits: ok, freeze: false });
expect(Object.keys(s.frozenDays)).toEqual(["2026-09-20"]);
expect(s.lastGoodLimits).toEqual({ fetchedAt: ok.fetchedAt, items: ok.items });
});
it("updates lastGoodLimits on `ok` only", () => {
const ok = { status: "ok", fetchedAt: "2026-10-04T16:00:00.000Z", items: [{ kind: "session" }] };
const s1 = nextState(emptyState(), { openDays: {}, today: "2026-10-04", limits: ok });
expect(s1.lastGoodLimits).toEqual({ fetchedAt: ok.fetchedAt, items: ok.items });
for (const failed of [
{ status: "http_error", detail: "http_429", fetchedAt: null, items: [] },
{ status: "token_expired", fetchedAt: null, items: [] },
{ status: "unavailable", detail: "timeout", fetchedAt: null, items: [] },
{ status: "disabled", fetchedAt: null, items: [] },
]) {
const s2 = nextState(s1, { openDays: {}, today: "2026-10-04", limits: failed });
expect(s2.lastGoodLimits).toEqual(s1.lastGoodLimits);
expect(limitsForSnapshot(failed, s2)).toEqual({
status: failed.status,
...(failed.detail ? { detail: failed.detail } : {}),
fetchedAt: ok.fetchedAt,
items: ok.items,
});
}
expect(limitsForSnapshot({ status: "token_expired", fetchedAt: null, items: [] }, emptyState())).toEqual({
status: "token_expired",
fetchedAt: null,
items: [],
});
});
it("writes atomically: mode 600, temp file suffixed with the pid in the same directory, then rename", () => {
const file = path.join(tmpDir, "deep", "state", "state.json");
const writeSpy = vi.spyOn(fs, "writeFileSync");
const renameSpy = vi.spyOn(fs, "renameSync");
saveState(file, { ...emptyState(), frozenDays: { "2026-10-01": { models: {}, projects: {} } } });
const tmp = `${file}.${process.pid}.tmp`;
expect(writeSpy.mock.calls.map((c) => c[0])).toEqual([tmp]);
expect(renameSpy.mock.calls).toEqual([[tmp, file]]);
expect(fs.existsSync(tmp)).toBe(false);
expect(fs.statSync(file).mode & 0o777).toBe(0o600);
expect(loadState(file).frozenDays).toEqual({ "2026-10-01": { models: {}, projects: {} } });
});
it("a temp file left at mode 644 by a killed run still ends in a mode 600 cache", () => {
// writeFileSync only applies `mode` when it creates the file (#54).
const file = path.join(tmpDir, "state", "state.json");
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.${process.pid}.tmp`;
fs.writeFileSync(tmp, "leftover");
fs.chmodSync(tmp, 0o644); // explicit: the umask could already give 600
expect(fs.statSync(tmp).mode & 0o777).toBe(0o644);
saveState(file, emptyState());
expect(fs.existsSync(tmp)).toBe(false);
expect(fs.statSync(file).mode & 0o777).toBe(0o600);
});
it("the default state dir is ~/.local/state/maximus-claude-usage", () => {
expect(stateFilePath(defaultStateDir("/home/u"))).toBe("/home/u/.local/state/maximus-claude-usage/state.json");
});
});
// --- push-claude-usage: one beat ----------------------------------------------
describe("push-claude-usage — one beat", () => {
it("two consecutive beats around an empty day: the empty day is frozen, the second beat opens no file before the threshold", async () => {
const { home, stateFile } = makeHome({ credentials: undefined });
const root = path.join(home, ".claude", "projects");
const projectDir = path.join(root, "-home-tester-claude-code-alpha");
fs.mkdirSync(projectDir, { recursive: true });
const write = (name, lines, mtimeIso) => {
const file = path.join(projectDir, name);
fs.writeFileSync(file, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`);
setMtime(file, mtimeIso);
return file;
};
// Lines on Oct 1 (Toronto 23:00), file last touched Oct 2 00:30 Toronto:
// Oct 2 itself has no line at all. Unfrozen, that empty day would hold the
// threshold on Oct 2 and this file would be re-read on every beat.
const old = write("old.jsonl", [line({ ts: "2026-10-02T03:00:00Z", output: 40 })], "2026-10-02T04:30:00Z");
const recent = write(
"recent.jsonl",
[line({ ts: "2026-10-03T15:00:00Z", output: 7 }), line({ ts: "2026-10-04T15:00:00Z", output: 9 })],
"2026-10-04T15:30:00Z",
);
const env = agentEnv({ CLAUDE_USAGE_LIMITS: "off" });
const firstOpened = [];
const fetch1 = routedFetch();
const r1 = await runAgent({
home, fetch: fetch1, env,
openFile: (f, o) => { firstOpened.push(f); return fs.createReadStream(f, o); },
});
expect(r1.code).toBe(EXIT_OK);
expect(firstOpened.sort()).toEqual([old, recent].sort());
const frozen = JSON.parse(fs.readFileSync(stateFile, "utf-8")).frozenDays;
expect(frozen["2026-10-02"]).toEqual({ models: {}, projects: {} });
expect(frozen["2026-10-01"].models["claude-opus-5-5"].output).toBe(40);
expect(frozen["2026-10-03"]).toBeUndefined(); // yesterday stays open
expect(frozen["2026-10-04"]).toBeUndefined();
expect(Object.keys(frozen)).toHaveLength(28); // the window minus yesterday and today
const secondOpened = [];
const fetch2 = routedFetch();
const r2 = await runAgent({
home, fetch: fetch2, env, now: NOW + 15 * 60 * 1000,
openFile: (f, o) => { secondOpened.push(f); return fs.createReadStream(f, o); },
});
expect(r2.code).toBe(EXIT_OK);
expect(secondOpened).toEqual([recent]);
const snap1 = JSON.parse(pushCalls(fetch1)[0].init.body);
const snap2 = JSON.parse(pushCalls(fetch2)[0].init.body);
expect(snap2.days).toEqual(snap1.days); // frozen days served as they were counted
expect(snap2.projects).toEqual(snap1.projects);
});
it("a beat with a read error freezes no day, still pushes, logs a count only; the next clean beat freezes", async () => {
const { home, stateFile } = makeHome({ credentials: undefined });
const projectDir = path.join(home, ".claude", "projects", "-home-tester-claude-code-alpha");
fs.mkdirSync(projectDir, { recursive: true });
const write = (name, lines) => {
const file = path.join(projectDir, name);
fs.writeFileSync(file, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`);
setMtime(file, "2026-10-04T15:30:00Z");
return file;
};
write("good.jsonl", [line({ ts: "2026-10-01T15:00:00Z", output: 3 })]);
const bad = write("secret-name.jsonl", [line({ ts: "2026-10-01T15:00:00Z", output: 8 })]);
const env = agentEnv({ CLAUDE_USAGE_LIMITS: "off" });
const fetch1 = routedFetch();
const r1 = await runAgent({
home, fetch: fetch1, env,
openFile: (f, o) => fs.createReadStream(f === bad ? path.join(projectDir, "missing.jsonl") : f, o),
});
expect(r1.code).toBe(EXIT_OK);
expect(pushCalls(fetch1)).toHaveLength(1);
expect(loadState(stateFile).frozenDays).toEqual({});
expect(r1.stderr).toContain("read_errors=1");
expect(r1.stderr).not.toContain("secret-name");
expect(r1.stderr).not.toContain(home);
expect(r1.stdout).not.toContain("secret-name");
const fetch2 = routedFetch();
const r2 = await runAgent({ home, fetch: fetch2, env, now: NOW + 15 * 60 * 1000 });
expect(r2.code).toBe(EXIT_OK);
expect(r2.stderr).not.toContain("read_errors");
const frozen = loadState(stateFile).frozenDays;
expect(Object.keys(frozen)).toHaveLength(28);
expect(frozen["2026-10-01"].models["claude-opus-5-5"].output).toBe(11);
});
it("a corrupt state.json gives a full beat without failing, and is rewritten", async () => {
const { home, stateFile } = makeHome({ fixtures: true });
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
fs.writeFileSync(stateFile, '{"frozenDays":{"2026-10-0');
const opened = [];
const fetch = routedFetch();
const r = await runAgent({
home, fetch, env: agentEnv({ CLAUDE_USAGE_LIMITS: "off" }),
openFile: (f, o) => { opened.push(f); return fs.createReadStream(f, o); },
});
expect(r.code).toBe(EXIT_OK);
expect(opened).toHaveLength(2); // the whole fixture tree
expect(Object.keys(loadState(stateFile).frozenDays).length).toBeGreaterThan(0);
expect(fs.statSync(stateFile).mode & 0o777).toBe(0o600);
});
it("writes the cache before the push: a failed push still leaves the frozen days on disk", async () => {
const { home, stateFile } = makeHome({ fixtures: true });
const fetch = routedFetch({ push: () => new Response("nope", { status: 503 }) });
const r = await runAgent({ home, fetch, env: agentEnv({ CLAUDE_USAGE_LIMITS: "off" }) });
expect(r.code).toBe(EXIT_PUSH);
expect(r.stderr).toContain("push failed: HTTP 503");
expect(Object.keys(loadState(stateFile).frozenDays).length).toBe(28);
});
it("the snapshot built on the fixtures goes through sanitizeClaudeUsage unchanged", async () => {
const { home } = makeHome({ fixtures: true, credentials: goodCredentials() });
const fetch = routedFetch();
const r = await runAgent({ home, fetch });
expect(r.code).toBe(EXIT_OK);
const sent = JSON.parse(pushCalls(fetch)[0].init.body);
expect(sent.schemaVersion).toBe(1);
expect(sent.timeZone).toBe("America/Toronto");
expect(sent.claudeCodeVersion).toBe("2.1.283");
expect(sent.days).toHaveLength(30);
expect(sent.days[29].date).toBe("2026-10-04");
expect(sent.limits.status).toBe("ok");
expect(sent.limits.items.find((i) => i.kind === "weekly_scoped").model).toBe("Fable");
expect(sent.projects.length).toBeGreaterThan(0);
const { snapshot, error } = sanitizeClaudeUsage(sent);
expect(error).toBeUndefined();
expect(snapshot).toEqual(sent);
});
it.each([
["ok limits", { credentials: goodCredentials() }, {}],
["expired token", { credentials: goodCredentials(NOW - 1) }, {}],
["no credentials", {}, {}],
["limits off", { credentials: goodCredentials() }, { CLAUDE_USAGE_LIMITS: "off" }],
])("with %s, the snapshot still round-trips through the sanitizer", async (_label, homeOpts, envExtra) => {
const { home } = makeHome({ fixtures: true, ...homeOpts });
const fetch = routedFetch();
expect((await runAgent({ home, fetch, env: agentEnv(envExtra) })).code).toBe(EXIT_OK);
const sent = JSON.parse(pushCalls(fetch)[0].init.body);
expect(sanitizeClaudeUsage(sent).snapshot).toEqual(sent);
});
it("sends the OAuth bearer only to api.anthropic.com and the ingest token only to the health API", async () => {
const { home } = makeHome({ fixtures: true, credentials: goodCredentials() });
const fetch = routedFetch();
await runAgent({ home, fetch });
expect(fetch.calls.map((c) => c.url)).toEqual([USAGE_URL, `${API_URL}/claude-usage`]);
const [usage, push] = fetch.calls;
expect(usage.init.headers.Authorization).toBe(`Bearer ${OAUTH_TOKEN}`);
expect(JSON.stringify(usage)).not.toContain(INGEST_TOKEN);
expect(push.init.headers.Authorization).toBe(`Bearer ${INGEST_TOKEN}`);
expect(push.init.method).toBe("POST");
expect(push.init.redirect).toBe("error");
expect(push.init.signal).toBeInstanceOf(AbortSignal);
expect(JSON.stringify(push.init.headers)).not.toContain(OAUTH_TOKEN);
expect(push.init.body).not.toContain(OAUTH_TOKEN);
});
it("neither token appears in the snapshot nor in the output, on success or on hostile failures", async () => {
const scenarios = [
routedFetch(),
routedFetch({ push: () => new Response(`denied ${INGEST_TOKEN} ${OAUTH_TOKEN}`, { status: 401 }) }),
routedFetch({ push: () => { throw new TypeError(`Headers.append: "Bearer ${INGEST_TOKEN}" is an invalid header value.`); } }),
routedFetch({ usage: () => new Response(`Authorization: Bearer ${OAUTH_TOKEN}`, { status: 500 }) }),
routedFetch({ push: () => { throw Object.assign(new Error(`connect to ${INGEST_TOKEN}`), { code: `E${INGEST_TOKEN}` }); } }),
];
for (const fetch of scenarios) {
fs.rmSync(path.join(tmpDir, "home"), { recursive: true, force: true });
const { home } = makeHome({ fixtures: true, credentials: goodCredentials() });
const r = await runAgent({ home, fetch });
expectNoLeak(`${r.stdout}\n${r.stderr}`);
expectNoLeak(pushCalls(fetch)[0].init.body);
expectNoLeak(fs.readFileSync(stateFilePath(defaultStateDir(home)), "utf-8"));
}
});
it("the real process output contains no fragment of either token", async () => {
const { home } = makeHome({ fixtures: true, credentials: goodCredentials(Date.now() - 1000) });
// Expired credentials (no Anthropic call), and a push target that refuses
// the connection: a real failing run, nothing leaves the machine.
const result = await execFileAsync(process.execPath, [PUSH_PATH], {
env: {
PATH: process.env.PATH,
HOME: home,
CLAUDE_USAGE_API_URL: "https://127.0.0.1:1",
CLAUDE_USAGE_INGEST_TOKEN: INGEST_TOKEN,
NO_PROXY: "*",
no_proxy: "*",
},
}).catch((e) => e);
expect(result.code).toBe(EXIT_PUSH);
expect(result.stderr).toMatch(/claude-usage: push failed: request error \(code=[A-Za-z0-9_]+\) limits=token_expired/);
expectNoLeak(`${result.stdout}${result.stderr}`);
});
it.each([
["http:", { CLAUDE_USAGE_API_URL: "http://health.example.test" }, "must be https://"],
["unset URL", { CLAUDE_USAGE_API_URL: "" }, "CLAUDE_USAGE_API_URL is not set"],
["garbage URL", { CLAUDE_USAGE_API_URL: "::not a url::" }, "not a valid URL"],
["unset token", { CLAUDE_USAGE_INGEST_TOKEN: "" }, "CLAUDE_USAGE_INGEST_TOKEN is not set"],
["token with a trailing newline", { CLAUDE_USAGE_INGEST_TOKEN: `${INGEST_TOKEN}\n` }, "invalid character"],
])("configuration error (%s) exits 1 before any scan or call", async (_label, envExtra, message) => {
const { home, stateFile } = makeHome({ fixtures: true, credentials: goodCredentials() });
const fetch = routedFetch();
const openFile = vi.fn();
const r = await runAgent({ home, fetch, env: agentEnv(envExtra), openFile });
expect(r.code).toBe(EXIT_CONFIG);
expect(r.stderr).toContain(message);
expect(fetch.calls).toHaveLength(0);
expect(openFile).not.toHaveBeenCalled();
expect(fs.existsSync(stateFile)).toBe(false);
expectNoLeak(`${r.stdout}${r.stderr}`);
});
it("readConfig appends /claude-usage to the base URL", () => {
expect(agentReadConfig(agentEnv({ CLAUDE_USAGE_API_URL: "https://h.example/" })).config.target).toBe(
"https://h.example/claude-usage",
);
});
it(`a snapshot over ${MAX_BODY_BYTES} bytes logs payload_too_large, exits 2, sends nothing`, async () => {
const { home } = makeHome();
const projectDir = path.join(home, ".claude", "projects", "big");
fs.mkdirSync(projectDir, { recursive: true });
// 30 days x 12 models with 64-character names and maximal counters: about
// 70 KB once serialized, every value still inside the contract.
const big = Number.MAX_SAFE_INTEGER;
const lines = [];
for (const date of windowDays("2026-10-04")) {
for (let m = 0; m < 12; m++) {
lines.push(
line({
ts: `${date}T15:00:00Z`,
model: `m${String(m).padStart(2, "0")}-${"x".repeat(60)}`,
input: big, output: big, cacheCreation: big, cacheRead: big,
}),
);
}
}
const file = path.join(projectDir, "s.jsonl");
fs.writeFileSync(file, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`);
setMtime(file, "2026-10-04T15:30:00Z");
const fetch = routedFetch();
const r = await runAgent({ home, fetch, env: agentEnv({ CLAUDE_USAGE_LIMITS: "off" }) });
expect(r.code).toBe(EXIT_PUSH);
expect(r.stderr).toMatch(/payload_too_large bytes=\d+ max=65536/);
expect(Number(/bytes=(\d+)/.exec(r.stderr)[1])).toBeGreaterThan(MAX_BODY_BYTES);
expect(fetch.calls).toHaveLength(0);
});
it("one push attempt only: a failure is not retried", async () => {
const { home } = makeHome({ fixtures: true });
const fetch = routedFetch({ push: () => { throw Object.assign(new TypeError("fetch failed"), { cause: { code: "ECONNRESET" } }); } });
const r = await runAgent({ home, fetch, env: agentEnv({ CLAUDE_USAGE_LIMITS: "off" }) });
expect(r.code).toBe(EXIT_PUSH);
expect(r.stderr).toContain("push failed: request error (code=ECONNRESET)");
expect(pushCalls(fetch)).toHaveLength(1);
});
it("a 2xx answer exits 0 with a one-line summary", async () => {
const { home } = makeHome({ fixtures: true, credentials: goodCredentials() });
const r = await runAgent({ home, fetch: routedFetch() });
expect(r.code).toBe(EXIT_OK);
expect(r.stdout).toMatch(/^claude-usage: ok status=204 limits=ok bytes=\d+$/);
expect(r.stderr).toBe("");
});
it("--dry-run prints the snapshot, needs no configuration, pushes nothing and leaves the cache alone", async () => {
const { home, stateFile } = makeHome({ fixtures: true });
const fetch = routedFetch();
const r = await runAgent({ home, fetch, env: { CLAUDE_USAGE_LIMITS: "off" }, argv: ["--dry-run"] });
expect(r.code).toBe(EXIT_OK);
const snap = JSON.parse(r.stdout);
expect(snap.days).toHaveLength(30);
expect(snap.limits).toEqual({ status: "disabled", fetchedAt: null, items: [] });
expect(fetch.calls).toHaveLength(0);
expect(fs.existsSync(stateFile)).toBe(false);
});
it("the agent modules load nothing but node built-ins and their siblings", () => {
for (const name of ["limits.js", "state.js", "push-claude-usage.js"]) {
const src = fs.readFileSync(path.join(AGENT_DIR, name), "utf-8");
for (const [, mod] of src.matchAll(/require\("([^"]+)"\)/g)) {
expect(mod.startsWith("node:") || mod.startsWith("./"), `${name} requires ${mod}`).toBe(true);
}
}
});
});
// --- run-claude-usage.sh ----------------------------------------------------------
describe("run-claude-usage.sh", () => {
function writeEnvFile(home, lines, mode = 0o600) {
const file = path.join(home, ".config", "maximus-claude-usage.env");
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${lines.join("\n")}\n`, { mode });
fs.chmodSync(file, mode);
return file;
}
// A stand-in for node: records that it ran, then holds the lock a while.
function writeStubNode(home, seconds) {
const stub = path.join(home, "stub-node");
fs.writeFileSync(stub, `#!/bin/sh\necho run >> "$HOME/runs"\nsleep ${seconds}\n`, { mode: 0o755 });
return stub;
}
function runs(home) {
const file = path.join(home, "runs");
return fs.existsSync(file) ? fs.readFileSync(file, "utf-8").trim().split("\n").length : 0;
}
function wrapperEnv(home) {
return { PATH: process.env.PATH, HOME: home };
}
test("two runs in parallel: the second exits 0 without working", async () => {
const { home } = makeHome();
const stub = writeStubNode(home, 2);
writeEnvFile(home, [
`CLAUDE_USAGE_API_URL=${API_URL}`,
`CLAUDE_USAGE_INGEST_TOKEN=${INGEST_TOKEN}`,
`NODE_BIN=${stub}`,
]);
const first = spawn(WRAPPER_PATH, [], { env: wrapperEnv(home), stdio: "ignore" });
const firstDone = new Promise((resolve) => first.on("exit", (code) => resolve(code)));
const deadline = Date.now() + 5000;
while (runs(home) === 0 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 25));
expect(runs(home)).toBe(1);
const started = Date.now();
const second = await execFileAsync(WRAPPER_PATH, [], { env: wrapperEnv(home) });
expect(Date.now() - started).toBeLessThan(1500);
expect(second.stdout).toContain("claude-usage: a run is already in progress, skipping");
expect(second.stderr).toBe("");
expect(runs(home)).toBe(1);
expect(await firstDone).toBe(0);
// The lock is released with the first run: the next beat works again.
const stub0 = writeStubNode(home, 0);
expect(stub0).toBe(stub);
await execFileAsync(WRAPPER_PATH, [], { env: wrapperEnv(home) });
expect(runs(home)).toBe(2);
expect(fs.existsSync(path.join(home, ".local", "state", "maximus-claude-usage", "run.lock"))).toBe(true);
}, 15000);
test("a dry run through the wrapper prints a valid snapshot and no token", async () => {
const { home } = makeHome({ fixtures: true });
writeEnvFile(home, [
`CLAUDE_USAGE_API_URL=${API_URL}`,
`CLAUDE_USAGE_INGEST_TOKEN=${INGEST_TOKEN}`,
"CLAUDE_USAGE_LIMITS=off",
`NODE_BIN=${process.execPath}`,
]);
const result = await execFileAsync(WRAPPER_PATH, ["--dry-run"], { env: wrapperEnv(home) });
const snap = JSON.parse(result.stdout);
expect(snap.limits.status).toBe("disabled"); // CLAUDE_USAGE_LIMITS was exported
expect(sanitizeClaudeUsage(snap).snapshot).toEqual(snap);
expect(result.stderr).toBe("");
expectNoLeak(`${result.stdout}${result.stderr}`);
});
test.each([
[["CLAUDE_USAGE_INGEST_TOKEN"], "CLAUDE_USAGE_API_URL is not set"],
[["CLAUDE_USAGE_API_URL"], "CLAUDE_USAGE_INGEST_TOKEN is not set"],
])("stops when a variable is missing (%j)", async (present, message) => {
const { home } = makeHome();
const values = { CLAUDE_USAGE_API_URL: API_URL, CLAUDE_USAGE_INGEST_TOKEN: INGEST_TOKEN };
writeEnvFile(home, present.map((k) => `${k}=${values[k]}`));
const result = await execFileAsync(WRAPPER_PATH, [], { env: wrapperEnv(home) }).catch((e) => e);
expect(result.code).toBe(1);
expect(result.stderr).toContain(message);
expectNoLeak(`${result.stdout}${result.stderr}`);
});
test("stops when the env file is absent, and warns on a mode other than 600", async () => {
const { home } = makeHome();
const missing = await execFileAsync(WRAPPER_PATH, [], { env: wrapperEnv(home) }).catch((e) => e);
expect(missing.code).toBe(1);
expect(missing.stderr).toContain("claude-usage: env file not found:");
const stub = writeStubNode(home, 0);
writeEnvFile(
home,
[`CLAUDE_USAGE_API_URL=${API_URL}`, `CLAUDE_USAGE_INGEST_TOKEN=${INGEST_TOKEN}`, `NODE_BIN=${stub}`],
0o644,
);
const loose = await execFileAsync(WRAPPER_PATH, [], { env: wrapperEnv(home) });
expect(loose.stderr).toContain("is mode 644, expected 600");
expect(runs(home)).toBe(1);
expectNoLeak(`${loose.stdout}${loose.stderr}`);
});
test("never enables shell tracing — `set -x` would echo the token into syslog", () => {
const script = fs.readFileSync(WRAPPER_PATH, "utf-8");
const traced = script
.split("\n")
.filter((l) => !l.trim().startsWith("#"))
.filter((l) => /\bset\b[^#\n]*-[a-z]*x/.test(l));
expect(traced).toEqual([]);
});
test("is executable, and takes a non-blocking flock under ~/.local/state", () => {
expect(fs.statSync(WRAPPER_PATH).mode & 0o111).toBeGreaterThan(0);
const script = fs.readFileSync(WRAPPER_PATH, "utf-8");
expect(script).toContain('STATE_DIR="$HOME/.local/state/maximus-claude-usage"');
expect(script).toMatch(/flock -n 9/);
});
});