Skip to content
322 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 foldTasks,
6 gigabytes,
7 shownMeters,
8 alertText,
9 alertTone,
10 cardCheckResult,
11 cardFeeCents,
12 feeAndTax,
13 creditLine,
14 needsAttention,
15 usageGlance,
16 parseCaps,
17 parseLimitRequest,
18 parsePrepay,
19 parseSpendLimit,
20 planStatus,
21 readDollars,
22 requestStatus,
23 share,
24 spendPath,
25 spendRange,
26} from "./billing.ts";
27
28test("a credit from g1t reads in a line, with what is left and when it expires", () => {
29 const now = new Date("2026-10-07T12:00:00Z");
30 const grant = { amountMicros: 25_000_000, leftMicros: 12_400_000, expiresAt: "2027-01-05T23:59:59Z", state: "open" as const };
31 assert.equal(creditLine(grant, now), "$25.00 credit, $12.40 left, expires Jan 5, 2027");
32 assert.equal(creditLine({ ...grant, expiresAt: "2026-12-05T23:59:59Z" }, now), "$25.00 credit, $12.40 left, expires Dec 5");
33 assert.equal(creditLine({ ...grant, expiresAt: null }, now), "$25.00 credit, $12.40 left");
34 assert.equal(creditLine({ ...grant, state: "used", leftMicros: 0 }, now), "$25.00 credit, all used");
35 assert.equal(creditLine({ ...grant, state: "revoked", leftMicros: 0 }, now), "$25.00 credit, withdrawn");
36});
37
38test("the card fee shown before paying is the one billing charges, and prices exclude tax", () => {
39 const fee = { on: true, percentMicros: 29_000, fixedCents: 30 };
40 // The same figures as billing's own tests (ai.rs, tax.rs).
41 assert.equal(cardFeeCents(2_500, fee), 106);
42 assert.equal(cardFeeCents(2_000, fee), 91);
43 assert.equal(cardFeeCents(2_000, { ...fee, on: false }), 0);
44 assert.equal(cardFeeCents(0, fee), 0);
45 assert.equal(cardFeeCents(2_000, null), 0);
46 assert.equal(feeAndTax(59), "Card processing fee $0.59, plus tax where it applies");
47 assert.equal(feeAndTax(0), "Plus tax where it applies");
48});
49
50test("dollars are read from what people type", () => {
51 assert.equal(readDollars("$1,000"), 1_000_000_000);
52 assert.equal(readDollars(""), null);
53 assert.equal(readDollars("lots"), null);
54});
55
56test("a prepayment is $25 or more, by card up to $10,000, by bank transfer from $1,000", () => {
57 assert.deepEqual(parsePrepay({ amount: "100" }), { ok: true, value: { amountCents: 10_000, method: "card" } });
58 assert.deepEqual(parsePrepay({ amount: "100", custom: "250" }), {
59 ok: true,
60 value: { amountCents: 25_000, method: "card" },
61 });
62 assert.equal(parsePrepay({ custom: "10" }).ok, false);
63 assert.equal(parsePrepay({ custom: "20000" }).ok, false);
64 assert.equal(parsePrepay({ custom: "500", method: "bank_transfer" }).ok, false);
65 assert.deepEqual(parsePrepay({ custom: "20000", method: "bank_transfer" }), {
66 ok: true,
67 value: { amountCents: 2_000_000, method: "bank_transfer" },
68 });
69 assert.equal(parsePrepay({}).ok, false);
70});
71
72test("caps stay in range, and empty goes back to the default", () => {
73 assert.deepEqual(parseCaps({ run: "", issue: "" }), { ok: true, value: { runCapMicros: null, issueCapMicros: null } });
74 assert.deepEqual(parseCaps({ run: "0.10", issue: "1000" }), {
75 ok: true,
76 value: { runCapMicros: 100_000, issueCapMicros: 1_000_000_000 },
77 });
78 assert.equal(parseCaps({ run: "0.05" }).ok, false);
79 assert.equal(parseCaps({ issue: "2000" }).ok, false);
80 assert.equal(parseCaps({ run: "two" }).ok, false);
81});
82
83test("a limit request needs an amount, a reason and a monthly spend; an overage only a reason", () => {
84 assert.deepEqual(parseLimitRequest({ kind: "limit", amount: "5000", reason: " A launch ", expected: "3000" }), {
85 ok: true,
86 value: { kind: "limit", amountMicros: 5_000_000_000, reason: "A launch", expectedMonthlyMicros: 3_000_000_000 },
87 });
88 assert.equal(parseLimitRequest({ kind: "limit", amount: "5000", reason: "" , expected: "1" }).ok, false);
89 assert.equal(parseLimitRequest({ kind: "limit", reason: "x", expected: "1" }).ok, false);
90 assert.deepEqual(parseLimitRequest({ kind: "overage", reason: "A loop ran all night" }), {
91 ok: true,
92 value: { kind: "overage", amountMicros: 0, reason: "A loop ran all night", expectedMonthlyMicros: 0 },
93 });
94});
95
96test("a spend limit is automatic, fixed, the full amount available, or the one-time raise", () => {
97 assert.deepEqual(parseSpendLimit({ mode: "automatic" }), { ok: true, value: { micros: null, useFull: false, raiseOnce: false } });
98 assert.deepEqual(parseSpendLimit({ mode: "fixed", limit: "500" }), {
99 ok: true,
100 value: { micros: 500_000_000, useFull: false, raiseOnce: false },
101 });
102 assert.equal(parseSpendLimit({ mode: "fixed", limit: "0.5" }).ok, false);
103 assert.deepEqual(parseSpendLimit({ mode: "full" }), { ok: true, value: { micros: null, useFull: true, raiseOnce: false } });
104 assert.deepEqual(parseSpendLimit({ mode: "raise" }), { ok: true, value: { micros: null, useFull: false, raiseOnce: true } });
105 assert.deepEqual(parseSpendLimit({ mode: "raise", limit: "150" }), {
106 ok: true,
107 value: { micros: 150_000_000, useFull: false, raiseOnce: true },
108 });
109});
110
111test("the spend limit's range: up to the highest ceiling plus prepaid, then once to twice it, then ask", () => {
112 // A first month at $100, nothing prepaid, the raise unused: $100 alone, $200 once.
113 const range = spendRange({ availableMicros: 100_000_000, ceilingMicros: 100_000_000, raiseOnceMicros: 200_000_000, raisedAt: null });
114 assert.deepEqual(range, { selfServeMicros: 100_000_000, raiseOnceMicros: 200_000_000, raisedAt: null });
115 assert.equal(spendPath(80_000_000, range), "self");
116 assert.equal(spendPath(100_000_000, range), "self");
117 assert.equal(spendPath(150_000_000, range), "raise");
118 assert.equal(spendPath(200_000_000, range), "raise");
119 assert.equal(spendPath(200_000_001, range), "ask");
120 // The raise used: only asking goes further.
121 const used = spendRange({ availableMicros: 200_000_000, ceilingMicros: 200_000_000, raiseOnceMicros: null, raisedAt: "2026-10-01T00:00:00Z" });
122 assert.equal(used.raiseOnceMicros, null);
123 assert.equal(spendPath(300_000_000, used), "ask");
124 // No ceiling: anything goes.
125 assert.equal(spendPath(1e12, spendRange({ availableMicros: null, ceilingMicros: null, raiseOnceMicros: null })), "self");
126 // A raise no higher than what is available is no raise.
127 assert.equal(spendRange({ availableMicros: 300_000_000, ceilingMicros: 100_000_000, raiseOnceMicros: 300_000_000 }).raiseOnceMicros, null);
128});
129
130test("the plan card says where the workspace stands", () => {
131 const free = { plan: "free" as const, trialMicrosLeft: 0, trialVerified: false, firstMonth: false };
132 assert.deepEqual(planStatus(null, free), { kind: "free", label: "Free" });
133 assert.equal(planStatus(null, { ...free, trialVerified: true, trialMicrosLeft: 5_000_000 }).kind, "trial");
134 assert.equal(planStatus(null, { ...free, trialVerified: true }).kind, "free");
135 const subscription = { feature: "plan" as const, status: "active" as const, periodEnd: null, startedBy: "a", startedAt: "" };
136 assert.deepEqual(planStatus({ on: true, subscription }, { ...free, plan: "paid", firstMonth: true }), {
137 kind: "paid",
138 label: "On the g1t plan, first month",
139 });
140 assert.equal(planStatus({ on: true, subscription: { ...subscription, status: "canceling" } }, { ...free, plan: "paid" }).kind, "canceling");
141 assert.equal(planStatus({ on: false, subscription: { ...subscription, status: "past_due" } }, free).kind, "past_due");
142 assert.equal(planStatus({ on: true, included: true, subscription: null }, free).kind, "comped");
143 assert.equal(planStatus(null, { ...free, plan: "internal" }).kind, "comped");
144 assert.equal(planStatus(null, { ...free, plan: "enterprise" }).kind, "enterprise");
145});
146
147test("the overview's Usage card reads the month by what pays", () => {
148 const usage = {
149 free: false,
150 spentMicros: 14_000_000,
151 usedMicros: 11_000_000,
152 byTask: [
153 { key: "review", micros: 2_000_000, runs: 4 },
154 { key: "implement", micros: 12_000_000, runs: 9 },
155 { key: "plan", micros: 0, runs: 1 },
156 ],
157 };
158 const entitlements = { includedMicros: 10_000_000, includedUsedMicros: 10_000_000, trialMicrosLeft: 0 };
159 const plan = usageGlance({ usage, status: { kind: "paid", label: "" }, entitlements, limit: { spentMicros: 4_000_000, spendLimitMicros: 200_000_000 }, trialMicros: 5_000_000 });
160 assert.equal(plan.kind, "plan");
161 assert.deepEqual(plan.credit, { label: "Included usage", usedMicros: 10_000_000, ofMicros: 10_000_000 });
162 assert.deepEqual(plan.onDemand, { micros: 4_000_000, limitMicros: 200_000_000 });
163 // Most first, and nothing that cost nothing.
164 assert.deepEqual(plan.lines.map((line) => line.label), ["Making changes", "Reviews"]);
165 const trial = usageGlance({ usage, status: { kind: "trial", label: "" }, entitlements: { ...entitlements, trialMicrosLeft: 1_500_000 }, limit: null, trialMicros: 5_000_000 });
166 assert.deepEqual(trial.credit, { label: "Trial credit", usedMicros: 3_500_000, ofMicros: 5_000_000 });
167 assert.equal(trial.onDemand, null);
168 assert.equal(usageGlance({ usage, status: { kind: "comped", label: "" }, entitlements, limit: null, trialMicros: 0 }).kind, "comped");
169 assert.equal(usageGlance({ usage, status: { kind: "free", label: "" }, entitlements, limit: null, trialMicros: 0 }).kind, "forge");
170 // While g1t charges nothing, usage is shown at cost.
171 const beta = usageGlance({ usage: { ...usage, free: true }, status: { kind: "paid", label: "" }, entitlements, limit: null, trialMicros: 0 });
172 assert.equal(beta.kind, "beta");
173 assert.equal(beta.spentMicros, 11_000_000);
174});
175
176test("a meter's share stays between 0 and 1", () => {
177 assert.equal(share(5, 10), 0.5);
178 assert.equal(share(15, 10), 1);
179 assert.equal(share(5, 0), 0);
180 assert.equal(share(5, null), 0);
181});
182
183test("after a card check the owner reads whether the trial started", () => {
184 assert.match(cardCheckResult({ trialVerified: true, trialMicrosLeft: 5_000_000 }, 5_000_000), /[$]5 trial is ready/);
185 assert.match(cardCheckResult({ trialVerified: true, trialMicrosLeft: 0 }, 5_000_000), /Prepaid cards can still pay for the plan/);
186 assert.match(cardCheckResult({ trialVerified: false, trialMicrosLeft: 0 }, 5_000_000), /did not finish/);
187});
188
189test("alerts read as sentences", () => {
190 const alert = { meter: "included", level: 90, usedMicros: 9_000_000, limitMicros: 10_000_000, message: "" };
191 assert.equal(alertText(alert), "90% of the plan's included usage: $9.00 of $10.00.");
192 assert.equal(alertText({ ...alert, meter: "spend_limit", level: 100 }), "All of your spend limit: $9.00 of $10.00.");
193 assert.equal(alertTone(50), "ok");
194 assert.equal(alertTone(75), "warning");
195 assert.equal(alertTone(100), "stopped");
196});
197
198test("a workspace needs attention when paused, on a spike, or at 90%", () => {
199 assert.equal(needsAttention(null), false);
200 assert.equal(needsAttention({ paused: null, spike: null, alerts: [] }), false);
201 assert.equal(needsAttention({ paused: "Spend spike", spike: null, alerts: [] }), true);
202 const spike = { id: "s", status: "continued", hourMicros: 1, averageMicros: 1, detectedAt: "" };
203 assert.equal(needsAttention({ paused: null, spike, alerts: [] }), false);
204 assert.equal(needsAttention({ paused: null, spike: { ...spike, status: "open" }, alerts: [] }), true);
205 const at = (level: number) => ({ meter: "spend_limit", level, usedMicros: 0, limitMicros: 0, message: "" });
206 assert.equal(needsAttention({ paused: null, spike: null, alerts: [at(75)] }), false);
207 assert.equal(needsAttention({ paused: null, spike: null, alerts: [at(90)] }), true);
208});
209
210test("a request says where it stands", () => {
211 const request = {
212 id: "r",
213 workspace: "acme",
214 kind: "limit",
215 amountMicros: 1,
216 reason: "",
217 expectedMonthlyMicros: 0,
218 status: "open",
219 createdBy: "a",
220 createdAt: "",
221 };
222 assert.equal(requestStatus(request), "Waiting for an answer");
223 assert.equal(requestStatus({ ...request, status: "approved", decidedMicros: 2_000_000_000 }), "Approved at $2,000");
224 assert.equal(requestStatus({ ...request, status: "declined" }), "Declined");
225});
226
227const METERS = [
228 { key: "agents", label: "Agents & sandboxes", micros: 1_200_000, quantity: "4 runs" },
229 { key: "builds", label: "Builds", micros: 0, quantity: null },
230 { key: "requests", label: "Requests & CPU", micros: 0, quantity: null },
231 { key: "domains", label: "Custom domains", micros: 120_000, quantity: "1 custom domain" },
232 { key: "git_storage", label: "Git operations & storage", micros: 0, quantity: "1,200 git operations" },
233 { key: "search_scans", label: "Search & security scans", micros: 3_000, quantity: null },
234];
235
236test("the plan shows every meter, with no quota beside any of them", () => {
237 const shown = shownMeters(METERS, true);
238 assert.deepEqual(
239 shown.map((m) => m.key),
240 ["agents", "builds", "requests", "domains", "git_storage", "search_scans"],
241 );
242 for (const meter of shown) assert.ok(!/ of |apps up/i.test(`${meter.label} ${meter.quantity ?? ""}`));
243});
244
245test("a free workspace sees only what it can use, and anything it used anyway", () => {
246 assert.deepEqual(
247 shownMeters(METERS, false).map((m) => m.key),
248 ["agents", "domains", "git_storage", "search_scans"],
249 );
250});
251
252test("storage reads in powers of ten", () => {
253 assert.equal(gigabytes(1_000_000_000), "1 GB");
254 assert.equal(gigabytes(1_500_000_000), "1.5 GB");
255 assert.equal(gigabytes(500_000_000), "500 MB");
256});
257
258test("kinds of work without words of their own are one Other, listed once and last", () => {
259 const slices = [
260 { key: "check", micros: 5, runs: 50 },
261 { key: "review", micros: 3, runs: 5 },
262 { key: "other", micros: 1, runs: 7 },
263 { key: "implement", micros: 9, runs: 4 },
264 { key: "answer", micros: 2, runs: 11 },
265 ];
266 assert.deepEqual(foldTasks(slices), [
267 { key: "review", micros: 3, runs: 5 },
268 { key: "implement", micros: 9, runs: 4 },
269 { key: "other", micros: 8, runs: 68 },
270 ]);
271 assert.deepEqual(foldTasks([{ key: "plan", micros: 1, runs: 1 }]), [{ key: "plan", micros: 1, runs: 1 }]);
272});
273
274test("AI credit is bought in whole dollars from $10 to $1,000", async () => {
275 const { parseAiPurchase } = await import("./billing.ts");
276 assert.deepEqual(parseAiPurchase({ amount: "25" }), { ok: true, value: { amountCents: 2_500 } });
277 assert.deepEqual(parseAiPurchase({ amount: "custom", custom: "$150" }), { ok: true, value: { amountCents: 15_000 } });
278 assert.equal(parseAiPurchase({ amount: "custom", custom: "5" }).ok, false);
279 assert.equal(parseAiPurchase({ amount: "custom", custom: "12.50" }).ok, false);
280 assert.equal(parseAiPurchase({ amount: "custom", custom: "" }).ok, false);
281});
282
283test("auto-reload's amounts are checked before billing sees them", async () => {
284 const { parseAiReload } = await import("./billing.ts");
285 assert.deepEqual(parseAiReload({ enabled: "on", threshold: "10", target: "25", monthly: "100" }), {
286 ok: true,
287 value: { enabled: true, thresholdMicros: 10_000_000, targetMicros: 25_000_000, monthlyMaxMicros: 100_000_000 },
288 });
289 assert.equal(parseAiReload({ threshold: "10", target: "15", monthly: "100" }).ok, false);
290 assert.equal(parseAiReload({ threshold: "10", target: "25", monthly: "10" }).ok, false);
291 assert.equal(parseAiReload({ threshold: "10", target: "25.5", monthly: "100" }).ok, false);
292});
293
294test("budget alerts keep the levels ticked, highest first", async () => {
295 const { parseBudgetAlerts } = await import("./billing.ts");
296 assert.deepEqual(parseBudgetAlerts({ alerts: ["50", "100", "75", "12"], pause: "on", webhook: "" }), {
297 ok: true,
298 value: { alerts: [100, 75, 50], pauseAtLimit: true, webhook: null },
299 });
300 assert.equal(parseBudgetAlerts({ alerts: [], webhook: "http://x.test" }).ok, false);
301 assert.deepEqual(parseBudgetAlerts({ alerts: [], webhook: "https://hooks.acme.test/g1t" }).ok, true);
302});
303
304test("invoice details read from the form, with the country in capitals", async () => {
305 const { parseInvoiceDetails } = await import("./billing.ts");
306 const form = new FormData();
307 form.set("email", "billing@acme.test");
308 form.set("country", "de");
309 form.set("taxIdType", "eu_vat");
310 form.set("taxId", "DE123456789");
311 const parsed = parseInvoiceDetails(form);
312 assert.ok(parsed.ok);
313 assert.equal(parsed.value.address.country, "DE");
314 form.set("taxId", "");
315 assert.equal(parseInvoiceDetails(form).ok, false);
316 // Tax is worked out from the address: in the US, the ZIP code is needed.
317 form.set("taxIdType", "");
318 form.set("country", "us");
319 assert.equal(parseInvoiceDetails(form).ok, false);
320 form.set("postalCode", "94107");
321 assert.ok(parseInvoiceDetails(form).ok);
322});