Skip to content
86 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { answered, describeError, 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});