vps-health-api/__tests__/agent-claude-usage.test.js
le king fu 5cb9182911
All checks were successful
CI / verification (pull_request) Successful in 26s
fix(agent): collapse doubled slashes in cwd before project mapping
Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:17:56 -04:00

636 lines
23 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", () => {
const 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 } },
},
"2026-09-30": { models: {}, projects: {} },
};
it("opens no file whose mtime day is before the threshold (spy on file opening)", async () => {
// Threshold = day after the most recent frozen day = 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)).toHaveLength(28);
expect(r.projects).toEqual([{ name: "alpha", work: 21 + 1 + 3, cacheRead: 7, requests: 8 }]);
});
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)/,
);
});
});