@norskvideo/ctl-test-harness 0.1.70 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/daemon.d.ts +16 -0
- package/daemon.js +45 -2
- package/package.json +1 -1
package/daemon.d.ts
CHANGED
|
@@ -110,11 +110,24 @@ export declare function daemonSpawnEnv(o: {
|
|
|
110
110
|
overrides?: Record<string, string>;
|
|
111
111
|
}): Record<string, string | undefined>;
|
|
112
112
|
export declare function startDaemon(storeDir: string, options?: StartDaemonOptions): StartedDaemon;
|
|
113
|
+
/** How long one `instance delete` may take before teardown stops waiting for
|
|
114
|
+
* it and reaps the instance's containers itself. A healthy delete is seconds. */
|
|
115
|
+
export declare const INSTANCE_DELETE_TIMEOUT_MS = 20000;
|
|
113
116
|
/**
|
|
114
117
|
* Tear down a daemon-based test: delete instances, stop proxy, stop daemon, kill
|
|
115
118
|
* process, rm store. The teardown actions are injected as pre-bound thunks
|
|
116
119
|
* (`deleteInstance`/`stopProxy`/`stopDaemon`) — the caller binds them to
|
|
117
120
|
* norsk-ctl's typed CLI, so no command type leaks into this package.
|
|
121
|
+
*
|
|
122
|
+
* Deleting through the daemon is not enough on its own. When bun times a test
|
|
123
|
+
* out it kills the test's dangling processes, the daemon among them, and the
|
|
124
|
+
* afterAll that then calls this has a dead daemon to ask: every delete fails
|
|
125
|
+
* (or hangs into the hook's own timeout) and the instance's containers run on
|
|
126
|
+
* with nothing left that knows they exist. commentary's publish-docs left a
|
|
127
|
+
* composition guide's engine running that way for 2.6 hours on 2026-10-03; it
|
|
128
|
+
* grew to 28 GB and the host OOM-killed the next morning's product CI. So each
|
|
129
|
+
* delete is bounded, and whatever an instance leaves behind is then removed by
|
|
130
|
+
* its `norsk-ctl.instance` label, which needs only Docker.
|
|
118
131
|
*/
|
|
119
132
|
export declare function cleanupDaemon(opts: {
|
|
120
133
|
deleteInstance: (id: string) => Promise<unknown>;
|
|
@@ -125,6 +138,9 @@ export declare function cleanupDaemon(opts: {
|
|
|
125
138
|
daemon: DaemonProcess | null;
|
|
126
139
|
storeDir: string;
|
|
127
140
|
containers: string[];
|
|
141
|
+
/** Injected for tests; defaults to the Docker-label reap. */
|
|
142
|
+
reapInstance?: (id: string) => string[];
|
|
143
|
+
deleteTimeoutMs?: number;
|
|
128
144
|
}): Promise<void>;
|
|
129
145
|
/** Tear down a direct-API test: stop proxy, remove instances, rm temp dir. */
|
|
130
146
|
export declare function cleanupDirect(opts: {
|
package/daemon.js
CHANGED
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { cliCommand } from "./cli-command.js";
|
|
16
16
|
import { rememberDaemonStore } from "./daemon-stores.js";
|
|
17
17
|
import { pollUntil } from "./poll.js";
|
|
18
|
+
import { reapInstance } from "./reap.js";
|
|
18
19
|
import { makeStoreDir, makeTempDir, TEST_TMP_BASE } from "./temp-dir.js";
|
|
19
20
|
export const HEALTH_TIMEOUT_MS = 120_000;
|
|
20
21
|
// The daemon's readiness probe (/api/ready) cannot answer until `serve` has
|
|
@@ -28,6 +29,22 @@ export const HEALTH_TIMEOUT_MS = 120_000;
|
|
|
28
29
|
export const DAEMON_READY_TIMEOUT_MS = 60_000;
|
|
29
30
|
export { makeStoreDir, makeTempDir, pollUntil, TEST_TMP_BASE };
|
|
30
31
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
/** "done" once `p` settles, or "timeout" after `ms` — whichever is first. A
|
|
33
|
+
* rejection inside the window rejects; one after it is dropped (the caller has
|
|
34
|
+
* moved on). */
|
|
35
|
+
async function settleWithin(p, ms) {
|
|
36
|
+
let timer;
|
|
37
|
+
const late = new Promise((resolve) => {
|
|
38
|
+
timer = setTimeout(() => resolve("timeout"), ms);
|
|
39
|
+
});
|
|
40
|
+
p.catch(() => { });
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.race([p.then(() => "done"), late]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
31
48
|
function splitLines(text) {
|
|
32
49
|
return text
|
|
33
50
|
.split("\n")
|
|
@@ -301,22 +318,48 @@ async function awaitContainerShutdown(containers) {
|
|
|
301
318
|
if (still.length)
|
|
302
319
|
throw new Error(`Containers still running after cleanup: ${still.join(", ")}`);
|
|
303
320
|
}
|
|
321
|
+
/** How long one `instance delete` may take before teardown stops waiting for
|
|
322
|
+
* it and reaps the instance's containers itself. A healthy delete is seconds. */
|
|
323
|
+
export const INSTANCE_DELETE_TIMEOUT_MS = 20_000;
|
|
304
324
|
/**
|
|
305
325
|
* Tear down a daemon-based test: delete instances, stop proxy, stop daemon, kill
|
|
306
326
|
* process, rm store. The teardown actions are injected as pre-bound thunks
|
|
307
327
|
* (`deleteInstance`/`stopProxy`/`stopDaemon`) — the caller binds them to
|
|
308
328
|
* norsk-ctl's typed CLI, so no command type leaks into this package.
|
|
329
|
+
*
|
|
330
|
+
* Deleting through the daemon is not enough on its own. When bun times a test
|
|
331
|
+
* out it kills the test's dangling processes, the daemon among them, and the
|
|
332
|
+
* afterAll that then calls this has a dead daemon to ask: every delete fails
|
|
333
|
+
* (or hangs into the hook's own timeout) and the instance's containers run on
|
|
334
|
+
* with nothing left that knows they exist. commentary's publish-docs left a
|
|
335
|
+
* composition guide's engine running that way for 2.6 hours on 2026-10-03; it
|
|
336
|
+
* grew to 28 GB and the host OOM-killed the next morning's product CI. So each
|
|
337
|
+
* delete is bounded, and whatever an instance leaves behind is then removed by
|
|
338
|
+
* its `norsk-ctl.instance` label, which needs only Docker.
|
|
309
339
|
*/
|
|
310
340
|
export async function cleanupDaemon(opts) {
|
|
341
|
+
const deleteTimeoutMs = opts.deleteTimeoutMs ?? INSTANCE_DELETE_TIMEOUT_MS;
|
|
311
342
|
for (const id of opts.instances ?? []) {
|
|
312
343
|
try {
|
|
313
|
-
await opts.deleteInstance(id)
|
|
344
|
+
if ((await settleWithin(opts.deleteInstance(id), deleteTimeoutMs)) === "timeout")
|
|
345
|
+
console.error(`cleanup: delete ${id} gave no answer in ${deleteTimeoutMs}ms`);
|
|
314
346
|
}
|
|
315
347
|
catch (e) {
|
|
316
348
|
console.error(`cleanup: delete ${id} failed: ${e}`);
|
|
317
349
|
}
|
|
318
350
|
}
|
|
319
|
-
|
|
351
|
+
const reap = opts.reapInstance ?? reapInstance;
|
|
352
|
+
for (const id of opts.instances ?? []) {
|
|
353
|
+
const left = reap(id);
|
|
354
|
+
if (left.length > 0)
|
|
355
|
+
console.error(`cleanup: removed ${left.length} container(s) instance ${id} left running`);
|
|
356
|
+
}
|
|
357
|
+
// Stop the proxy unless the caller says it never ran one (`proxy: false`).
|
|
358
|
+
// It used to need `proxy: true`, and three callers passing a real stopProxy
|
|
359
|
+
// omitted it — so `shutdown --daemon-only` left their proxy pair running.
|
|
360
|
+
// On the shared ar-arm-vm runner a leftover pair then answered for the next
|
|
361
|
+
// job's proxy (2026-10-02, norsk-ctl Checks 37048720935).
|
|
362
|
+
if (opts.proxy ?? true)
|
|
320
363
|
try {
|
|
321
364
|
await opts.stopProxy();
|
|
322
365
|
}
|