@norskvideo/ctl-test-harness 0.1.22 → 0.1.24
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/compose-advancing.d.ts +22 -2
- package/compose-advancing.js +42 -12
- package/demo/gates.js +13 -2
- package/demo/run.js +6 -0
- package/package.json +1 -1
package/compose-advancing.d.ts
CHANGED
|
@@ -2,9 +2,24 @@ 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
|
+
/** Root workflows the visualiser listed — 0 while nothing is deployed. */
|
|
8
|
+
roots: number;
|
|
9
|
+
/** Whether any root workflow's summary carried a node of that name — a
|
|
10
|
+
* flat 0 with the node absent is a different failure from a stalled one. */
|
|
11
|
+
nodeSeen: boolean;
|
|
12
|
+
/** Why the sample is 0 when it is: the visualiser was unreachable, and how. */
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
5
15
|
/** One sample: the highest mediaIn of `node` across every root workflow under
|
|
6
|
-
* `base` (`.../visualiser`). An unreachable visualiser reads as 0
|
|
7
|
-
* caller's second sample, not a throw, decides. */
|
|
16
|
+
* `base` (`.../visualiser`). An unreachable visualiser reads as 0 with the
|
|
17
|
+
* reason beside it — the caller's second sample, not a throw, decides. */
|
|
18
|
+
export declare function composeSample(opts: {
|
|
19
|
+
fetch: FetchLike;
|
|
20
|
+
base: string;
|
|
21
|
+
node?: string;
|
|
22
|
+
}): Promise<ComposeSample>;
|
|
8
23
|
export declare function composeFrames(opts: {
|
|
9
24
|
fetch: FetchLike;
|
|
10
25
|
base: string;
|
|
@@ -15,6 +30,11 @@ export interface ComposeAdvancingResult {
|
|
|
15
30
|
before: number;
|
|
16
31
|
after: number;
|
|
17
32
|
node: string;
|
|
33
|
+
/** From the second sample: root workflows listed, and whether the node was in any. */
|
|
34
|
+
roots: number;
|
|
35
|
+
nodeSeen: boolean;
|
|
36
|
+
/** Set when a sample could not read the visualiser at all. */
|
|
37
|
+
error?: string;
|
|
18
38
|
}
|
|
19
39
|
/** Two samples `settleMs` apart: advancing when the second is above the first
|
|
20
40
|
* and above zero — the fallback card and a stalled feed both read flat. */
|
package/compose-advancing.js
CHANGED
|
@@ -24,38 +24,68 @@ export function maxNodeMediaIn(summary, node) {
|
|
|
24
24
|
}
|
|
25
25
|
return max;
|
|
26
26
|
}
|
|
27
|
+
function hasNode(summary, node) {
|
|
28
|
+
const nodes = summary?.nodes;
|
|
29
|
+
return Array.isArray(nodes) && nodes.some((n) => n?.name === node);
|
|
30
|
+
}
|
|
27
31
|
async function getJson(fetch, url) {
|
|
28
32
|
const r = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
29
33
|
if (!r.ok)
|
|
30
34
|
throw new Error(`GET ${url} -> ${r.status}`);
|
|
31
|
-
|
|
35
|
+
const text = await r.text();
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`GET ${url} -> ${r.status}, not JSON (a sign-in page? the route needs no auth): ${text.slice(0, 80)}`);
|
|
41
|
+
}
|
|
32
42
|
}
|
|
33
43
|
/** One sample: the highest mediaIn of `node` across every root workflow under
|
|
34
|
-
* `base` (`.../visualiser`). An unreachable visualiser reads as 0
|
|
35
|
-
* caller's second sample, not a throw, decides. */
|
|
36
|
-
export async function
|
|
44
|
+
* `base` (`.../visualiser`). An unreachable visualiser reads as 0 with the
|
|
45
|
+
* reason beside it — the caller's second sample, not a throw, decides. */
|
|
46
|
+
export async function composeSample(opts) {
|
|
37
47
|
const node = opts.node ?? DEFAULT_COMPOSE_NODE;
|
|
38
48
|
try {
|
|
39
49
|
const wf = (await getJson(opts.fetch, `${opts.base}/workflow`));
|
|
40
50
|
let max = 0;
|
|
51
|
+
let roots = 0;
|
|
52
|
+
let nodeSeen = false;
|
|
41
53
|
for (const root of wf?.rootWorkflows ?? []) {
|
|
42
|
-
|
|
54
|
+
// The engine numbers its workflows (`wfid: 3339`); a string-only check
|
|
55
|
+
// here skipped every root and the gate read a silent 0 -> 0 everywhere.
|
|
56
|
+
const wfid = root?.wfid;
|
|
57
|
+
if (typeof wfid !== "string" && typeof wfid !== "number")
|
|
43
58
|
continue;
|
|
44
|
-
|
|
59
|
+
roots += 1;
|
|
60
|
+
const summary = await getJson(opts.fetch, `${opts.base}/workflow/${encodeURIComponent(String(wfid))}/summary`);
|
|
61
|
+
if (hasNode(summary, node))
|
|
62
|
+
nodeSeen = true;
|
|
45
63
|
max = Math.max(max, maxNodeMediaIn(summary, node));
|
|
46
64
|
}
|
|
47
|
-
return max;
|
|
65
|
+
return { frames: max, roots, nodeSeen };
|
|
48
66
|
}
|
|
49
|
-
catch {
|
|
50
|
-
return 0;
|
|
67
|
+
catch (e) {
|
|
68
|
+
return { frames: 0, roots: 0, nodeSeen: false, error: e instanceof Error ? e.message : String(e) };
|
|
51
69
|
}
|
|
52
70
|
}
|
|
71
|
+
export async function composeFrames(opts) {
|
|
72
|
+
return (await composeSample(opts)).frames;
|
|
73
|
+
}
|
|
53
74
|
/** Two samples `settleMs` apart: advancing when the second is above the first
|
|
54
75
|
* and above zero — the fallback card and a stalled feed both read flat. */
|
|
55
76
|
export async function composeAdvancing(opts) {
|
|
56
77
|
const node = opts.node ?? DEFAULT_COMPOSE_NODE;
|
|
57
|
-
const before = await
|
|
78
|
+
const before = await composeSample({ fetch: opts.fetch, base: opts.base, node });
|
|
58
79
|
await new Promise((r) => setTimeout(r, opts.settleMs ?? DEFAULT_SETTLE_MS));
|
|
59
|
-
const after = await
|
|
60
|
-
|
|
80
|
+
const after = await composeSample({ fetch: opts.fetch, base: opts.base, node });
|
|
81
|
+
const error = after.error ?? before.error;
|
|
82
|
+
return {
|
|
83
|
+
advancing: after.frames > before.frames && after.frames > 0,
|
|
84
|
+
before: before.frames,
|
|
85
|
+
after: after.frames,
|
|
86
|
+
node,
|
|
87
|
+
roots: after.roots,
|
|
88
|
+
nodeSeen: after.nodeSeen,
|
|
89
|
+
...(error !== undefined ? { error } : {}),
|
|
90
|
+
};
|
|
61
91
|
}
|
package/demo/gates.js
CHANGED
|
@@ -48,8 +48,19 @@ export function composeAdvancingGate(opts = {}) {
|
|
|
48
48
|
node,
|
|
49
49
|
...(opts.settleMs !== undefined ? { settleMs: opts.settleMs } : {}),
|
|
50
50
|
});
|
|
51
|
-
if (
|
|
52
|
-
ctx.log(
|
|
51
|
+
if (r.error)
|
|
52
|
+
ctx.log(`visualiser unreachable at ${base}: ${r.error}`);
|
|
53
|
+
else if (!r.advancing) {
|
|
54
|
+
// A flat 0 has three causes worth telling apart before anyone reads
|
|
55
|
+
// engine logs: nothing deployed yet, a graph without that node, or a
|
|
56
|
+
// stalled feed into a node that is there.
|
|
57
|
+
const why = r.roots === 0
|
|
58
|
+
? "; no root workflows deployed yet"
|
|
59
|
+
: r.nodeSeen
|
|
60
|
+
? ""
|
|
61
|
+
: `; no node named ${node} in ${r.roots} root workflow(s)`;
|
|
62
|
+
ctx.log(`${node} not advancing yet (${r.before} -> ${r.after}${why})`);
|
|
63
|
+
}
|
|
53
64
|
return r.advancing;
|
|
54
65
|
},
|
|
55
66
|
};
|
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] : []),
|