/** * @file The Analytics date range: `parseRange` (local dates → UTC instants, * 400 on bad input), `?from=&to=` on GET /api/pricing/cost (totals, breakdown * and by_tool by spend day; daily_costs whole), GET /api/analytics (counts by * started_at / created_at, tokens and cost from the days in the range) and * GET /api/workflows. * @author Nguyễn Ngọc Trí Vĩ */ "use strict"; const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const http = require("node:http"); const ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "analytics-range-"))); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); process.env.CLAUDE_HOME = path.join(ROOT, "claude"); process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data"); process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp } = require("../index"); const { db, stmts } = require("../db"); const { parseRange, inRange } = require("../lib/source-filter"); const M = 1_000_000; let server; let base; async function get(url) { const res = await fetch(`${base}${url}`); return { status: res.status, body: await res.json() }; } function session(id, startedAt, tool = "claude") { db.prepare( "INSERT INTO sessions (id, name, status, started_at, updated_at, tool) VALUES (?, ?, 'completed', ?, ?, ?)" ).run(id, id, startedAt, startedAt, tool); } const usage = (id, input) => stmts.setTokenUsage.run( id, "range-model", "standard", "global", "standard", input, 0, 0, 0, 0, 0, 0, 0 ); const hour = (id, h, input) => stmts.upsertHourlyUsage.run( id, h, "range-model", "standard", "global", "standard", input, 0, 0, 0, 0, 0, 0, 0 ); const event = (sid, at, tool) => db .prepare( "INSERT INTO events (session_id, event_type, tool_name, created_at) VALUES (?, 'PreToolUse', ?, ?)" ) .run(sid, tool, at); before(async () => { server = http.createServer(createApp()); await new Promise((r) => server.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${server.address().port}`; db.prepare( "INSERT OR REPLACE INTO model_pricing (model_pattern, display_name, input_per_mtok, output_per_mtok, cache_read_per_mtok, cache_write_per_mtok) VALUES ('range-model%', 'R', 1, 0, 0, 0)" ).run(); // "a": started 2026-09-01, 1 M on the 1st, 2 M on the 10th (hourly rows). session("a", "2026-09-01T10:00:00.000Z"); usage("a", 3 * M); hour("a", "2026-09-01T10", 1 * M); hour("a", "2026-09-10T10", 2 * M); // "b": a codex session started 2026-09-20 with 4 M and no hourly rows. session("b", "2026-09-20T10:00:00.000Z", "codex"); usage("b", 4 * M); event("a", "2026-09-01T11:00:00.000Z", "Read"); event("a", "2026-09-10T11:00:00.000Z", "Edit"); event("b", "2026-09-20T11:00:00.000Z", "Bash"); }); after(() => { server.close(); fs.rmSync(ROOT, { recursive: true, force: true }); }); describe("parseRange", () => { const q = (query) => parseRange({ query }); it("is null with no range and false on bad input", () => { assert.equal(q({}), null); assert.equal(q({ from: "", to: "" }), null); assert.equal(q({ from: "2026-02-30" }), false); assert.equal(q({ from: "09/01/2026" }), false); assert.equal(q({ from: "2026-09-10", to: "2026-09-01" }), false); assert.equal(q({ to: ["2026-09-01"] }), false); assert.equal(q({ to: "9999-12-31" }), false); }); it("turns local dates into UTC instants with tz_offset, end exclusive", () => { assert.deepEqual(q({ from: "2026-09-01", to: "2026-09-02", tz_offset: "-420" }), { from: "2026-09-01", to: "2026-09-02", start: "2026-08-31T17:00:00.000Z", end: "2026-09-02T17:00:00.000Z", }); const open = q({ to: "2026-09-02" }); assert.equal(open.start, null); assert.equal(open.end, "2026-09-03T00:00:00.000Z"); assert.ok(inRange(open, "2026-01-01")); assert.ok(!inRange(open, "2026-09-03")); }); }); describe("GET /api/pricing/cost?from=&to=", () => { it("prices only the spend days in the range and keeps the whole daily series", async () => { const all = await get("/api/pricing/cost?tz_offset=0"); assert.equal(all.body.total_cost, 7); const r = await get("/api/pricing/cost?tz_offset=0&from=2026-09-05&to=2026-09-25"); assert.equal(r.status, 200); assert.equal(r.body.total_cost, 6); // 2 M on the 10th + b's 4 M on the 20th assert.deepEqual( r.body.by_tool.map((x) => [x.tool, x.cost]), [ ["codex", 4], ["claude", 2], ] ); assert.equal(r.body.breakdown[0].input_tokens, 6 * M); assert.deepEqual(r.body.daily_costs, all.body.daily_costs); }); it("answers 400 for a bad range", async () => { const r = await get("/api/pricing/cost?from=2026-13-01"); assert.equal(r.status, 400); assert.equal(r.body.error.code, "INVALID_INPUT"); }); }); describe("GET /api/analytics?from=&to=", () => { it("leaves the unranged answer as it was", async () => { const r = await get("/api/analytics?tz_offset=0"); assert.equal(r.body.overview.total_sessions, 2); assert.equal(r.body.overview.total_events, 3); assert.equal(r.body.tokens.total_input, 7 * M); assert.equal(r.body.total_cost, 7); }); it("counts sessions and events in the range, tokens and cost by spend day", async () => { const r = await get("/api/analytics?tz_offset=0&from=2026-09-05&to=2026-09-15"); assert.equal(r.status, 200); assert.equal(r.body.overview.total_sessions, 0); // "a" started before assert.equal(r.body.overview.total_events, 1); assert.deepEqual(r.body.tool_usage, [{ tool_name: "Edit", count: 1 }]); assert.equal(r.body.tokens.total_input, 2 * M); assert.equal(r.body.total_cost, 2); // The daily chart keeps its own window. assert.equal(r.body.daily_events.length, 3); }); it("combines with ?tool= and answers 400 for a bad range", async () => { const r = await get("/api/analytics?tz_offset=0&tool=codex&from=2026-09-01&to=2026-09-30"); assert.equal(r.body.overview.total_sessions, 1); assert.equal(r.body.total_cost, 4); assert.equal((await get("/api/analytics?to=nope")).status, 400); }); }); describe("GET /api/workflows?from=&to=", () => { it("counts the sessions started in the range", async () => { const r = await get("/api/workflows?from=2026-09-15&to=2026-09-30"); assert.equal(r.status, 200); assert.equal(r.body.stats.totalSessions, 1); assert.equal((await get("/api/workflows?from=x")).status, 400); }); });