@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 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
- export async function runCli(storeDir, ...args) {
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] = cliCommand;
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 ready = pollUntil(async () => {
131
- const res = await fetch(`http://localhost:${port}/api/ready`, { signal: AbortSignal.timeout(1000) });
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 `https://localhost:${ports.proxyPort}${ref.proxy.replaceAll("{id}", instanceId)}`;
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, { cwd: opts.cwd, stdio: "inherit", env: { ...process.env, ...opts.env } });
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
- try {
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: (pid) => {
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 `https://${host}:${o.ports.proxyPort}${ref.proxy.replaceAll("{id}", o.instanceId)}`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {