Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
270 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 type ComputeEntitlements,
6 type ComputeKind,
7 type PlanMemory,
8 ComputeGate,
9 actualMicros,
10 alwaysPasses,
11 entitlementsKeptSeconds,
12 localRefusal,
13 onBillingError,
14 readEntitlements,
15 refusalCode,
16 refusalMessage,
17 sandboxEstimateMicros,
18} from "../../../packages/contracts/src/compute.ts";
19import { BUILD_HOSTS, buildHosts, dockerFor, withPlanLimits } from "./egress.ts";
20
21const repo = { namespace: "acme", name: "web" };
22
23function ent(change: Partial<ComputeEntitlements> = {}): ComputeEntitlements {
24 return {
25 plan: "paid",
26 compute: true,
27 trialMicrosLeft: 0,
28 trialVerified: true,
29 firstMonth: false,
30 maxRunMinutes: 0,
31 runCapMicros: 2_000_000,
32 ceilingMicros: 100_000_000,
33 exposureMicros: 0,
34 paused: null,
35 ...change,
36 };
37}
38
39/** A billing service that answers as told, and records what it was asked. */
40function billing(answers: {
41 entitlements?: unknown;
42 reserve?: unknown;
43 fail?: Set<string>;
44}) {
45 const asked: { method: string; body: Record<string, unknown> }[] = [];
46 const binding = {
47 async fetch(url: string, init?: RequestInit): Promise<Response> {
48 const method = url.split("/rpc/")[1];
49 asked.push({ method, body: JSON.parse(String(init?.body ?? "{}")) });
50 if (answers.fail?.has(method)) return new Response("down", { status: 503 });
51 if (method === "entitlements") return Response.json(answers.entitlements ?? null);
52 if (method === "reserve") return Response.json(answers.reserve ?? { ok: true, value: { id: "rsv_1", paidBy: "credit" } });
53 if (method === "settle") return Response.json({ ok: true, value: true });
54 if (method === "prices") return Response.json({ prices: [{ meter: "sandbox_second", costMicros: 20 }] });
55 return new Response("not found", { status: 404 });
56 },
57 };
58 return { binding, asked };
59}
60
61/** A plan memory that remembers nothing unless told. */
62function memory(plan: ComputeEntitlements["plan"] | null = null): PlanMemory {
63 return { get: async () => plan, put: async () => undefined };
64}
65
66const quiet = () => undefined;
67
68function wire(e: ComputeEntitlements) {
69 // As billing serialises it: camelCase.
70 return { workspace: "acme", ...e };
71}
72
73const request = (kind: ComputeKind, isPublic = false) => ({
74 workspace: "acme",
75 repo,
76 public: isPublic,
77 kind,
78 estimateMicros: 100_000,
79});
80
81// ---- Gating decisions ---------------------------------------------------------------
82
83test("a paid workspace's start is reserved and goes ahead", async () => {
84 const { binding, asked } = billing({ entitlements: wire(ent()) });
85 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("deploy"));
86 assert.equal(admitted.ok, true);
87 assert.deepEqual(admitted.ok && admitted.reservation, { id: "rsv_1", paidBy: "credit" });
88 const reserve = asked.find((call) => call.method === "reserve")!;
89 assert.deepEqual(reserve.body, { workspace: "acme", repo, public: false, kind: "deploy", estimateMicros: 100_000 });
90});
91
92test("billing's refusal is shown in its own words", async () => {
93 for (const code of ["not_paid", "trial_used", "limit", "oss_pool_empty"] as const) {
94 const { binding } = billing({
95 entitlements: wire(ent({ plan: "free", compute: false })),
96 reserve: { ok: false, error: { code, message: `Refused: ${code}. /acme/-/billing` } },
97 });
98 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("check", true));
99 assert.equal(admitted.ok, false, code);
100 assert.equal(!admitted.ok && admitted.code, code);
101 assert.equal(!admitted.ok && admitted.message, `Refused: ${code}. /acme/-/billing`);
102 }
103});
104
105test("a refusal without words gets g1t's, with what to do and where", async () => {
106 const { binding } = billing({
107 entitlements: wire(ent({ plan: "free", compute: false })),
108 reserve: { ok: false, error: { code: "not_paid", message: "" } },
109 });
110 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("deploy"));
111 assert.equal(!admitted.ok && admitted.message, refusalMessage("not_paid", "acme", "deploy"));
112 assert.match(refusalMessage("not_paid", "acme", "deploy"), /^Deployments need a paid workspace\. Start the \$20 plan or try it with \$5 of free usage after a card check: \/acme\/-\/billing$/);
113});
114
115test("a paused workspace starts nothing, and billing is not asked to reserve", async () => {
116 const { binding, asked } = billing({ entitlements: wire(ent({ paused: "A spend spike is waiting for an owner." })) });
117 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("workflow"));
118 assert.equal(!admitted.ok && admitted.code, "paused");
119 assert.match(!admitted.ok ? admitted.message : "", /A spend spike is waiting for an owner/);
120 assert.equal(asked.some((call) => call.method === "reserve"), false);
121});
122
123test("an answer that holds compute back is kept seconds, one that lets it through half a minute", () => {
124 assert.equal(entitlementsKeptSeconds(ent()), 30);
125 assert.equal(entitlementsKeptSeconds(ent({ paused: "A spend spike is waiting for an owner." })), 3);
126 assert.equal(entitlementsKeptSeconds(ent({ plan: "free", compute: false })), 3);
127 // A free workspace with compute (its trial) runs, so it is kept as long.
128 assert.equal(entitlementsKeptSeconds(ent({ plan: "free", compute: true })), 30);
129});
130
131test("internal and enterprise plans pass even when billing refuses", async () => {
132 for (const plan of ["internal", "enterprise"] as const) {
133 const { binding } = billing({
134 entitlements: wire(ent({ plan })),
135 reserve: { ok: false, error: { code: "limit", message: "Over." } },
136 });
137 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("deploy"));
138 assert.equal(admitted.ok, true, plan);
139 }
140 assert.equal(alwaysPasses("internal") && alwaysPasses("enterprise") && !alwaysPasses("paid"), true);
141});
142
143test("a pause from billing holds for every plan: g1t's own budget and its daily breaker", async () => {
144 for (const plan of ["internal", "enterprise", "paid"] as const) {
145 const message = "flagon-io's monthly budget is used up ($150.00 of $150.00 this month at cost), so new runs wait.";
146 const { binding } = billing({ entitlements: wire(ent({ plan })), reserve: { ok: false, error: { code: "paused", message } } });
147 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("workflow"));
148 assert.equal(!admitted.ok && admitted.code, "paused", plan);
149 assert.equal(!admitted.ok && admitted.message, message);
150 }
151 // Billing's pause reason reads cleanly inside g1t's sentence.
152 assert.equal(
153 refusalMessage("paused", "acme", "workflow", "Its budget is used up."),
154 "g1t paused compute for this workspace: Its budget is used up. Contact support@g1t.sh to have it looked at.",
155 );
156});
157
158test("billing down: free workspaces fail closed, paying ones go on", async () => {
159 const down = new Set(["entitlements", "reserve"]);
160 // Nothing known about the workspace: treated as free.
161 let gate = new ComputeGate(billing({ fail: down }).binding, memory(null), quiet);
162 let admitted = await gate.admit(request("deploy"));
163 assert.equal(!admitted.ok && admitted.code, "billing_unavailable");
164 // Last seen paid: goes ahead, without a reservation.
165 gate = new ComputeGate(billing({ fail: down }).binding, memory("paid"), quiet);
166 admitted = await gate.admit(request("deploy"));
167 assert.deepEqual(admitted, { ok: true, reservation: null, entitlements: null });
168 // Entitlements answer, reserve errors: the plan decides.
169 gate = new ComputeGate(billing({ entitlements: wire(ent({ plan: "free", compute: false, trialMicrosLeft: 5_000_000 })), fail: new Set(["reserve"]) }).binding, memory(), quiet);
170 admitted = await gate.admit(request("deploy"));
171 assert.equal(admitted.ok, false);
172 gate = new ComputeGate(billing({ entitlements: wire(ent()), fail: new Set(["reserve"]) }).binding, memory(), quiet);
173 assert.equal((await gate.admit(request("deploy"))).ok, true);
174 assert.equal(onBillingError("free"), "refuse");
175 assert.equal(onBillingError(null), "refuse");
176 assert.equal(onBillingError("paid"), "allow");
177});
178
179test("an answer that is not a refusal counts as billing being down", async () => {
180 const { binding } = billing({
181 entitlements: wire(ent({ plan: "free", compute: false })),
182 reserve: { ok: false, error: { code: "invalid", message: "Unknown method" } },
183 });
184 const admitted = await new ComputeGate(binding, memory(), quiet).admit(request("deploy"));
185 assert.equal(!admitted.ok && admitted.code, "billing_unavailable");
186 assert.equal(refusalCode({ code: "payment_required" }), "not_paid");
187 assert.equal(refusalCode({ code: "conflict", reason: "trial_used" }), "trial_used");
188 assert.equal(refusalCode({ code: "invalid" }), null);
189});
190
191test("entitlements read in either case, and not at all without a plan", () => {
192 const snake = readEntitlements({ plan: "free", trial_micros_left: 5, trial_verified: true, max_run_minutes: 30, paused: "" });
193 assert.equal(snake?.trialMicrosLeft, 5);
194 assert.equal(snake?.maxRunMinutes, 30);
195 assert.equal(snake?.paused, null);
196 assert.equal(readEntitlements({ team: true })?.plan, undefined);
197 assert.equal(readEntitlements({ ok: true, value: { plan: "paid" } })?.plan, "paid");
198});
199
200test("before they try: what a free workspace can start, by kind and repository", () => {
201 const free = ent({ plan: "free", compute: false, trialVerified: false });
202 assert.equal(localRefusal(free, "deploy", false), "not_paid");
203 assert.equal(localRefusal(free, "check", true), "not_paid", "the pool needs a card check");
204 const verified = ent({ plan: "free", compute: false, trialVerified: true, trialMicrosLeft: 0 });
205 // The open-source path: public repositories' checks, workflows and queue.
206 for (const kind of ["check", "workflow", "queue"] as const) assert.equal(localRefusal(verified, kind, true), null, kind);
207 assert.equal(localRefusal(verified, "deploy", true), "trial_used");
208 assert.equal(localRefusal(verified, "check", false), "trial_used");
209 const trial = ent({ plan: "free", compute: true, trialVerified: true, trialMicrosLeft: 4_000_000 });
210 assert.equal(localRefusal(trial, "deploy", false), null);
211 assert.equal(localRefusal(ent(), "deploy", false), null);
212 assert.equal(localRefusal(ent({ paused: "Held." }), "deploy", false), "paused");
213});
214
215// ---- Estimates ------------------------------------------------------------------------
216
217test("an estimate is the sandbox for its whole time cap", () => {
218 assert.equal(sandboxEstimateMicros(60, 25), 90_000);
219 assert.equal(sandboxEstimateMicros(-5, 25), 0);
220});
221
222test("what work cost: its seconds at the sandbox price", () => {
223 assert.equal(actualMicros(600, 20), 12_000);
224 assert.equal(actualMicros(-10, 20), 0);
225});
226
227test("the gate reads the sandbox price from the price book", async () => {
228 const gate = new ComputeGate(billing({}).binding, memory(), quiet);
229 assert.equal(await gate.microsPerSecond(), 20);
230 const down = new ComputeGate(billing({ fail: new Set(["prices"]) }).binding, memory(), quiet);
231 assert.equal(await down.microsPerSecond(), 63);
232});
233
234// ---- Caps -------------------------------------------------------------------------------
235
236test("caps are the lower of the guardrails' and the plan's", () => {
237 const guard = { policy: { budgetUsd: 5 } as never, minutes: 90 };
238 const limited = withPlanLimits(guard as never, { minutes: 60, budgetUsd: 2 });
239 assert.equal(limited.minutes, 60);
240 assert.equal((limited.policy as { budgetUsd: number }).budgetUsd, 2);
241 const unlimited = withPlanLimits(guard as never, { minutes: null, budgetUsd: null });
242 assert.equal(unlimited.minutes, 90);
243 assert.equal((unlimited.policy as { budgetUsd: number }).budgetUsd, 5);
244});
245
246// ---- Builds' network ---------------------------------------------------------------------
247
248test("builds reach registries and git hosts, and no mining pool", () => {
249 for (const host of ["registry.npmjs.org", "codeload.github.com", "nodejs.org", "crates.io"]) {
250 assert.ok(BUILD_HOSTS.includes(host), host);
251 }
252 // A job's Docker Engine pulls from Docker Hub (through its mirror) and Quay.
253 for (const host of ["registry-1.docker.io", "auth.docker.io", "production.cloudflare.docker.com", "mirror.gcr.io", "ghcr.io", "quay.io"]) {
254 assert.ok(BUILD_HOSTS.includes(host), host);
255 }
256 assert.ok(buildHosts("deploy").includes("api.cloudflare.com"));
257 // A container build pushes its image to Cloudflare's registry, from a sandbox or a job.
258 assert.ok(buildHosts("deploy").includes("registry.cloudflare.com"));
259 assert.ok(buildHosts("actions").includes("registry.cloudflare.com"));
260 assert.ok(!buildHosts("actions").includes("api.cloudflare.com"));
261 for (const host of buildHosts("deploy")) {
262 assert.doesNotMatch(host, /pool|xmr|monero|nicehash|^\*/, host);
263 }
264});
265
266test("workflow jobs get Docker unless the operator turns it off", () => {
267 assert.equal(dockerFor(undefined), "on");
268 assert.equal(dockerFor("on"), "on");
269 assert.equal(dockerFor(" OFF "), "off");
270});