@norskvideo/ctl-test-harness 0.1.14 → 0.1.16

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/smoke.d.ts +8 -2
  3. package/smoke.js +28 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/smoke.d.ts CHANGED
@@ -87,7 +87,7 @@ export interface SmokeSpec {
87
87
  clearedMs?: number;
88
88
  };
89
89
  /** Override the banded ports (see smokePorts). */
90
- ports?: Partial<BaseHarnessPorts>;
90
+ ports?: Partial<SmokePorts>;
91
91
  }
92
92
  /** What the observation kinds read. Injected so the phase rule is tested
93
93
  * without Studio; defaults are studio-state's fetchers. */
@@ -125,6 +125,8 @@ export interface SmokeDeps {
125
125
  /** Docker leaves root-owned bind-mount targets under the store; a non-root
126
126
  * runner cannot rm them, so they go from a throwaway root container. */
127
127
  nukeStoreAsRoot(storeDir: string): void;
128
+ /** After the nuke: is the store still there? (Then the nuke did not work.) */
129
+ storeExists(storeDir: string): boolean;
128
130
  ensureNetwork(): ReturnType<typeof ensureRunnerOnNetwork>;
129
131
  supportsNoPublish(): Promise<boolean>;
130
132
  containerUser(): string | undefined;
@@ -133,7 +135,11 @@ export interface SmokeDeps {
133
135
  /** Per-slug port band, above the product integration harnesses' own bands:
134
136
  * daemon, dev backend, published Studio port and an instance base, twenty
135
137
  * apart per slug. */
136
- export declare function smokePorts(slug: string, overrides?: Partial<BaseHarnessPorts>): BaseHarnessPorts;
138
+ export interface SmokePorts extends BaseHarnessPorts {
139
+ /** The daemon's oauth2 proxy; banded so co-located runs never meet on :9443. */
140
+ proxyPort: number;
141
+ }
142
+ export declare function smokePorts(slug: string, overrides?: Partial<SmokePorts>): SmokePorts;
137
143
  export declare const defaultSmokeDeps: SmokeDeps;
138
144
  /** Whether an observation depends on a source being pumped. Structural ones
139
145
  * run the satisfied phase only. */
package/smoke.js CHANGED
@@ -18,7 +18,7 @@
18
18
  // not a dependency of this package (daemon.ts's cleanupDaemon has the same
19
19
  // rule); a product wanting typed commands bridges with toArgv in its own spec.
20
20
  import { spawnSync } from "node:child_process";
21
- import { writeFileSync } from "node:fs";
21
+ import { existsSync, writeFileSync } from "node:fs";
22
22
  import { basename, dirname, join } from "node:path";
23
23
  import { ensureRunnerOnNetwork, netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
24
24
  import { cleanupDaemon, requireLicenseFile, runCli, startDaemon, } from "./daemon.js";
@@ -33,15 +33,13 @@ const SMOKE_BAND_WIDTH = 20;
33
33
  const SMOKE_BANDS = 50;
34
34
  const STUDIO_HOST_PORT_PARAM = "STUDIO_HOST_PORT";
35
35
  const NUKE_IMAGE = "alpine:3";
36
- /** Per-slug port band, above the product integration harnesses' own bands:
37
- * daemon, dev backend, published Studio port and an instance base, twenty
38
- * apart per slug. */
39
36
  export function smokePorts(slug, overrides = {}) {
40
37
  const daemonPort = SMOKE_PORT_BASE + hashSlot(`smoke:${slug}`, SMOKE_BANDS) * SMOKE_BAND_WIDTH;
41
38
  return {
42
39
  daemonPort,
43
40
  backendPort: daemonPort + 1,
44
41
  studioHostPort: daemonPort + 2,
42
+ proxyPort: daemonPort + 3,
45
43
  instancePortBase: daemonPort + 10,
46
44
  ...overrides,
47
45
  };
@@ -79,6 +77,7 @@ export const defaultSmokeDeps = {
79
77
  `rm -rf /base/${basename(storeDir)}`,
80
78
  ]);
81
79
  },
80
+ storeExists: (storeDir) => existsSync(storeDir),
82
81
  ensureNetwork: () => ensureRunnerOnNetwork(),
83
82
  supportsNoPublish: ctlSupportsNoPublish,
84
83
  containerUser: runnerContainerUser,
@@ -188,14 +187,20 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
188
187
  let daemon = null;
189
188
  let launched = false;
190
189
  let handles = [];
190
+ let journeyError;
191
191
  try {
192
- // The same zero point as the guides: `init` on a virgin store, not a seeded config.yaml.
192
+ // The same zero point as the guides: `init` on a virgin store, not a seeded
193
+ // config.yaml. No http redirect (port 80 needs root) and a banded proxy port
194
+ // (the default :9443 would meet a co-located integration run).
193
195
  await cliOk([
194
196
  "init",
195
197
  "--network-mode",
196
198
  spec.networkMode ?? "docker",
197
199
  "--working-directory",
198
200
  join(storeDir, "norsk-runtime"),
201
+ "--proxy-port",
202
+ String(ports.proxyPort),
203
+ "--no-http-redirect",
199
204
  "--no-start-server",
200
205
  ]);
201
206
  const started = deps.startDaemon(storeDir, { port: ports.daemonPort, seedConfig: false });
@@ -316,9 +321,15 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
316
321
  deps.log(`${slug}: cleared phase passed for ${phased.length} observation(s)`);
317
322
  }
318
323
  }
319
- finally {
320
- if (handles.length)
321
- await deps.stopSources(handles).catch(() => { });
324
+ catch (e) {
325
+ journeyError = e;
326
+ }
327
+ if (handles.length)
328
+ await deps.stopSources(handles).catch(() => { });
329
+ // cleanupDaemon ends with rmSync(storeDir); Docker's root-owned bind-mount
330
+ // targets make that EACCES on a non-root runner. Expected: the root nuke
331
+ // below is what removes them. Only a store that survives the nuke fails.
332
+ try {
322
333
  await deps.cleanup({
323
334
  deleteInstance: (id) => cli(["instance", "delete", id, "--purge"]),
324
335
  stopDaemon: () => cli(["shutdown", "--daemon-only"]),
@@ -327,6 +338,14 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
327
338
  storeDir,
328
339
  containers: [`norsk-inst-${instanceId}-studio`, `norsk-inst-${instanceId}-media`],
329
340
  });
330
- deps.nukeStoreAsRoot(storeDir);
341
+ }
342
+ catch (e) {
343
+ deps.log(`${slug}: cleanup could not remove the store itself (${e instanceof Error ? e.message : String(e)}); nuking as root`);
344
+ }
345
+ deps.nukeStoreAsRoot(storeDir);
346
+ if (journeyError !== undefined)
347
+ throw journeyError;
348
+ if (deps.storeExists(storeDir)) {
349
+ throw new Error(`${slug}: store ${storeDir} still present after the root-container nuke`);
331
350
  }
332
351
  }