@norskvideo/ctl-test-harness 0.1.18 → 0.1.19
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 +9 -0
- package/daemon.js +31 -9
- package/demo/cli.js +1 -1
- package/demo/index.d.ts +1 -1
- package/demo/index.js +1 -1
- package/demo/run.d.ts +3 -0
- package/demo/run.js +29 -11
- package/package.json +1 -1
package/daemon.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export { makeStoreDir, makeTempDir, pollUntil, TEST_TMP_BASE };
|
|
|
6
6
|
export declare function requireLicenseFile(opts?: {
|
|
7
7
|
missing?: "exit" | "throw";
|
|
8
8
|
}): string;
|
|
9
|
+
/** What a caller sees when the CLI itself cannot be started: in a product
|
|
10
|
+
* repo outside its nix shell nothing resolves (cli-command.ts), and spawn's
|
|
11
|
+
* ENOENT must read as one line, not an uncaught 'error' event. */
|
|
12
|
+
export declare function cliUnavailableMessage(command: readonly string[], cause: string): string;
|
|
13
|
+
export declare function runCliWith(command: readonly string[], storeDir: string, ...args: string[]): Promise<{
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
exitCode: number;
|
|
17
|
+
}>;
|
|
9
18
|
export declare function runCli(storeDir: string, ...args: string[]): Promise<{
|
|
10
19
|
stdout: string;
|
|
11
20
|
stderr: string;
|
package/daemon.js
CHANGED
|
@@ -50,17 +50,30 @@ export function requireLicenseFile(opts = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
return licenseFile;
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/** What a caller sees when the CLI itself cannot be started: in a product
|
|
54
|
+
* repo outside its nix shell nothing resolves (cli-command.ts), and spawn's
|
|
55
|
+
* ENOENT must read as one line, not an uncaught 'error' event. */
|
|
56
|
+
export function cliUnavailableMessage(command, cause) {
|
|
57
|
+
return `could not run ${command.join(" ")}: ${cause} — set NORSK_CTL_BINARY to a norsk-ctl binary, or run inside the product's \`nix develop .#dev\` shell, which puts the pinned release on PATH`;
|
|
58
|
+
}
|
|
59
|
+
export async function runCliWith(command, storeDir, ...args) {
|
|
54
60
|
return new Promise((res) => {
|
|
55
|
-
const [cmd, ...cmdArgs] =
|
|
61
|
+
const [cmd, ...cmdArgs] = command;
|
|
56
62
|
const proc = spawn(cmd, [...cmdArgs, ...args], {
|
|
57
63
|
env: { ...process.env, NORSK_CTL_STORE_DIR: storeDir },
|
|
58
64
|
});
|
|
59
65
|
const stdout = [];
|
|
60
66
|
const stderr = [];
|
|
67
|
+
let failed = false;
|
|
61
68
|
proc.stdout.on("data", (d) => stdout.push(d));
|
|
62
69
|
proc.stderr.on("data", (d) => stderr.push(d));
|
|
70
|
+
proc.on("error", (e) => {
|
|
71
|
+
failed = true;
|
|
72
|
+
res({ stdout: "", stderr: cliUnavailableMessage(command, e.message), exitCode: 127 });
|
|
73
|
+
});
|
|
63
74
|
proc.on("close", (code) => {
|
|
75
|
+
if (failed)
|
|
76
|
+
return;
|
|
64
77
|
res({
|
|
65
78
|
stdout: Buffer.concat(stdout).toString(),
|
|
66
79
|
stderr: Buffer.concat(stderr).toString(),
|
|
@@ -69,6 +82,9 @@ export async function runCli(storeDir, ...args) {
|
|
|
69
82
|
});
|
|
70
83
|
});
|
|
71
84
|
}
|
|
85
|
+
export async function runCli(storeDir, ...args) {
|
|
86
|
+
return runCliWith(cliCommand, storeDir, ...args);
|
|
87
|
+
}
|
|
72
88
|
export function isPortFree(port) {
|
|
73
89
|
return new Promise((resolve) => {
|
|
74
90
|
const srv = createServer();
|
|
@@ -127,14 +143,20 @@ export function startDaemon(storeDir, options) {
|
|
|
127
143
|
detached: options?.detached === true,
|
|
128
144
|
env: { ...process.env, NORSK_CTL_STORE_DIR: storeDir, NORSK_CTL_PORT: String(port), ...options?.env },
|
|
129
145
|
});
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
return res.ok;
|
|
133
|
-
}, {
|
|
134
|
-
timeoutMs: options?.readyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS,
|
|
135
|
-
intervalMs: 500,
|
|
136
|
-
label: "Daemon did not become ready",
|
|
146
|
+
const spawnFailed = new Promise((_, reject) => {
|
|
147
|
+
proc.once("error", (e) => reject(new Error(cliUnavailableMessage(cliCommand, e.message))));
|
|
137
148
|
});
|
|
149
|
+
const ready = Promise.race([
|
|
150
|
+
spawnFailed,
|
|
151
|
+
pollUntil(async () => {
|
|
152
|
+
const res = await fetch(`http://localhost:${port}/api/ready`, { signal: AbortSignal.timeout(1000) });
|
|
153
|
+
return res.ok;
|
|
154
|
+
}, {
|
|
155
|
+
timeoutMs: options?.readyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS,
|
|
156
|
+
intervalMs: 500,
|
|
157
|
+
label: "Daemon did not become ready",
|
|
158
|
+
}),
|
|
159
|
+
]);
|
|
138
160
|
return { daemon: daemonHandle(proc), ready };
|
|
139
161
|
}
|
|
140
162
|
function runningContainers(names) {
|
package/demo/cli.js
CHANGED
|
@@ -91,7 +91,7 @@ export function resolvedSpecView(spec, opts) {
|
|
|
91
91
|
if ("control" in ref)
|
|
92
92
|
return `http://localhost:${ports.backendPort}${ref.control}`;
|
|
93
93
|
if ("proxy" in ref)
|
|
94
|
-
return `
|
|
94
|
+
return `http://localhost:${ports.proxyPort}${ref.proxy.replaceAll("{id}", instanceId)}`;
|
|
95
95
|
return `http://localhost:${studioHostPort}${ref.studio}`;
|
|
96
96
|
};
|
|
97
97
|
const t = spec.template;
|
package/demo/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { DemoArgs, DemoCliIo, ResolvedSpecView } from "./cli.js";
|
|
2
2
|
export { defaultDemoCliIo, demoMain, parseDemoArgs, resolvedSpecView } from "./cli.js";
|
|
3
3
|
export type { CliResult, DemoDeps, DemoPorts, DemoRunOptions, DemoRunResult, DemoState, DemoStateStore, DemoTimeouts, ExportCheckOptions, IngestPortRow, ProcessHandle, TemplateParams, } from "./run.js";
|
|
4
|
-
export { defaultDemoDeps, demoDown, demoPorts, demoSlug, expandHome, fileStateStore, findBrokenSymlinks, resolveIngestPort, runDemo, runExportCheck, } from "./run.js";
|
|
4
|
+
export { defaultDemoDeps, demoDown, demoPorts, demoSlug, expandHome, fileStateStore, findBrokenSymlinks, killGroup, resolveIngestPort, runDemo, runExportCheck, } from "./run.js";
|
|
5
5
|
export type { DemoContext, DemoIngest, DemoOpen, DemoReady, DemoSource, DemoSpec, DemoTemplate, DemoUrl, } from "./spec.js";
|
|
6
6
|
export { DemoSpecSchema, defineDemo, formatSpecIssues } from "./spec.js";
|
package/demo/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { defaultDemoCliIo, demoMain, parseDemoArgs, resolvedSpecView } from "./cli.js";
|
|
2
|
-
export { defaultDemoDeps, demoDown, demoPorts, demoSlug, expandHome, fileStateStore, findBrokenSymlinks, resolveIngestPort, runDemo, runExportCheck, } from "./run.js";
|
|
2
|
+
export { defaultDemoDeps, demoDown, demoPorts, demoSlug, expandHome, fileStateStore, findBrokenSymlinks, killGroup, resolveIngestPort, runDemo, runExportCheck, } from "./run.js";
|
|
3
3
|
export { DemoSpecSchema, defineDemo, formatSpecIssues } from "./spec.js";
|
package/demo/run.d.ts
CHANGED
|
@@ -138,6 +138,9 @@ export declare function findBrokenSymlinks(dir: string): string[];
|
|
|
138
138
|
/** `<cwd>/test-temp/demo/<product>.json` — beside the store dirs, inside the
|
|
139
139
|
* consumer's repo, where `down` from another shell can find it. */
|
|
140
140
|
export declare function fileStateStore(cwd: string): DemoStateStore;
|
|
141
|
+
/** SIGTERM a process group (a pid startProcess spawned detached leads its
|
|
142
|
+
* own), falling back to the pid alone if it is not a group leader. */
|
|
143
|
+
export declare function killGroup(pid: number): void;
|
|
141
144
|
export declare function defaultDemoDeps(cwd: string): DemoDeps;
|
|
142
145
|
export declare function runDemo(spec: DemoSpec, opts: DemoRunOptions, deps?: DemoDeps): Promise<DemoRunResult>;
|
|
143
146
|
export interface ExportCheckOptions {
|
package/demo/run.js
CHANGED
|
@@ -170,6 +170,19 @@ export function fileStateStore(cwd) {
|
|
|
170
170
|
remove: (product) => rmSync(path(product), { force: true }),
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
/** SIGTERM a process group (a pid startProcess spawned detached leads its
|
|
174
|
+
* own), falling back to the pid alone if it is not a group leader. */
|
|
175
|
+
export function killGroup(pid) {
|
|
176
|
+
try {
|
|
177
|
+
process.kill(-pid, "SIGTERM");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
try {
|
|
182
|
+
process.kill(pid, "SIGTERM");
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
}
|
|
173
186
|
export function defaultDemoDeps(cwd) {
|
|
174
187
|
return {
|
|
175
188
|
licenseFile: () => requireLicenseFile({ missing: "throw" }),
|
|
@@ -187,17 +200,25 @@ export function defaultDemoDeps(cwd) {
|
|
|
187
200
|
}
|
|
188
201
|
},
|
|
189
202
|
cli: (storeDir, argv) => runCli(storeDir, ...argv),
|
|
203
|
+
// The dev command is a tree (`bun run dev` = concurrently -> bun run
|
|
204
|
+
// --watch -> ...), so it gets its own process group and the group is what
|
|
205
|
+
// dies: a SIGTERM to the parent alone left the watcher alive after `down`.
|
|
190
206
|
startProcess: (command, opts) => {
|
|
191
207
|
const [cmd, ...args] = command;
|
|
192
|
-
const proc = spawn(cmd, args, {
|
|
208
|
+
const proc = spawn(cmd, args, {
|
|
209
|
+
cwd: opts.cwd,
|
|
210
|
+
stdio: "inherit",
|
|
211
|
+
detached: true,
|
|
212
|
+
env: { ...process.env, ...opts.env },
|
|
213
|
+
});
|
|
193
214
|
const exited = new Promise((res) => proc.once("exit", (code) => res(code)));
|
|
194
215
|
return {
|
|
195
216
|
pid: proc.pid,
|
|
196
217
|
kill: () => {
|
|
197
|
-
|
|
218
|
+
if (proc.pid !== undefined)
|
|
219
|
+
killGroup(proc.pid);
|
|
220
|
+
else
|
|
198
221
|
proc.kill("SIGTERM");
|
|
199
|
-
}
|
|
200
|
-
catch { }
|
|
201
222
|
},
|
|
202
223
|
exited,
|
|
203
224
|
};
|
|
@@ -240,12 +261,7 @@ export function defaultDemoDeps(cwd) {
|
|
|
240
261
|
process.once("SIGTERM", release);
|
|
241
262
|
}
|
|
242
263
|
}),
|
|
243
|
-
killPid:
|
|
244
|
-
try {
|
|
245
|
-
process.kill(pid, "SIGTERM");
|
|
246
|
-
}
|
|
247
|
-
catch { }
|
|
248
|
-
},
|
|
264
|
+
killPid: killGroup,
|
|
249
265
|
log: (line) => console.log(`[demo] ${line}`),
|
|
250
266
|
};
|
|
251
267
|
}
|
|
@@ -416,8 +432,10 @@ function urlResolver(o) {
|
|
|
416
432
|
return ref.url;
|
|
417
433
|
if ("control" in ref)
|
|
418
434
|
return `http://localhost:${o.ports.backendPort}${ref.control}`;
|
|
435
|
+
// The private daemon is initialised without a cert source, so its proxy
|
|
436
|
+
// speaks plain http (reuse mode, 05-demo s7 step 2, will read the real one).
|
|
419
437
|
if ("proxy" in ref)
|
|
420
|
-
return `
|
|
438
|
+
return `http://${host}:${o.ports.proxyPort}${ref.proxy.replaceAll("{id}", o.instanceId)}`;
|
|
421
439
|
return `${studioBaseFrom({ instanceId: o.instanceId, studioHostPort: o.studioHostPort }, host, mode)}${ref.studio}`;
|
|
422
440
|
};
|
|
423
441
|
}
|