@norskvideo/ctl-test-harness 0.1.79 → 0.1.81

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/demo/cli.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { type DevLoopOptions, type DevLoopResult } from "./dev-loop.js";
3
3
  import { type DemoUi, type PaneLayout } from "./panes.js";
4
4
  import { type DemoRunOptions, type DemoRunResult, demoPorts } from "./run.js";
5
- import type { DemoDaemonPolicy, DemoMode, DemoSpec } from "./spec.js";
5
+ import type { DemoDaemonPolicy, DemoMode, DemoPortPolicy, DemoSpec } from "./spec.js";
6
6
  export interface DemoArgs {
7
7
  /** `dev-loop <action>`: the standalone tier's verbs. */
8
8
  devLoop: boolean;
@@ -17,6 +17,8 @@ export interface DemoArgs {
17
17
  ui?: DemoUi;
18
18
  /** Default: PUBLIC_HOST in the environment. */
19
19
  publicHost?: string;
20
+ /** `up`/`check`: absent is `assigned`. */
21
+ ports?: DemoPortPolicy;
20
22
  }
21
23
  export declare function parseDemoArgs(argv: string[]): DemoArgs;
22
24
  export interface ResolvedSpecView {
package/demo/cli.js CHANGED
@@ -30,7 +30,7 @@ import { runDemo } from "./harness.js";
30
30
  import { demoPaneList, devLoopPaneList, launchLayout, renderLayout, standaloneDirs, } from "./panes.js";
31
31
  import { demoDown, demoPorts, demoSlug, runExportCheck } from "./run.js";
32
32
  const USAGE = `usage: demo <up|check|down|spec|ui> [--mode dev|image|standalone] [--daemon private|reuse] [--export-only] [--json]
33
- [--ui zellij|tmux|none] [--public-host <host>] [--spec <path>]
33
+ [--ports assigned|band] [--ui zellij|tmux|none] [--public-host <host>] [--spec <path>]
34
34
  demo dev-loop <up|refresh|down|ui> [--ui zellij|tmux|none] [--public-host <host>] [--spec <path>]
35
35
 
36
36
  up launch, print the URLs, hold (Ctrl-C tears down)
@@ -54,6 +54,10 @@ const USAGE = `usage: demo <up|check|down|spec|ui> [--mode dev|image|standalone]
54
54
  --daemon reuse your listening daemon and real store; the driver deletes the instance,
55
55
  removes the template and product, then adds again (stored templates
56
56
  are immutable by name)
57
+ --ports assigned up/check: the daemon and proxy take ports the system assigns, so two runs
58
+ never collide (default)
59
+ --ports band up/check: the product's fixed band instead, so the proxy URL (and a
60
+ browser's secure-origin exception for it) survives a relaunch
57
61
  --ui zellij|tmux|none the multiplexer (\`none\` prints the pane commands); zellij unless said
58
62
  --public-host <host> the name other machines reach this box by: every URL, the daemon's
59
63
  publicHost, Studio's MoQ preview (default: PUBLIC_HOST)
@@ -127,12 +131,22 @@ export function parseDemoArgs(argv) {
127
131
  case "--public-host":
128
132
  args.publicHost = value();
129
133
  break;
134
+ case "--ports": {
135
+ const p = value();
136
+ if (p !== "assigned" && p !== "band")
137
+ throw new Error(`unknown ports '${p}'\n${USAGE}`);
138
+ args.ports = p;
139
+ break;
140
+ }
130
141
  default:
131
142
  throw new Error(`unknown flag '${flag}'\n${USAGE}`);
132
143
  }
133
144
  }
134
145
  if (args.ui !== undefined && args.action !== "ui")
135
146
  throw new Error("--ui belongs to ui (or dev-loop ui)");
147
+ if (args.ports !== undefined && (devLoop || (args.action !== "up" && args.action !== "check"))) {
148
+ throw new Error("--ports belongs to up and check");
149
+ }
136
150
  if (devLoop && (args.mode !== undefined || args.daemon !== "private" || args.exportOnly || args.json)) {
137
151
  throw new Error("dev-loop takes only --ui, --public-host and --spec (no --mode, --daemon, --export-only or --json): it is always the source tier on a private daemon");
138
152
  }
@@ -290,6 +304,7 @@ export async function demoMain(argv, io) {
290
304
  daemon: args.daemon,
291
305
  cwd: io.cwd,
292
306
  ...publicHost,
307
+ ...(args.ports !== undefined ? { ports: args.ports } : {}),
293
308
  ...(io.abort ? { abort: io.abort } : {}),
294
309
  });
295
310
  return 0;
package/demo/harness.d.ts CHANGED
@@ -115,6 +115,8 @@ export declare class ProductHarness {
115
115
  * daemon, the dev backend and the store. Throws if the store outlives the
116
116
  * root-container nuke: a leak a runner would otherwise accumulate. */
117
117
  teardown(): Promise<void>;
118
+ private instancePortsIssued;
119
+ private nextInstancePort;
118
120
  private nextStudioHostPort;
119
121
  }
120
122
  /** The demo: a ProductHarness set up, run, held (`up`) or not (`check`), torn down. */
package/demo/harness.js CHANGED
@@ -23,6 +23,12 @@ const STUDIO_HOST_PORT_PARAM = "STUDIO_HOST_PORT";
23
23
  /** Launches after the first take studio host ports above the proxy's, inside
24
24
  * the band and below `instancePortBase`: six of them. */
25
25
  const EXTRA_STUDIO_PORTS = 6;
26
+ /** Allocated-port params come from `instancePortBase` to the band's end: ten. */
27
+ const INSTANCE_PORTS = 10;
28
+ /** A template's allocated host port: a `*_PORT` param defaulting to `auto`,
29
+ * which ctl resolves to the first free port from the template's PREFERRED
30
+ * one (probe's MOQ_PORT: 4443). */
31
+ const isAllocatedPortParam = (name, dflt) => dflt === "auto" && /_PORT$/.test(name);
26
32
  export class ProductHarness {
27
33
  spec;
28
34
  slug;
@@ -135,6 +141,24 @@ export class ProductHarness {
135
141
  else if (!internalOnly) {
136
142
  launchArgv.push("--host-ports", `${studioHostPort}:${STUDIO_INTERNAL_PORT}@studio`);
137
143
  }
144
+ // Allocated ports get a port from this run's band, not ctl's `auto`. Auto
145
+ // is "the first free port from the template's preferred one", and on a
146
+ // shared docker host every daemon prefers the same one: on 2026-10-08
147
+ // probe's guide gate lost MOQ_PORT's 4443 between ctl's free-port check
148
+ // and container start ("host port 4443 was taken between validation and
149
+ // container start") -- to another instance falling back to its own MoQ
150
+ // listener, or to the node MoQ relay, which publishes 4443 itself. The
151
+ // band is per product and below the ephemeral range (port-bands.test.ts),
152
+ // so nothing else on the host reaches for these. An internal-only launch
153
+ // publishes no host port at all, so `auto` is harmless there and stays.
154
+ if (!internalOnly) {
155
+ for (const [name, dflt] of declared) {
156
+ if (name === STUDIO_HOST_PORT_PARAM || specParams[name] !== undefined)
157
+ continue;
158
+ if (isAllocatedPortParam(name, dflt))
159
+ params.push(`${name}=${this.nextInstancePort(name)}`);
160
+ }
161
+ }
138
162
  for (const p of params)
139
163
  launchArgv.push("--param", p);
140
164
  const hardware = overrides.hardware ?? spec.launch?.hardware;
@@ -260,6 +284,14 @@ export class ProductHarness {
260
284
  throw new Error(`store ${s.storeDir} still present after the root-container nuke`);
261
285
  }
262
286
  }
287
+ instancePortsIssued = 0;
288
+ nextInstancePort(param) {
289
+ const n = this.instancePortsIssued++;
290
+ if (n >= INSTANCE_PORTS) {
291
+ throw new Error(`${param}: the band holds ${INSTANCE_PORTS} allocated instance ports and this run has used them all -- pass params.${param}`);
292
+ }
293
+ return this.ports.instancePortBase + n;
294
+ }
263
295
  nextStudioHostPort() {
264
296
  const n = this.launchedIds.length;
265
297
  if (n === 0)
@@ -368,8 +400,9 @@ export async function runDemo(spec, opts, deps = defaultDemoDeps(opts.cwd)) {
368
400
  ...(opts.daemonPort !== undefined ? { daemonPort: opts.daemonPort } : {}),
369
401
  ...(opts.timeouts !== undefined ? { timeouts: opts.timeouts } : {}),
370
402
  ...(opts.publicHost !== undefined ? { publicHost: opts.publicHost } : {}),
371
- // The driver owns every port of this run, so none can be left behind.
372
- ephemeralPorts: true,
403
+ // The driver owns every port of this run, so none can be left behind;
404
+ // `band` trades that for URLs that survive a relaunch.
405
+ ephemeralPorts: opts.ports !== "band",
373
406
  });
374
407
  if (opts.action === "up") {
375
408
  await refuseIfUp(spec, h.session, deps);
package/demo/index.d.ts CHANGED
@@ -9,5 +9,5 @@ export type { DemoPane, DemoPaneOptions, DemoUi, DevLoopPaneOptions, PaneLayout,
9
9
  export { DEV_LOOP_SOURCES, DEV_LOOP_STAMP, demoPaneList, devLoopPaneList, launchLayout, paneArgv, paneCommandLine, paneShellLine, renderLayout, shellQuote, standaloneDirs, } from "./panes.js";
10
10
  export type { CliResult, CliStep, DemoDeps, DemoPorts, DemoRunOptions, DemoRunResult, DemoState, DemoStateKind, DemoStateStore, DemoTimeouts, ExportCheckOptions, IngestPortRow, PinMismatch, ProcessHandle, TemplateParams, } from "./run.js";
11
11
  export { checkTemplatePins, defaultDemoDeps, demoDown, demoPorts, demoSlug, expandHome, exportStandaloneWorkdir, fileStateStore, findBrokenSymlinks, killGroup, resolveIngestPort, runExportCheck, } from "./run.js";
12
- export type { DemoContext, DemoDaemonPolicy, DemoExtra, DemoIngest, DemoLaunchContext, DemoMode, DemoOpen, DemoReady, DemoSource, DemoSpec, DemoTemplate, DemoUrl, } from "./spec.js";
12
+ export type { DemoContext, DemoDaemonPolicy, DemoExtra, DemoIngest, DemoLaunchContext, DemoMode, DemoOpen, DemoPortPolicy, DemoReady, DemoSource, DemoSpec, DemoTemplate, DemoUrl, } from "./spec.js";
13
13
  export { DemoSpecSchema, defineDemo, formatSpecIssues } from "./spec.js";
package/demo/run.d.ts CHANGED
@@ -3,7 +3,7 @@ import { ensureRunnerOnNetwork } from "../container-net.js";
3
3
  import { type DaemonProcess, type RunningDaemon, type StartDaemonOptions } from "../daemon.js";
4
4
  import { type BaseHarnessPorts } from "../harness-config.js";
5
5
  import { type SourceHandle, type SrtPumpTarget } from "../source-pump.js";
6
- import type { DemoDaemonPolicy, DemoIngest, DemoMode, DemoSpec, DemoTemplate } from "./spec.js";
6
+ import type { DemoDaemonPolicy, DemoIngest, DemoMode, DemoPortPolicy, DemoSpec, DemoTemplate } from "./spec.js";
7
7
  export interface CliResult {
8
8
  stdout: string;
9
9
  stderr: string;
@@ -127,6 +127,8 @@ export interface DemoRunOptions {
127
127
  /** The hostname other machines reach this box by: every printed URL uses
128
128
  * it, and a private daemon is initialised with it as its publicHost. */
129
129
  publicHost?: string;
130
+ /** Default assigned. */
131
+ ports?: DemoPortPolicy;
130
132
  }
131
133
  export interface DemoRunResult {
132
134
  instanceId: string;
package/demo/spec.d.ts CHANGED
@@ -37,6 +37,9 @@ export type DemoMode = "dev" | "image";
37
37
  * `check` always uses), or `reuse` the developer's listening daemon and its
38
38
  * real store — where `product add` refuses an existing name and stored
39
39
  * templates are immutable, so the driver deletes/removes/re-adds first. */
40
+ /** Whether a run's daemon and proxy take system-assigned ports, or the
41
+ * product's fixed band (stable URLs across relaunches, for development). */
42
+ export type DemoPortPolicy = "assigned" | "band";
40
43
  export type DemoDaemonPolicy = "private" | "reuse";
41
44
  /** What `beforeLaunch` sees: the template is stored, nothing is launched. */
42
45
  export interface DemoLaunchContext {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {