Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge remote-tracking branch 'origin/main' into workspace-chat | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| Billing is ready to take real money, and stays on Stripe's test key until the switch. The g1t plan is $21 a month, all-in, with $10 of usage included: the card processing fee is part of the price, and no payment carries a fee line any more (old fee rows still read as they were). Card, billing address, tax IDs, invoices and cancelling are on Stripe's billing page (Manage billing), whose configuration billing keeps in step; Checkout asks for the billing address and a tax ID; g1t's Billing page keeps usage, the spend limit, budget alerts and prepaying. Members other than owners and billing managers no longer see billing details or invoices, from the page or the API. | 4 | import { |
| 5 | answered, | |
| 6 | capReached, | |
| 7 | describeError, | |
| 8 | OWED_GIVE_UP_MS, | |
| 9 | OWED_MAX_DELAY_SECONDS, | |
| 10 | owedDelaySeconds, | |
| 11 | recordOwed, | |
| 12 | stoppedShort, | |
| 13 | tellStopped, | |
| 14 | withinTimeCap, | |
| 15 | } from "./lifecycle.ts"; | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 16 | |
| 17 | const quiet = { waitMs: 0 }; | |
| 18 | ||
| 19 | test("a stop that is told the first time is told once and logs nothing", async () => { | |
| 20 | const logged: unknown[][] = []; | |
| 21 | let calls = 0; | |
| 22 | const told = await tellStopped("checks", async () => void calls++, { ...quiet, log: (...data) => logged.push(data) }); | |
| 23 | assert.equal(told, true); | |
| 24 | assert.equal(calls, 1); | |
| 25 | assert.deepEqual(logged, []); | |
| 26 | }); | |
| 27 | ||
| 28 | test("a stop that fails once is tried again, and gets through", async () => { | |
| 29 | const logged: unknown[][] = []; | |
| 30 | let calls = 0; | |
| 31 | const told = await tellStopped( | |
| 32 | "queue", | |
| 33 | async () => { | |
| 34 | calls++; | |
| 35 | if (calls === 1) throw new Error("report_queue failed with status 500"); | |
| 36 | }, | |
| 37 | { ...quiet, log: (...data) => logged.push(data) }, | |
| 38 | ); | |
| 39 | assert.equal(told, true); | |
| 40 | assert.equal(calls, 2); | |
| 41 | assert.deepEqual(logged, []); | |
| 42 | }); | |
| 43 | ||
| 44 | test("a stop that cannot be told never throws, and is logged with what and why", async () => { | |
| 45 | const logged: unknown[][] = []; | |
| 46 | let calls = 0; | |
| 47 | const told = await tellStopped( | |
| 48 | "actions", | |
| 49 | async () => { | |
| 50 | calls++; | |
| 51 | throw new Error("job_report answered 503"); | |
| 52 | }, | |
| 53 | { ...quiet, log: (...data) => logged.push(data) }, | |
| 54 | ); | |
| 55 | assert.equal(told, false); | |
| 56 | assert.equal(calls, 2); | |
| 57 | assert.deepEqual(logged, [["sandbox stop not reported", "actions", "job_report answered 503"]]); | |
| 58 | }); | |
| 59 | ||
| 60 | test("whatever is thrown, even nothing, is logged as one line", async () => { | |
| 61 | const logged: unknown[][] = []; | |
| 62 | await tellStopped("plan", () => Promise.reject(undefined), { ...quiet, attempts: 1, log: (...data) => logged.push(data) }); | |
| 63 | assert.deepEqual(logged, [["sandbox stop not reported", "plan", "no reason given"]]); | |
| 64 | assert.equal(describeError("gone"), "gone"); | |
| 65 | assert.equal(describeError({ code: 7 }), '{"code":7}'); | |
| 66 | assert.equal(describeError(new TypeError("bad")), "bad"); | |
| 67 | }); | |
| 68 | ||
| 69 | test("a service that cannot answer is a failure; one that refuses is not", async () => { | |
| 70 | await answered("job_report", new Response("{}", { status: 200 })); | |
| 71 | // The deployments service's answer for a build that already finished. | |
| 72 | await answered("deploy fail", new Response("{}", { status: 404 })); | |
| 73 | await answered("job_report", new Response("{}", { status: 409 })); | |
| 74 | await assert.rejects(answered("job_report", new Response("worker threw", { status: 500 })), /job_report answered 500: worker threw/); | |
| 75 | await assert.rejects(answered("deploy fail", new Response("", { status: 503 })), /^Error: deploy fail answered 503$/); | |
| 76 | await assert.rejects(answered("job_report", new Response("slow down", { status: 429 })), /429/); | |
| 77 | }); | |
| 78 | ||
| 79 | test("a run inside its time cap is not stopped for inactivity", () => { | |
| 80 | const started = Date.UTC(2026, 9, 8, 12, 0, 0); | |
| 81 | const minute = 60_000; | |
| 82 | const grace = 180; | |
| 83 | // A 240-minute cap outlives the 100-minute sleepAfter. | |
| 84 | assert.equal(withinTimeCap(started, 240, started + 100 * minute, grace), true); | |
| 85 | assert.equal(withinTimeCap(started, 240, started + 242 * minute, grace), true); | |
| 86 | // Past the cap and its alarm's grace: the library may stop it. | |
| 87 | assert.equal(withinTimeCap(started, 240, started + 243 * minute, grace), false); | |
| 88 | assert.equal(withinTimeCap(started, 90, started + 100 * minute, grace), false); | |
| 89 | }); | |
| 90 | ||
| 91 | test("a sandbox with no cap or no start is stopped for inactivity as before", () => { | |
| 92 | const now = Date.now(); | |
| 93 | assert.equal(withinTimeCap(undefined, 90, now, 180), false); | |
| 94 | assert.equal(withinTimeCap(now - 1000, undefined, now, 180), false); | |
| 95 | assert.equal(withinTimeCap(now - 1000, null, now, 180), false); | |
| 96 | assert.equal(withinTimeCap(now - 1000, 0, now, 180), false); | |
| 97 | }); | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 98 | |
| 99 | test("an earlier attempt's time cap does not stop the attempt running now", () => { | |
| 100 | const minute = 60_000; | |
| 101 | // Desktop release, Linux job: attempt 1 started 12:47:17 with a 60-minute | |
| 102 | // cap, so its alarm fired at 13:50:17, four minutes into attempt 3 in the | |
| 103 | // same Durable Object. | |
| 104 | const attempt3 = Date.UTC(2026, 9, 12, 13, 46, 15); | |
| 105 | const alarm = Date.UTC(2026, 9, 12, 13, 50, 17); | |
| 106 | assert.equal(capReached(attempt3, 60, alarm), false); | |
| 107 | // Its own alarm, at its cap and the alarm's grace, does stop it. | |
| 108 | assert.equal(capReached(attempt3, 60, attempt3 + 63 * minute), true); | |
| 109 | assert.equal(capReached(attempt3, 60, attempt3 + 60 * minute), true); | |
| 110 | assert.equal(capReached(attempt3, 60, attempt3 + 60 * minute - 1), false); | |
| 111 | }); | |
| 112 | ||
| 113 | test("a sandbox with no run or no cap is never stopped for time", () => { | |
| 114 | const now = Date.now(); | |
| 115 | assert.equal(capReached(undefined, 60, now), false); | |
| 116 | assert.equal(capReached(now - 120 * 60_000, undefined, now), false); | |
| 117 | assert.equal(capReached(now - 120 * 60_000, 0, now), false); | |
| 118 | }); | |
| 119 | ||
| 120 | test("a sandbox g1t stopped fails even when its exit code says 0", () => { | |
| 121 | assert.equal(stoppedShort(0, null), false); | |
| 122 | assert.equal(stoppedShort(0, "It ran into its time cap of 60 minutes."), true); | |
| 123 | assert.equal(stoppedShort(137, null), true); | |
| 124 | assert.equal(stoppedShort(1, "why"), true); | |
| 125 | }); | |
| Billing is ready to take real money, and stays on Stripe's test key until the switch. The g1t plan is $21 a month, all-in, with $10 of usage included: the card processing fee is part of the price, and no payment carries a fee line any more (old fee rows still read as they were). Card, billing address, tax IDs, invoices and cancelling are on Stripe's billing page (Manage billing), whose configuration billing keeps in step; Checkout asks for the billing address and a tax ID; g1t's Billing page keeps usage, the spend limit, budget alerts and prepaying. Members other than owners and billing managers no longer see billing details or invoices, from the page or the API. | 126 | |
| 127 | test("sandbox time billing confirms is forgotten; the rest is kept and tried again later", async () => { | |
| 128 | const now = 1_000_000_000; | |
| 129 | const owed = [ | |
| 130 | { usage: { reference: "sandbox/a/1" }, attempts: 0, since: now }, | |
| 131 | { usage: { reference: "sandbox/b/2" }, attempts: 2, since: now - 1000 }, | |
| 132 | ]; | |
| 133 | const sent: string[] = []; | |
| 134 | const result = await recordOwed( | |
| 135 | owed, | |
| 136 | async (usage) => { | |
| 137 | sent.push(usage.reference); | |
| 138 | if (usage.reference === "sandbox/b/2") throw new Error("billing answered 503"); | |
| 139 | return true; | |
| 140 | }, | |
| 141 | now, | |
| 142 | ); | |
| 143 | assert.deepEqual(sent, ["sandbox/a/1", "sandbox/b/2"]); | |
| 144 | assert.deepEqual(result.recorded.map((entry) => entry.usage.reference), ["sandbox/a/1"]); | |
| 145 | // The same usage, under the same reference: billing records it once. | |
| 146 | assert.deepEqual(result.kept, [{ usage: { reference: "sandbox/b/2" }, attempts: 3, since: now - 1000 }]); | |
| 147 | assert.deepEqual(result.lost, []); | |
| 148 | assert.equal(result.retryInSeconds, owedDelaySeconds(3)); | |
| 149 | }); | |
| 150 | ||
| 151 | test("nothing kept means no retry; time tried for a week is given up", async () => { | |
| 152 | const now = 1_000_000_000; | |
| 153 | const done = await recordOwed([{ usage: { reference: "r" }, attempts: 0, since: now }], async () => true, now); | |
| 154 | assert.equal(done.retryInSeconds, null); | |
| 155 | const old = await recordOwed([{ usage: { reference: "r" }, attempts: 40, since: now - OWED_GIVE_UP_MS }], async () => false, now); | |
| 156 | assert.deepEqual(old.kept, []); | |
| 157 | assert.equal(old.lost.length, 1); | |
| 158 | assert.equal(old.retryInSeconds, null); | |
| 159 | }); | |
| 160 | ||
| 161 | test("tries back off from 30 seconds to an hour", () => { | |
| 162 | assert.equal(owedDelaySeconds(0), 30); | |
| 163 | assert.equal(owedDelaySeconds(1), 30); | |
| 164 | assert.equal(owedDelaySeconds(2), 60); | |
| 165 | assert.equal(owedDelaySeconds(5), 480); | |
| 166 | assert.equal(owedDelaySeconds(8), OWED_MAX_DELAY_SECONDS); | |
| 167 | assert.equal(owedDelaySeconds(1000), OWED_MAX_DELAY_SECONDS); | |
| 168 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.