Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepfix/audit-sweep-2026-07-26gatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
hosted-loop-spend-floor.test.ts6.1 KB · 166 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
 * Regression guard: a hosted loop's monthly budget must not be bypassable by
 * simply declining to report usage.
 *
 * The cap itself was fine — invokeLoop short-circuits with `budget_exceeded`
 * once monthly spend reaches monthlyBudgetCents. What fed it was not. Spend
 * came entirely from extractUsageFromStdout, i.e. from a `usage` block
 * printed by the snippet, and loop.sourceCode is arbitrary user-supplied
 * code that any registered account can run (the file says so itself, above
 * the env block).
 *
 * So a snippet that never prints a usage block costed 0 cents, its monthly
 * spend never advanced, and the cap could never stop it: unlimited
 * invocations, each burning up to LOOP_EXEC_TIMEOUT_MS of platform compute,
 * and platform Anthropic credits wherever HOSTED_LOOPS_PLATFORM_KEY=1.
 *
 * The fix charges a floor derived from the executor's own measured duration,
 * which the snippet cannot forge. Self-reported usage still wins when it is
 * higher — it is the better estimate of real token spend and nobody has an
 * incentive to overstate it. The floor only removes the option of reporting
 * nothing.
 *
 * NOTE this is a deliberate billing behaviour change, not purely internal:
 * runs that previously cost 0 now cost at least the per-run minimum. Both
 * knobs are env-tunable and read at access time.
 */

import { describe, it, expect, afterEach } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import {
  invocationFloorCents,
  extractUsageFromStdout,
  LOOP_EXEC_TIMEOUT_MS,
} from "../lib/hosted-claude-loop";

const ENV_MIN = "HOSTED_LOOP_MIN_CENTS_PER_RUN";
const ENV_RATE = "HOSTED_LOOP_MS_PER_CENT";

afterEach(() => {
  delete process.env[ENV_MIN];
  delete process.env[ENV_RATE];
});

describe("invocationFloorCents", () => {
  it("charges at least one cent even for an instant run", () => {
    // This is the whole point: a snippet reporting nothing is not free.
    expect(invocationFloorCents(0)).toBe(1);
    expect(invocationFloorCents(1)).toBe(1);
  });

  it("scales with measured wall clock", () => {
    expect(invocationFloorCents(10_000)).toBe(1);
    expect(invocationFloorCents(10_001)).toBe(2);
    expect(invocationFloorCents(25_000)).toBe(3);
  });

  it("bounds a maximum-length run sensibly", () => {
    // Execution is hard-capped, so the floor per run is bounded too.
    const maxFloor = invocationFloorCents(LOOP_EXEC_TIMEOUT_MS);
    expect(maxFloor).toBeGreaterThan(0);
    expect(maxFloor).toBeLessThanOrEqual(5);
  });

  it("never returns a negative or non-finite charge", () => {
    for (const bad of [-1, -100_000, NaN, Infinity, -Infinity]) {
      const got = invocationFloorCents(bad as number);
      expect(Number.isFinite(got)).toBe(true);
      expect(got).toBeGreaterThanOrEqual(1);
    }
  });

  it("honours the env knobs at access time", () => {
    process.env[ENV_MIN] = "5";
    process.env[ENV_RATE] = "1000";
    expect(invocationFloorCents(0)).toBe(5);
    expect(invocationFloorCents(9_000)).toBe(9);
  });

  it("falls back to safe defaults when the env is nonsense", () => {
    process.env[ENV_MIN] = "not-a-number";
    process.env[ENV_RATE] = "0";
    expect(invocationFloorCents(0)).toBe(1);
    expect(invocationFloorCents(10_000)).toBe(1);
  });

  it("allows the floor to be disabled deliberately", () => {
    // Explicit zero is a real operator choice, not nonsense — it must be
    // honoured rather than silently coerced back to the default.
    process.env[ENV_MIN] = "0";
    expect(invocationFloorCents(0)).toBe(0);
  });
});

describe("extractUsageFromStdout is the untrusted input", () => {
  it("reports nothing for a snippet that prints nothing", () => {
    // The exact shape that used to cost zero and never advance the cap.
    expect(extractUsageFromStdout("")).toEqual({
      inputTokens: 0,
      outputTokens: 0,
      model: null,
    });
    expect(extractUsageFromStdout("hello, no json here")).toEqual({
      inputTokens: 0,
      outputTokens: 0,
      model: null,
    });
  });

  it("still reads a well-formed usage block", () => {
    const got = extractUsageFromStdout(
      JSON.stringify({ usage: { input_tokens: 100, output_tokens: 50 } })
    );
    expect(got.inputTokens).toBe(100);
    expect(got.outputTokens).toBe(50);
  });
});

// --- the floor must actually be applied ------------------------------------
//
// Asserting the identifier merely APPEARS is not asserting it is invoked —
// `void fn;` would satisfy that. Assert the call form. Strip ONLY line
// comments.

describe("invokeLoop applies the floor to what it charges", () => {
  const src = (() => {
    const raw = readFileSync(
      join(import.meta.dir, "..", "lib", "hosted-claude-loop.ts"),
      "utf8"
    );
    const stripped = raw
      .split("\n")
      .filter((l) => !l.trim().startsWith("//") && !l.trim().startsWith("*"))
      .join("\n");
    expect(stripped.length).toBeGreaterThan(0);
    return stripped;
  })();

  it("takes the max of self-reported cost and the measured floor", () => {
    expect(src).toContain("invocationFloorCents(execResult.durationMs)");
    expect(src).toContain("Math.max(");
  });

  it("charges the floor before the run row is written", () => {
    // centsEstimate on the run row, the loop rollup and the agent charge all
    // read the same `cents`, so it has to be floored before any of them.
    const at = src.indexOf("invocationFloorCents(execResult.durationMs)");
    expect(at).toBeGreaterThan(-1);
    const after = src.slice(at);
    expect(after.length).toBeGreaterThan(0);
    expect(after).toContain("centsEstimate: cents");
    expect(after).toContain("bumpLoopTotals(loop.id, cents)");
  });

  it("still short-circuits over-budget loops before executing", () => {
    // The floor is worthless if the cap check itself goes away.
    const at = src.indexOf("getLoopMonthlySpendCents(loop.id)");
    expect(at).toBeGreaterThan(-1);
    const window = src.slice(at, at + 400);
    expect(window.length).toBeGreaterThan(0);
    expect(window).toContain("loop.monthlyBudgetCents");
    expect(window).toContain("budget_exceeded");
    // ...and it happens before the executor is invoked.
    expect(at).toBeLessThan(src.indexOf("await exec({"));
  });
});