Skip to content
114 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { answered, capReached, describeError, stoppedShort, tellStopped, withinTimeCap } from "./lifecycle.ts";
5
6const quiet = { waitMs: 0 };
7
8test("a stop that is told the first time is told once and logs nothing", async () => {
9 const logged: unknown[][] = [];
10 let calls = 0;
11 const told = await tellStopped("checks", async () => void calls++, { ...quiet, log: (...data) => logged.push(data) });
12 assert.equal(told, true);
13 assert.equal(calls, 1);
14 assert.deepEqual(logged, []);
15});
16
17test("a stop that fails once is tried again, and gets through", async () => {
18 const logged: unknown[][] = [];
19 let calls = 0;
20 const told = await tellStopped(
21 "queue",
22 async () => {
23 calls++;
24 if (calls === 1) throw new Error("report_queue failed with status 500");
25 },
26 { ...quiet, log: (...data) => logged.push(data) },
27 );
28 assert.equal(told, true);
29 assert.equal(calls, 2);
30 assert.deepEqual(logged, []);
31});
32
33test("a stop that cannot be told never throws, and is logged with what and why", async () => {
34 const logged: unknown[][] = [];
35 let calls = 0;
36 const told = await tellStopped(
37 "actions",
38 async () => {
39 calls++;
40 throw new Error("job_report answered 503");
41 },
42 { ...quiet, log: (...data) => logged.push(data) },
43 );
44 assert.equal(told, false);
45 assert.equal(calls, 2);
46 assert.deepEqual(logged, [["sandbox stop not reported", "actions", "job_report answered 503"]]);
47});
48
49test("whatever is thrown, even nothing, is logged as one line", async () => {
50 const logged: unknown[][] = [];
51 await tellStopped("plan", () => Promise.reject(undefined), { ...quiet, attempts: 1, log: (...data) => logged.push(data) });
52 assert.deepEqual(logged, [["sandbox stop not reported", "plan", "no reason given"]]);
53 assert.equal(describeError("gone"), "gone");
54 assert.equal(describeError({ code: 7 }), '{"code":7}');
55 assert.equal(describeError(new TypeError("bad")), "bad");
56});
57
58test("a service that cannot answer is a failure; one that refuses is not", async () => {
59 await answered("job_report", new Response("{}", { status: 200 }));
60 // The deployments service's answer for a build that already finished.
61 await answered("deploy fail", new Response("{}", { status: 404 }));
62 await answered("job_report", new Response("{}", { status: 409 }));
63 await assert.rejects(answered("job_report", new Response("worker threw", { status: 500 })), /job_report answered 500: worker threw/);
64 await assert.rejects(answered("deploy fail", new Response("", { status: 503 })), /^Error: deploy fail answered 503$/);
65 await assert.rejects(answered("job_report", new Response("slow down", { status: 429 })), /429/);
66});
67
68test("a run inside its time cap is not stopped for inactivity", () => {
69 const started = Date.UTC(2026, 9, 8, 12, 0, 0);
70 const minute = 60_000;
71 const grace = 180;
72 // A 240-minute cap outlives the 100-minute sleepAfter.
73 assert.equal(withinTimeCap(started, 240, started + 100 * minute, grace), true);
74 assert.equal(withinTimeCap(started, 240, started + 242 * minute, grace), true);
75 // Past the cap and its alarm's grace: the library may stop it.
76 assert.equal(withinTimeCap(started, 240, started + 243 * minute, grace), false);
77 assert.equal(withinTimeCap(started, 90, started + 100 * minute, grace), false);
78});
79
80test("a sandbox with no cap or no start is stopped for inactivity as before", () => {
81 const now = Date.now();
82 assert.equal(withinTimeCap(undefined, 90, now, 180), false);
83 assert.equal(withinTimeCap(now - 1000, undefined, now, 180), false);
84 assert.equal(withinTimeCap(now - 1000, null, now, 180), false);
85 assert.equal(withinTimeCap(now - 1000, 0, now, 180), false);
86});
87
88test("an earlier attempt's time cap does not stop the attempt running now", () => {
89 const minute = 60_000;
90 // Desktop release, Linux job: attempt 1 started 12:47:17 with a 60-minute
91 // cap, so its alarm fired at 13:50:17, four minutes into attempt 3 in the
92 // same Durable Object.
93 const attempt3 = Date.UTC(2026, 9, 12, 13, 46, 15);
94 const alarm = Date.UTC(2026, 9, 12, 13, 50, 17);
95 assert.equal(capReached(attempt3, 60, alarm), false);
96 // Its own alarm, at its cap and the alarm's grace, does stop it.
97 assert.equal(capReached(attempt3, 60, attempt3 + 63 * minute), true);
98 assert.equal(capReached(attempt3, 60, attempt3 + 60 * minute), true);
99 assert.equal(capReached(attempt3, 60, attempt3 + 60 * minute - 1), false);
100});
101
102test("a sandbox with no run or no cap is never stopped for time", () => {
103 const now = Date.now();
104 assert.equal(capReached(undefined, 60, now), false);
105 assert.equal(capReached(now - 120 * 60_000, undefined, now), false);
106 assert.equal(capReached(now - 120 * 60_000, 0, now), false);
107});
108
109test("a sandbox g1t stopped fails even when its exit code says 0", () => {
110 assert.equal(stoppedShort(0, null), false);
111 assert.equal(stoppedShort(0, "It ran into its time cap of 60 minutes."), true);
112 assert.equal(stoppedShort(137, null), true);
113 assert.equal(stoppedShort(1, "why"), true);
114});