@norskvideo/ctl-test-harness 0.1.74 → 0.1.75

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.
@@ -18,7 +18,9 @@ export declare const authority: (e: Endpoint) => string;
18
18
  export declare function productServiceHost(instanceId: string, service: string): string;
19
19
  export interface ResolveOpts {
20
20
  instanceId: string;
21
- service: "studio" | "media";
21
+ /** A compose service name: "studio" and "media" for a Studio product, or a
22
+ * direct-SDK product's own control service (the smoke `surface`). */
23
+ service: string;
22
24
  /** Port the listener binds INSIDE the container (studio 8000, media SRT its
23
25
  * own configured port). Used in `direct` mode. */
24
26
  internalPort: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/smoke.d.ts CHANGED
@@ -33,6 +33,25 @@ export interface SmokeContext {
33
33
  output?: "json" | "yaml";
34
34
  }): Promise<CliResult>;
35
35
  fetchStudio(path: string): Promise<Response>;
36
+ /** The spec's `surface` (Studio's by default), reached the same two ways:
37
+ * the banded published port, or `<id>-<service>-1:<internalPort>` direct. */
38
+ surfaceUrl(path: string): string;
39
+ /** GET surfaceUrl(path) through the injected reader -- for observations. A
40
+ * prepare step that acts (POST, DELETE) uses fetch(ctx.surfaceUrl(...)). */
41
+ fetchSurface(path: string): Promise<Response>;
42
+ }
43
+ /** The instance service whose HTTP surface the journey publishes and reaches.
44
+ * Studio's by default; a direct-SDK product (no Studio, e.g. norsk-relay)
45
+ * names its own control API. */
46
+ export interface SmokeSurface {
47
+ service: string;
48
+ internalPort: number;
49
+ /** The host port the TEMPLATE publishes the surface on (a `--param` the spec
50
+ * sets). ctl emits `--host-ports` only for Studio and media -- one naming any
51
+ * other service is persisted but never wired -- so a direct-SDK product's
52
+ * surface is published by its own compose. Given, no `--host-ports` is
53
+ * passed and publish mode dials this port. */
54
+ hostPort?: number;
36
55
  }
37
56
  /** Set on any observation that holds with or without a source — a resilient
38
57
  * switch that keeps a ladder fed from a slate, a control surface that is up
@@ -85,6 +104,14 @@ export interface SmokeSpec {
85
104
  licenseFile?: string;
86
105
  };
87
106
  template: SmokeTemplate;
107
+ /** Default { service: "studio", internalPort: 8000 }. Published on the banded
108
+ * studioHostPort; STUDIO_HOST_PORT is passed instead only for Studio's. */
109
+ surface?: SmokeSurface;
110
+ /** Runs once the instance reports running, before the not-yet phase: what a
111
+ * product needs in place before its sources can be accepted (the relay
112
+ * refuses SRT for a stream nobody declared). Not an observation -- it may
113
+ * act, and a throw fails the journey. */
114
+ prepare?(ctx: SmokeContext): Promise<void>;
88
115
  /** Extra `--param NAME=value`. INSTANCE_NAME is always passed; STUDIO_HOST_PORT
89
116
  * when the template declares it (else the port is pinned with --host-ports). */
90
117
  params?: string[];
@@ -206,5 +233,15 @@ export declare function assertManifestRoutesServed(opts: {
206
233
  export declare function sourceDependent(o: SmokeObservation): boolean;
207
234
  /** One sample of an observation — the predicate the three phases poll. */
208
235
  export declare function observationSatisfied(o: SmokeObservation, ctx: SmokeContext, readers: SmokeReaders): Promise<boolean>;
236
+ /** A direct-SDK product's licence identity -- the sdk-client cert and key it
237
+ * answers the engine's V2 handshake with, base64 -- from what smoke.yml
238
+ * exports (repo secrets NORSK_SDK_CLIENT_CERT_B64 / NORSK_SDK_CLIENT_KEY_B64).
239
+ * Undefined when neither is set (a Studio product, or a local run without
240
+ * one); half a pair is a misconfigured repo, so it throws rather than
241
+ * launching an instance that restarts on "License handshake required". */
242
+ export declare function sdkClientIdentityFromEnv(env?: Record<string, string | undefined>): {
243
+ certB64: string;
244
+ keyB64: string;
245
+ } | undefined;
209
246
  export declare function runProductSmoke(slug: string, spec: SmokeSpec, deps?: SmokeDeps): Promise<void>;
210
247
  export {};
package/smoke.js CHANGED
@@ -20,7 +20,7 @@
20
20
  import { existsSync, writeFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
22
  import { dumpInstanceContainerLogs, preserveEngineLogs, preserveInstanceContainerLogs } from "./container-logs.js";
23
- import { ensureRunnerOnNetwork, netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
23
+ import { authority, ensureRunnerOnNetwork, netReachMode, resolveEndpoint, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
24
24
  import { cleanupDaemon, ctlSupportsEphemeralPorts, PROXY_PORT_FILE, requireLicenseFile, runCli, startDaemon, } from "./daemon.js";
25
25
  import { hashSlot } from "./harness-config.js";
26
26
  import { ctlSupportsNoPublish, ctlSupportsPublishDebugPorts, runnerContainerUser } from "./launch.js";
@@ -191,6 +191,22 @@ function parseJson(r, what) {
191
191
  throw new Error(`${what}: not JSON: ${r.stdout.slice(0, 200)}`);
192
192
  }
193
193
  }
194
+ /** A direct-SDK product's licence identity -- the sdk-client cert and key it
195
+ * answers the engine's V2 handshake with, base64 -- from what smoke.yml
196
+ * exports (repo secrets NORSK_SDK_CLIENT_CERT_B64 / NORSK_SDK_CLIENT_KEY_B64).
197
+ * Undefined when neither is set (a Studio product, or a local run without
198
+ * one); half a pair is a misconfigured repo, so it throws rather than
199
+ * launching an instance that restarts on "License handshake required". */
200
+ export function sdkClientIdentityFromEnv(env = process.env) {
201
+ const certB64 = env.NORSK_SDK_CLIENT_CERT_B64 ?? "";
202
+ const keyB64 = env.NORSK_SDK_CLIENT_KEY_B64 ?? "";
203
+ if (!certB64 && !keyB64)
204
+ return undefined;
205
+ if (!certB64 || !keyB64) {
206
+ throw new Error(`NORSK_SDK_CLIENT_${certB64 ? "KEY" : "CERT"}_B64 is empty but its pair is set -- set both repo secrets or neither`);
207
+ }
208
+ return { certB64, keyB64 };
209
+ }
194
210
  export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
195
211
  const ports = smokePorts(slug, spec.ports);
196
212
  const storeDir = deps.storeDir(slug);
@@ -202,6 +218,8 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
202
218
  };
203
219
  const mode = netReachMode();
204
220
  const studio = { instanceId, studioHostPort: ports.studioHostPort };
221
+ const surface = spec.surface ?? { service: "studio", internalPort: STUDIO_INTERNAL_PORT };
222
+ const studioSurface = surface.service === "studio" && surface.internalPort === STUDIO_INTERNAL_PORT;
205
223
  const cli = async (argv, opts = {}) => {
206
224
  const full = ["--port", String(ports.daemonPort), ...argv, ...(opts.output ? ["-o", opts.output] : [])];
207
225
  return deps.cli(storeDir, full);
@@ -223,6 +241,18 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
223
241
  studio,
224
242
  cli,
225
243
  fetchStudio: (path) => deps.readers.fetch(`${studioBaseFrom(studio, process.env.NORSK_TEST_HOST ?? "localhost", mode)}${path}`),
244
+ surfaceUrl: (path) => {
245
+ const e = resolveEndpoint({
246
+ instanceId,
247
+ service: surface.service,
248
+ internalPort: surface.internalPort,
249
+ publishHost: process.env.NORSK_TEST_HOST ?? "localhost",
250
+ publishPort: surface.hostPort ?? ports.studioHostPort,
251
+ mode,
252
+ });
253
+ return `http://${authority(e)}${path}`;
254
+ },
255
+ fetchSurface: (path) => deps.readers.fetch(ctx.surfaceUrl(path)),
226
256
  };
227
257
  let daemon = null;
228
258
  let launched = false;
@@ -335,10 +365,12 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
335
365
  const params = [`INSTANCE_NAME=${instanceId}`, ...(spec.params ?? [])];
336
366
  const launchArgv = ["instance", "launch-template", instanceId, "--template", templateName];
337
367
  const internalOnly = mode === "direct" && (await deps.supportsNoPublish());
338
- if (declared.has(STUDIO_HOST_PORT_PARAM))
368
+ if (studioSurface && declared.has(STUDIO_HOST_PORT_PARAM)) {
339
369
  params.push(`${STUDIO_HOST_PORT_PARAM}=${ports.studioHostPort}`);
340
- else if (!internalOnly)
341
- launchArgv.push("--host-ports", `${ports.studioHostPort}:${STUDIO_INTERNAL_PORT}@studio`);
370
+ }
371
+ else if (!internalOnly && surface.hostPort === undefined) {
372
+ launchArgv.push("--host-ports", `${ports.studioHostPort}:${surface.internalPort}@${surface.service}`);
373
+ }
342
374
  for (const p of params)
343
375
  launchArgv.push("--param", p);
344
376
  if (spec.hardware)
@@ -376,6 +408,10 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
376
408
  const inst = parseJson(r, "instance list").instances?.find((i) => i.id === instanceId);
377
409
  return inst?.status === "running" || inst?.status === "healthy";
378
410
  }, { timeoutMs: timeouts.healthyMs, intervalMs: 1000, label: `instance ${instanceId} never reported running` });
411
+ if (spec.prepare) {
412
+ await spec.prepare(ctx);
413
+ deps.log(`${slug}: prepare done`);
414
+ }
379
415
  // Phases.
380
416
  const vacuous = spec.sources.length === 0;
381
417
  if (vacuous) {