@norskvideo/ctl-test-harness 0.1.15 → 0.1.17

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 +14 -3
  3. package/smoke.js +24 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/smoke.d.ts CHANGED
@@ -34,7 +34,14 @@ export interface SmokeContext {
34
34
  }): Promise<CliResult>;
35
35
  fetchStudio(path: string): Promise<Response>;
36
36
  }
37
- export type SmokeObservation = {
37
+ /** Set on any observation that holds with or without a source — a resilient
38
+ * switch that keeps a ladder fed from a slate, a control surface that is up
39
+ * regardless — so it runs the satisfied phase only. `components` is
40
+ * structural by nature and needs no flag. */
41
+ interface Structural {
42
+ structural?: boolean;
43
+ }
44
+ export type SmokeObservation = Structural & ({
38
45
  kind: "components";
39
46
  ids: string[];
40
47
  } | {
@@ -60,7 +67,7 @@ export type SmokeObservation = {
60
67
  kind: "custom";
61
68
  label: string;
62
69
  satisfied: (ctx: SmokeContext) => Promise<boolean>;
63
- };
70
+ });
64
71
  export interface SmokeSpec {
65
72
  product: {
66
73
  /** Manifest name, e.g. "norsk-probe". The registration must report it. */
@@ -125,6 +132,8 @@ export interface SmokeDeps {
125
132
  /** Docker leaves root-owned bind-mount targets under the store; a non-root
126
133
  * runner cannot rm them, so they go from a throwaway root container. */
127
134
  nukeStoreAsRoot(storeDir: string): void;
135
+ /** After the nuke: is the store still there? (Then the nuke did not work.) */
136
+ storeExists(storeDir: string): boolean;
128
137
  ensureNetwork(): ReturnType<typeof ensureRunnerOnNetwork>;
129
138
  supportsNoPublish(): Promise<boolean>;
130
139
  containerUser(): string | undefined;
@@ -140,8 +149,10 @@ export interface SmokePorts extends BaseHarnessPorts {
140
149
  export declare function smokePorts(slug: string, overrides?: Partial<SmokePorts>): SmokePorts;
141
150
  export declare const defaultSmokeDeps: SmokeDeps;
142
151
  /** Whether an observation depends on a source being pumped. Structural ones
143
- * run the satisfied phase only. */
152
+ * (`components`, or anything the spec marks `structural`) run the satisfied
153
+ * phase only. */
144
154
  export declare function sourceDependent(o: SmokeObservation): boolean;
145
155
  /** One sample of an observation — the predicate the three phases poll. */
146
156
  export declare function observationSatisfied(o: SmokeObservation, ctx: SmokeContext, readers: SmokeReaders): Promise<boolean>;
147
157
  export declare function runProductSmoke(slug: string, spec: SmokeSpec, deps?: SmokeDeps): Promise<void>;
158
+ export {};
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";
@@ -77,6 +77,7 @@ export const defaultSmokeDeps = {
77
77
  `rm -rf /base/${basename(storeDir)}`,
78
78
  ]);
79
79
  },
80
+ storeExists: (storeDir) => existsSync(storeDir),
80
81
  ensureNetwork: () => ensureRunnerOnNetwork(),
81
82
  supportsNoPublish: ctlSupportsNoPublish,
82
83
  containerUser: runnerContainerUser,
@@ -99,9 +100,10 @@ function describeObservation(o) {
99
100
  }
100
101
  }
101
102
  /** Whether an observation depends on a source being pumped. Structural ones
102
- * run the satisfied phase only. */
103
+ * (`components`, or anything the spec marks `structural`) run the satisfied
104
+ * phase only. */
103
105
  export function sourceDependent(o) {
104
- return o.kind !== "components";
106
+ return o.kind !== "components" && o.structural !== true;
105
107
  }
106
108
  /** One sample of an observation — the predicate the three phases poll. */
107
109
  export async function observationSatisfied(o, ctx, readers) {
@@ -186,6 +188,7 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
186
188
  let daemon = null;
187
189
  let launched = false;
188
190
  let handles = [];
191
+ let journeyError;
189
192
  try {
190
193
  // The same zero point as the guides: `init` on a virgin store, not a seeded
191
194
  // config.yaml. No http redirect (port 80 needs root) and a banded proxy port
@@ -319,9 +322,15 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
319
322
  deps.log(`${slug}: cleared phase passed for ${phased.length} observation(s)`);
320
323
  }
321
324
  }
322
- finally {
323
- if (handles.length)
324
- await deps.stopSources(handles).catch(() => { });
325
+ catch (e) {
326
+ journeyError = e;
327
+ }
328
+ if (handles.length)
329
+ await deps.stopSources(handles).catch(() => { });
330
+ // cleanupDaemon ends with rmSync(storeDir); Docker's root-owned bind-mount
331
+ // targets make that EACCES on a non-root runner. Expected: the root nuke
332
+ // below is what removes them. Only a store that survives the nuke fails.
333
+ try {
325
334
  await deps.cleanup({
326
335
  deleteInstance: (id) => cli(["instance", "delete", id, "--purge"]),
327
336
  stopDaemon: () => cli(["shutdown", "--daemon-only"]),
@@ -330,6 +339,14 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
330
339
  storeDir,
331
340
  containers: [`norsk-inst-${instanceId}-studio`, `norsk-inst-${instanceId}-media`],
332
341
  });
333
- deps.nukeStoreAsRoot(storeDir);
342
+ }
343
+ catch (e) {
344
+ deps.log(`${slug}: cleanup could not remove the store itself (${e instanceof Error ? e.message : String(e)}); nuking as root`);
345
+ }
346
+ deps.nukeStoreAsRoot(storeDir);
347
+ if (journeyError !== undefined)
348
+ throw journeyError;
349
+ if (deps.storeExists(storeDir)) {
350
+ throw new Error(`${slug}: store ${storeDir} still present after the root-container nuke`);
334
351
  }
335
352
  }