@norskvideo/ctl-test-harness 0.1.22 → 0.1.23

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.
@@ -2,9 +2,19 @@ export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
2
2
  export declare const DEFAULT_COMPOSE_NODE = "video_compose";
3
3
  /** The highest mediaIn across every node of that name; 0 when none. */
4
4
  export declare function maxNodeMediaIn(summary: unknown, node: string): number;
5
+ export interface ComposeSample {
6
+ frames: number;
7
+ /** Why the sample is 0 when it is: the visualiser was unreachable, and how. */
8
+ error?: string;
9
+ }
5
10
  /** One sample: the highest mediaIn of `node` across every root workflow under
6
- * `base` (`.../visualiser`). An unreachable visualiser reads as 0 the
7
- * caller's second sample, not a throw, decides. */
11
+ * `base` (`.../visualiser`). An unreachable visualiser reads as 0 with the
12
+ * reason beside it — the caller's second sample, not a throw, decides. */
13
+ export declare function composeSample(opts: {
14
+ fetch: FetchLike;
15
+ base: string;
16
+ node?: string;
17
+ }): Promise<ComposeSample>;
8
18
  export declare function composeFrames(opts: {
9
19
  fetch: FetchLike;
10
20
  base: string;
@@ -15,6 +25,8 @@ export interface ComposeAdvancingResult {
15
25
  before: number;
16
26
  after: number;
17
27
  node: string;
28
+ /** Set when a sample could not read the visualiser at all. */
29
+ error?: string;
18
30
  }
19
31
  /** Two samples `settleMs` apart: advancing when the second is above the first
20
32
  * and above zero — the fallback card and a stalled feed both read flat. */
@@ -28,12 +28,18 @@ async function getJson(fetch, url) {
28
28
  const r = await fetch(url, { signal: AbortSignal.timeout(5000) });
29
29
  if (!r.ok)
30
30
  throw new Error(`GET ${url} -> ${r.status}`);
31
- return r.json();
31
+ const text = await r.text();
32
+ try {
33
+ return JSON.parse(text);
34
+ }
35
+ catch {
36
+ throw new Error(`GET ${url} -> ${r.status}, not JSON (a sign-in page? the route needs no auth): ${text.slice(0, 80)}`);
37
+ }
32
38
  }
33
39
  /** One sample: the highest mediaIn of `node` across every root workflow under
34
- * `base` (`.../visualiser`). An unreachable visualiser reads as 0 the
35
- * caller's second sample, not a throw, decides. */
36
- export async function composeFrames(opts) {
40
+ * `base` (`.../visualiser`). An unreachable visualiser reads as 0 with the
41
+ * reason beside it — the caller's second sample, not a throw, decides. */
42
+ export async function composeSample(opts) {
37
43
  const node = opts.node ?? DEFAULT_COMPOSE_NODE;
38
44
  try {
39
45
  const wf = (await getJson(opts.fetch, `${opts.base}/workflow`));
@@ -44,18 +50,28 @@ export async function composeFrames(opts) {
44
50
  const summary = await getJson(opts.fetch, `${opts.base}/workflow/${encodeURIComponent(root.wfid)}/summary`);
45
51
  max = Math.max(max, maxNodeMediaIn(summary, node));
46
52
  }
47
- return max;
53
+ return { frames: max };
48
54
  }
49
- catch {
50
- return 0;
55
+ catch (e) {
56
+ return { frames: 0, error: e instanceof Error ? e.message : String(e) };
51
57
  }
52
58
  }
59
+ export async function composeFrames(opts) {
60
+ return (await composeSample(opts)).frames;
61
+ }
53
62
  /** Two samples `settleMs` apart: advancing when the second is above the first
54
63
  * and above zero — the fallback card and a stalled feed both read flat. */
55
64
  export async function composeAdvancing(opts) {
56
65
  const node = opts.node ?? DEFAULT_COMPOSE_NODE;
57
- const before = await composeFrames({ fetch: opts.fetch, base: opts.base, node });
66
+ const before = await composeSample({ fetch: opts.fetch, base: opts.base, node });
58
67
  await new Promise((r) => setTimeout(r, opts.settleMs ?? DEFAULT_SETTLE_MS));
59
- const after = await composeFrames({ fetch: opts.fetch, base: opts.base, node });
60
- return { advancing: after > before && after > 0, before, after, node };
68
+ const after = await composeSample({ fetch: opts.fetch, base: opts.base, node });
69
+ const error = after.error ?? before.error;
70
+ return {
71
+ advancing: after.frames > before.frames && after.frames > 0,
72
+ before: before.frames,
73
+ after: after.frames,
74
+ node,
75
+ ...(error !== undefined ? { error } : {}),
76
+ };
61
77
  }
package/demo/gates.js CHANGED
@@ -48,7 +48,9 @@ export function composeAdvancingGate(opts = {}) {
48
48
  node,
49
49
  ...(opts.settleMs !== undefined ? { settleMs: opts.settleMs } : {}),
50
50
  });
51
- if (!r.advancing)
51
+ if (r.error)
52
+ ctx.log(`visualiser unreachable at ${base}: ${r.error}`);
53
+ else if (!r.advancing)
52
54
  ctx.log(`${node} not advancing yet (${r.before} -> ${r.after})`);
53
55
  return r.advancing;
54
56
  },
package/demo/run.js CHANGED
@@ -421,6 +421,12 @@ export class DemoSession {
421
421
  join(this.storeDir, "norsk-runtime"),
422
422
  "--proxy-port",
423
423
  String(this.ports.proxyPort),
424
+ // A throwaway daemon on a banded port has nothing to protect, and its
425
+ // /instance/<id>/... routes (the visualiser a gate reads, the `open`
426
+ // URLs) must answer without a session: with oauth on, the compose gate
427
+ // read the sign-in page as "0 frames" for 180s (playout CI, 2026-08-29).
428
+ "--proxy-auth",
429
+ "none",
424
430
  "--no-http-redirect",
425
431
  "--no-start-server",
426
432
  ...(this.publicHost ? ["--public-host", this.publicHost] : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {