Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

hosted-loop-spend-floor.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

hosted-loop-spend-floor.test.tsBlame166 lines · 1 contributor
68eccaaccantynz-alt1/**
2 * Regression guard: a hosted loop's monthly budget must not be bypassable by
3 * simply declining to report usage.
4 *
5 * The cap itself was fine — invokeLoop short-circuits with `budget_exceeded`
6 * once monthly spend reaches monthlyBudgetCents. What fed it was not. Spend
7 * came entirely from extractUsageFromStdout, i.e. from a `usage` block
8 * printed by the snippet, and loop.sourceCode is arbitrary user-supplied
9 * code that any registered account can run (the file says so itself, above
10 * the env block).
11 *
12 * So a snippet that never prints a usage block costed 0 cents, its monthly
13 * spend never advanced, and the cap could never stop it: unlimited
14 * invocations, each burning up to LOOP_EXEC_TIMEOUT_MS of platform compute,
15 * and platform Anthropic credits wherever HOSTED_LOOPS_PLATFORM_KEY=1.
16 *
17 * The fix charges a floor derived from the executor's own measured duration,
18 * which the snippet cannot forge. Self-reported usage still wins when it is
19 * higher — it is the better estimate of real token spend and nobody has an
20 * incentive to overstate it. The floor only removes the option of reporting
21 * nothing.
22 *
23 * NOTE this is a deliberate billing behaviour change, not purely internal:
24 * runs that previously cost 0 now cost at least the per-run minimum. Both
25 * knobs are env-tunable and read at access time.
26 */
27
28import { describe, it, expect, afterEach } from "bun:test";
29import { readFileSync } from "node:fs";
30import { join } from "node:path";
31import {
32 invocationFloorCents,
33 extractUsageFromStdout,
34 LOOP_EXEC_TIMEOUT_MS,
35} from "../lib/hosted-claude-loop";
36
37const ENV_MIN = "HOSTED_LOOP_MIN_CENTS_PER_RUN";
38const ENV_RATE = "HOSTED_LOOP_MS_PER_CENT";
39
40afterEach(() => {
41 delete process.env[ENV_MIN];
42 delete process.env[ENV_RATE];
43});
44
45describe("invocationFloorCents", () => {
46 it("charges at least one cent even for an instant run", () => {
47 // This is the whole point: a snippet reporting nothing is not free.
48 expect(invocationFloorCents(0)).toBe(1);
49 expect(invocationFloorCents(1)).toBe(1);
50 });
51
52 it("scales with measured wall clock", () => {
53 expect(invocationFloorCents(10_000)).toBe(1);
54 expect(invocationFloorCents(10_001)).toBe(2);
55 expect(invocationFloorCents(25_000)).toBe(3);
56 });
57
58 it("bounds a maximum-length run sensibly", () => {
59 // Execution is hard-capped, so the floor per run is bounded too.
60 const maxFloor = invocationFloorCents(LOOP_EXEC_TIMEOUT_MS);
61 expect(maxFloor).toBeGreaterThan(0);
62 expect(maxFloor).toBeLessThanOrEqual(5);
63 });
64
65 it("never returns a negative or non-finite charge", () => {
66 for (const bad of [-1, -100_000, NaN, Infinity, -Infinity]) {
67 const got = invocationFloorCents(bad as number);
68 expect(Number.isFinite(got)).toBe(true);
69 expect(got).toBeGreaterThanOrEqual(1);
70 }
71 });
72
73 it("honours the env knobs at access time", () => {
74 process.env[ENV_MIN] = "5";
75 process.env[ENV_RATE] = "1000";
76 expect(invocationFloorCents(0)).toBe(5);
77 expect(invocationFloorCents(9_000)).toBe(9);
78 });
79
80 it("falls back to safe defaults when the env is nonsense", () => {
81 process.env[ENV_MIN] = "not-a-number";
82 process.env[ENV_RATE] = "0";
83 expect(invocationFloorCents(0)).toBe(1);
84 expect(invocationFloorCents(10_000)).toBe(1);
85 });
86
87 it("allows the floor to be disabled deliberately", () => {
88 // Explicit zero is a real operator choice, not nonsense — it must be
89 // honoured rather than silently coerced back to the default.
90 process.env[ENV_MIN] = "0";
91 expect(invocationFloorCents(0)).toBe(0);
92 });
93});
94
95describe("extractUsageFromStdout is the untrusted input", () => {
96 it("reports nothing for a snippet that prints nothing", () => {
97 // The exact shape that used to cost zero and never advance the cap.
98 expect(extractUsageFromStdout("")).toEqual({
99 inputTokens: 0,
100 outputTokens: 0,
101 model: null,
102 });
103 expect(extractUsageFromStdout("hello, no json here")).toEqual({
104 inputTokens: 0,
105 outputTokens: 0,
106 model: null,
107 });
108 });
109
110 it("still reads a well-formed usage block", () => {
111 const got = extractUsageFromStdout(
112 JSON.stringify({ usage: { input_tokens: 100, output_tokens: 50 } })
113 );
114 expect(got.inputTokens).toBe(100);
115 expect(got.outputTokens).toBe(50);
116 });
117});
118
119// --- the floor must actually be applied ------------------------------------
120//
121// Asserting the identifier merely APPEARS is not asserting it is invoked —
122// `void fn;` would satisfy that. Assert the call form. Strip ONLY line
123// comments.
124
125describe("invokeLoop applies the floor to what it charges", () => {
126 const src = (() => {
127 const raw = readFileSync(
128 join(import.meta.dir, "..", "lib", "hosted-claude-loop.ts"),
129 "utf8"
130 );
131 const stripped = raw
132 .split("\n")
133 .filter((l) => !l.trim().startsWith("//") && !l.trim().startsWith("*"))
134 .join("\n");
135 expect(stripped.length).toBeGreaterThan(0);
136 return stripped;
137 })();
138
139 it("takes the max of self-reported cost and the measured floor", () => {
140 expect(src).toContain("invocationFloorCents(execResult.durationMs)");
141 expect(src).toContain("Math.max(");
142 });
143
144 it("charges the floor before the run row is written", () => {
145 // centsEstimate on the run row, the loop rollup and the agent charge all
146 // read the same `cents`, so it has to be floored before any of them.
147 const at = src.indexOf("invocationFloorCents(execResult.durationMs)");
148 expect(at).toBeGreaterThan(-1);
149 const after = src.slice(at);
150 expect(after.length).toBeGreaterThan(0);
151 expect(after).toContain("centsEstimate: cents");
152 expect(after).toContain("bumpLoopTotals(loop.id, cents)");
153 });
154
155 it("still short-circuits over-budget loops before executing", () => {
156 // The floor is worthless if the cap check itself goes away.
157 const at = src.indexOf("getLoopMonthlySpendCents(loop.id)");
158 expect(at).toBeGreaterThan(-1);
159 const window = src.slice(at, at + 400);
160 expect(window.length).toBeGreaterThan(0);
161 expect(window).toContain("loop.monthlyBudgetCents");
162 expect(window).toContain("budget_exceeded");
163 // ...and it happens before the executor is invoked.
164 expect(at).toBeLessThan(src.indexOf("await exec({"));
165 });
166});