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.

179 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 answered,
6 capReached,
7 describeError,
8 noMachine,
9 OWED_GIVE_UP_MS,
10 OWED_MAX_DELAY_SECONDS,
11 owedDelaySeconds,
12 recordOwed,
13 stoppedShort,
14 tellStopped,
15 withinTimeCap,
16} from "./lifecycle.ts";
17
18const quiet = { waitMs: 0 };
19
20test("a stop that is told the first time is told once and logs nothing", async () => {
21 const logged: unknown[][] = [];
22 let calls = 0;
23 const told = await tellStopped("checks", async () => void calls++, { ...quiet, log: (...data) => logged.push(data) });
24 assert.equal(told, true);
25 assert.equal(calls, 1);
26 assert.deepEqual(logged, []);
27});
28
29test("a stop that fails once is tried again, and gets through", async () => {
30 const logged: unknown[][] = [];
31 let calls = 0;
32 const told = await tellStopped(
33 "queue",
34 async () => {
35 calls++;
36 if (calls === 1) throw new Error("report_queue failed with status 500");
37 },
38 { ...quiet, log: (...data) => logged.push(data) },
39 );
40 assert.equal(told, true);
41 assert.equal(calls, 2);
42 assert.deepEqual(logged, []);
43});
44
45test("a stop that cannot be told never throws, and is logged with what and why", async () => {
46 const logged: unknown[][] = [];
47 let calls = 0;
48 const told = await tellStopped(
49 "actions",
50 async () => {
51 calls++;
52 throw new Error("job_report answered 503");
53 },
54 { ...quiet, log: (...data) => logged.push(data) },
55 );
56 assert.equal(told, false);
57 assert.equal(calls, 2);
58 assert.deepEqual(logged, [["sandbox stop not reported", "actions", "job_report answered 503"]]);
59});
60
61test("whatever is thrown, even nothing, is logged as one line", async () => {
62 const logged: unknown[][] = [];
63 await tellStopped("plan", () => Promise.reject(undefined), { ...quiet, attempts: 1, log: (...data) => logged.push(data) });
64 assert.deepEqual(logged, [["sandbox stop not reported", "plan", "no reason given"]]);
65 assert.equal(describeError("gone"), "gone");
66 assert.equal(describeError({ code: 7 }), '{"code":7}');
67 assert.equal(describeError(new TypeError("bad")), "bad");
68});
69
70test("a service that cannot answer is a failure; one that refuses is not", async () => {
71 await answered("job_report", new Response("{}", { status: 200 }));
72 // The deployments service's answer for a build that already finished.
73 await answered("deploy fail", new Response("{}", { status: 404 }));
74 await answered("job_report", new Response("{}", { status: 409 }));
75 await assert.rejects(answered("job_report", new Response("worker threw", { status: 500 })), /job_report answered 500: worker threw/);
76 await assert.rejects(answered("deploy fail", new Response("", { status: 503 })), /^Error: deploy fail answered 503$/);
77 await assert.rejects(answered("job_report", new Response("slow down", { status: 429 })), /429/);
78});
79
80test("a run inside its time cap is not stopped for inactivity", () => {
81 const started = Date.UTC(2026, 9, 8, 12, 0, 0);
82 const minute = 60_000;
83 const grace = 180;
84 // A 240-minute cap outlives the 100-minute sleepAfter.
85 assert.equal(withinTimeCap(started, 240, started + 100 * minute, grace), true);
86 assert.equal(withinTimeCap(started, 240, started + 242 * minute, grace), true);
87 // Past the cap and its alarm's grace: the library may stop it.
88 assert.equal(withinTimeCap(started, 240, started + 243 * minute, grace), false);
89 assert.equal(withinTimeCap(started, 90, started + 100 * minute, grace), false);
90});
91
92test("a sandbox with no cap or no start is stopped for inactivity as before", () => {
93 const now = Date.now();
94 assert.equal(withinTimeCap(undefined, 90, now, 180), false);
95 assert.equal(withinTimeCap(now - 1000, undefined, now, 180), false);
96 assert.equal(withinTimeCap(now - 1000, null, now, 180), false);
97 assert.equal(withinTimeCap(now - 1000, 0, now, 180), false);
98});
99
100test("an earlier attempt's time cap does not stop the attempt running now", () => {
101 const minute = 60_000;
102 // Desktop release, Linux job: attempt 1 started 12:47:17 with a 60-minute
103 // cap, so its alarm fired at 13:50:17, four minutes into attempt 3 in the
104 // same Durable Object.
105 const attempt3 = Date.UTC(2026, 9, 12, 13, 46, 15);
106 const alarm = Date.UTC(2026, 9, 12, 13, 50, 17);
107 assert.equal(capReached(attempt3, 60, alarm), false);
108 // Its own alarm, at its cap and the alarm's grace, does stop it.
109 assert.equal(capReached(attempt3, 60, attempt3 + 63 * minute), true);
110 assert.equal(capReached(attempt3, 60, attempt3 + 60 * minute), true);
111 assert.equal(capReached(attempt3, 60, attempt3 + 60 * minute - 1), false);
112});
113
114test("a sandbox with no run or no cap is never stopped for time", () => {
115 const now = Date.now();
116 assert.equal(capReached(undefined, 60, now), false);
117 assert.equal(capReached(now - 120 * 60_000, undefined, now), false);
118 assert.equal(capReached(now - 120 * 60_000, 0, now), false);
119});
120
121test("a sandbox g1t stopped fails even when its exit code says 0", () => {
122 assert.equal(stoppedShort(0, null), false);
123 assert.equal(stoppedShort(0, "It ran into its time cap of 60 minutes."), true);
124 assert.equal(stoppedShort(137, null), true);
125 assert.equal(stoppedShort(1, "why"), true);
126});
127
128test("sandbox time billing confirms is forgotten; the rest is kept and tried again later", async () => {
129 const now = 1_000_000_000;
130 const owed = [
131 { usage: { reference: "sandbox/a/1" }, attempts: 0, since: now },
132 { usage: { reference: "sandbox/b/2" }, attempts: 2, since: now - 1000 },
133 ];
134 const sent: string[] = [];
135 const result = await recordOwed(
136 owed,
137 async (usage) => {
138 sent.push(usage.reference);
139 if (usage.reference === "sandbox/b/2") throw new Error("billing answered 503");
140 return true;
141 },
142 now,
143 );
144 assert.deepEqual(sent, ["sandbox/a/1", "sandbox/b/2"]);
145 assert.deepEqual(
146 result.recorded.map((entry) => entry.usage.reference),
147 ["sandbox/a/1"],
148 );
149 // The same usage, under the same reference: billing records it once.
150 assert.deepEqual(result.kept, [{ usage: { reference: "sandbox/b/2" }, attempts: 3, since: now - 1000 }]);
151 assert.deepEqual(result.lost, []);
152 assert.equal(result.retryInSeconds, owedDelaySeconds(3));
153});
154
155test("nothing kept means no retry; time tried for a week is given up", async () => {
156 const now = 1_000_000_000;
157 const done = await recordOwed([{ usage: { reference: "r" }, attempts: 0, since: now }], async () => true, now);
158 assert.equal(done.retryInSeconds, null);
159 const old = await recordOwed([{ usage: { reference: "r" }, attempts: 40, since: now - OWED_GIVE_UP_MS }], async () => false, now);
160 assert.deepEqual(old.kept, []);
161 assert.equal(old.lost.length, 1);
162 assert.equal(old.retryInSeconds, null);
163});
164
165test("tries back off from 30 seconds to an hour", () => {
166 assert.equal(owedDelaySeconds(0), 30);
167 assert.equal(owedDelaySeconds(1), 30);
168 assert.equal(owedDelaySeconds(2), 60);
169 assert.equal(owedDelaySeconds(5), 480);
170 assert.equal(owedDelaySeconds(8), OWED_MAX_DELAY_SECONDS);
171 assert.equal(owedDelaySeconds(1000), OWED_MAX_DELAY_SECONDS);
172});
173
174test("a sandbox Cloudflare has no container for waits for a machine; any other failure does not", () => {
175 assert.equal(noMachine(new Error("there is no container instance that can be provided to this durable object")), true);
176 assert.equal(noMachine("Error: There is no Container instance available at this time"), true);
177 assert.equal(noMachine(new Error("the container exited with code 1")), false);
178 assert.equal(noMachine(undefined), false);
179});