| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { money } from "./money.ts"; |
| 5 | import { monthSpan, spanFor } from "./spend.ts"; |
| 6 | import { byProject, columns, defaultGrain, isStaff, pendingSentence, quantity, receipt, resolveRange, ticks, usageCsv } from "./usage.ts"; |
| 7 | |
| 8 | const axisMoney = (micros: number) => money(micros, { compact: true }); |
| 9 | |
| 10 | const NOW = new Date("2026-10-08T15:00:00Z"); |
| 11 | |
| 12 | test("periods resolve to whole UTC days, both ends included", () => { |
| 13 | assert.deepEqual(resolveRange(null, {}, NOW), { period: "cycle", from: "2026-10-01", until: "2026-10-08" }); |
| 14 | // The current cycle is the one "this month" Spend and the top bar ask billing for. |
| 15 | assert.deepEqual(resolveRange("cycle", {}, NOW), { period: "cycle", ...monthSpan(NOW) }); |
| 16 | assert.deepEqual(spanFor("month", NOW), monthSpan(NOW)); |
| 17 | assert.deepEqual(resolveRange("last_cycle", {}, NOW), { period: "last_cycle", from: "2026-09-01", until: "2026-09-30" }); |
| 18 | assert.deepEqual(resolveRange("7d", {}, NOW), { period: "7d", from: "2026-10-02", until: "2026-10-08" }); |
| 19 | assert.deepEqual(resolveRange("custom", { from: "2026-10-05", until: "2026-09-20" }, NOW), { period: "custom", from: "2026-09-20", until: "2026-10-05" }); |
| 20 | // A custom range that is not days falls back to the cycle. |
| 21 | assert.equal(resolveRange("custom", { from: "yesterday", until: "2026-10-01" }, NOW).period, "cycle"); |
| 22 | assert.equal(resolveRange("nonsense", {}, NOW).period, "cycle"); |
| 23 | }); |
| 24 | |
| 25 | test("columns cover every day, stack products, and add up when cumulative", () => { |
| 26 | const days = [ |
| 27 | { day: "2026-10-01", product: "agent", micros: 2_000_000 }, |
| 28 | { day: "2026-10-01", product: "sandboxes", micros: 500_000 }, |
| 29 | { day: "2026-10-03", product: "agent", micros: 1_000_000 }, |
| 30 | ]; |
| 31 | const daily = columns(days, "2026-10-01", "2026-10-03"); |
| 32 | assert.deepEqual(daily.map((c) => c.total), [2_500_000, 0, 1_000_000]); |
| 33 | assert.equal(daily[0]!.parts.sandboxes, 500_000); |
| 34 | const running = columns(days, "2026-10-01", "2026-10-03", "day", true); |
| 35 | assert.deepEqual(running.map((c) => c.total), [2_500_000, 2_500_000, 3_500_000]); |
| 36 | assert.equal(running[2]!.parts.agent, 3_000_000); |
| 37 | // Weeks start on Monday: Oct 1 2026 is a Thursday. |
| 38 | const weekly = columns(days, "2026-10-01", "2026-10-08", "week"); |
| 39 | assert.deepEqual(weekly.map((c) => c.key), ["2026-09-28", "2026-10-05"]); |
| 40 | assert.equal(weekly[0]!.total, 3_500_000); |
| 41 | assert.equal(columns(days, "2026-09-15", "2026-10-03", "month").length, 2); |
| 42 | assert.equal(defaultGrain("2026-10-01", "2026-10-31"), "day"); |
| 43 | assert.equal(defaultGrain("2026-07-01", "2026-10-01"), "week"); |
| 44 | }); |
| 45 | |
| 46 | test("the money axis has clean ticks, never the same label twice", () => { |
| 47 | assert.deepEqual(ticks(0), [0]); |
| 48 | const t = ticks(3_320_000); |
| 49 | assert.equal(t[0], 0); |
| 50 | assert.ok(t[t.length - 1]! >= 3_320_000); |
| 51 | const labels = t.map(axisMoney); |
| 52 | assert.equal(new Set(labels).size, labels.length, labels.join(" ")); |
| 53 | const small = ticks(4_000).map(axisMoney); |
| 54 | assert.equal(new Set(small).size, small.length, small.join(" ")); |
| 55 | assert.equal(axisMoney(2_000_000), "$2"); |
| 56 | assert.equal(axisMoney(1_500_000_000), "$1.5K"); |
| 57 | }); |
| 58 | |
| 59 | test("quantities read in their units", () => { |
| 60 | assert.equal(quantity(1_234_567, "tokens"), "1.2M tokens"); |
| 61 | assert.equal(quantity(3_725, "seconds"), "1h 2m"); |
| 62 | assert.equal(quantity(504_000_000, "bytes"), "504 MB"); |
| 63 | assert.equal(quantity(1, "entries"), "1 entry"); |
| 64 | }); |
| 65 | |
| 66 | test("the receipt shows usage at price, then what paid, then what is charged", () => { |
| 67 | const lines = receipt( |
| 68 | { priceMicros: 3_320_000, discountMicros: 3_320_000, includedMicros: 0, creditsMicros: 0, chargedMicros: 0, pendingMicros: 0, costMicros: 0 }, |
| 69 | 100, |
| 70 | ); |
| 71 | assert.deepEqual(lines.map((l) => l.label), ["Usage at price", "Discount (100%)", "Charged"]); |
| 72 | }); |
| 73 | |
| 74 | test("grouping by project adds a project's meters together", () => { |
| 75 | const meter = (label: string, parts: [string, number][]) => ({ |
| 76 | key: label, label, product: "agent", unit: "entries", quantity: 0, micros: 0, daily: [], |
| 77 | byProject: parts.map(([project, micros]) => ({ project, micros, quantity: 0 })), |
| 78 | }); |
| 79 | const rows = byProject({ |
| 80 | products: [{ key: "agent", label: "Agent", micros: 0, meters: [meter("Model tokens", [["acme/web", 300], ["acme/api", 500]]), meter("Agent rate", [["acme/web", 400]])] }], |
| 81 | }); |
| 82 | assert.deepEqual(rows.map((r) => [r.project, r.micros]), [["acme/web", 700], ["acme/api", 500]]); |
| 83 | }); |
| 84 | |
| 85 | test("the CSV has a row per day and meter with anything on it", () => { |
| 86 | const csv = usageCsv({ |
| 87 | from: "2026-10-01", |
| 88 | until: "2026-10-02", |
| 89 | products: [{ key: "agent", label: "Agent", micros: 0, meters: [{ key: "agent_models", label: "Model tokens", product: "agent", unit: "tokens", quantity: 0, micros: 0, daily: [0, 1_500_000], byProject: [] }] }], |
| 90 | }); |
| 91 | assert.equal(csv, "day,product,meter,usd\n2026-10-02,Agent,Model tokens,1.500000\n"); |
| 92 | }); |
| 93 | |
| 94 | test("test-mode hints are for g1t's own people", () => { |
| 95 | assert.ok(isStaff({ workspaces: [{ slug: "flagon-io" }] })); |
| 96 | assert.ok(!isStaff({ workspaces: [{ slug: "acme" }] })); |
| 97 | assert.ok(!isStaff(null)); |
| 98 | }); |
| 99 | |
| 100 | test("the pending note says what the close will charge, with the fraction of a cent that explains a gap", () => { |
| 101 | // A comped workspace: $0.009 pending, nothing of it charged. |
| 102 | assert.equal( |
| 103 | pendingSentence({ pendingMicros: 9_000, pendingChargedMicros: 0 }), |
| 104 | "$0.009 of it is metered this month and not yet closed; nothing will be charged when the month closes, after your discount and included usage.", |
| 105 | ); |
| 106 | assert.equal( |
| 107 | pendingSentence({ pendingMicros: 9_000, pendingChargedMicros: 6_300 }), |
| 108 | "$0.009 of it is metered this month and not yet closed; $0.0063 of it will be charged when the month closes, after your discount and included usage.", |
| 109 | ); |
| 110 | assert.equal( |
| 111 | pendingSentence({ pendingMicros: 1_340_000, pendingChargedMicros: 1_340_000 }), |
| 112 | "$1.34 of it is metered this month and not yet closed; all of it will be charged when the month closes, after your discount and included usage.", |
| 113 | ); |
| 114 | // An older billing, with no split yet. |
| 115 | assert.equal(pendingSentence({ pendingMicros: 9_000 }), "$0.009 of it is metered this month and not yet closed; it is charged when the month closes."); |
| 116 | }); |