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: /claude-code//... -> ", () => { 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: /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-/-claude-code-

/... ->

", () => { 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 , 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(""); 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(), ); }); }); // ============================================================================ // 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: /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)); }); }); // --- 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(`error ${OAUTH_TOKEN} please retry`, { 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("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("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 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/); }); });