@norskvideo/ctl-test-harness 0.1.32 → 0.1.33
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/package.json +1 -1
- package/smoke.d.ts +31 -0
- package/smoke.js +54 -1
package/package.json
CHANGED
package/smoke.d.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface SmokeContext {
|
|
|
41
41
|
interface Structural {
|
|
42
42
|
structural?: boolean;
|
|
43
43
|
}
|
|
44
|
+
/** The manifest's nav slot, as ctl's product page renders it. */
|
|
45
|
+
export interface SidebarEntry {
|
|
46
|
+
label: string;
|
|
47
|
+
icon?: string;
|
|
48
|
+
route: string;
|
|
49
|
+
}
|
|
44
50
|
export type SmokeObservation = Structural & ({
|
|
45
51
|
kind: "components";
|
|
46
52
|
ids: string[];
|
|
@@ -141,6 +147,17 @@ export interface SmokeDeps {
|
|
|
141
147
|
* is still up. Optional so an existing custom-deps consumer keeps working;
|
|
142
148
|
* the default reads every container carrying the instance's label. */
|
|
143
149
|
diagnose?(instanceId: string): string;
|
|
150
|
+
/** GET a manifest-declared route through the daemon's product proxy — the
|
|
151
|
+
* same URL ctl's product page links to. */
|
|
152
|
+
fetchProductRoute(opts: {
|
|
153
|
+
daemonPort: number;
|
|
154
|
+
product: string;
|
|
155
|
+
route: string;
|
|
156
|
+
storeDir: string;
|
|
157
|
+
}): Promise<{
|
|
158
|
+
status: number;
|
|
159
|
+
body: string;
|
|
160
|
+
}>;
|
|
144
161
|
log(line: string): void;
|
|
145
162
|
}
|
|
146
163
|
/** Per-slug port band, above the product integration harnesses' own bands:
|
|
@@ -152,6 +169,20 @@ export interface SmokePorts extends BaseHarnessPorts {
|
|
|
152
169
|
}
|
|
153
170
|
export declare function smokePorts(slug: string, overrides?: Partial<SmokePorts>): SmokePorts;
|
|
154
171
|
export declare const defaultSmokeDeps: SmokeDeps;
|
|
172
|
+
/** A manifest's `ui.sidebarEntries` are what ctl's product page renders as
|
|
173
|
+
* links, under /products/<name>/<route>. Nothing else checks that the product
|
|
174
|
+
* serves them, so a button that 404s ships silently — the shape 04c calls a
|
|
175
|
+
* declaration nothing checks. Image registrations only: the routes are served
|
|
176
|
+
* out of the image (the docs bundle is baked by `build:image`), so a dev-url
|
|
177
|
+
* run would fail on an artefact that is not under test. */
|
|
178
|
+
export declare function assertManifestRoutesServed(opts: {
|
|
179
|
+
entries: readonly SidebarEntry[];
|
|
180
|
+
product: string;
|
|
181
|
+
daemonPort: number;
|
|
182
|
+
storeDir: string;
|
|
183
|
+
fetchRoute: SmokeDeps["fetchProductRoute"];
|
|
184
|
+
log(line: string): void;
|
|
185
|
+
}): Promise<void>;
|
|
155
186
|
/** Whether an observation depends on a source being pumped. Structural ones
|
|
156
187
|
* (`components`, or anything the spec marks `structural`) run the satisfied
|
|
157
188
|
* phase only. */
|
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 { existsSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { basename, dirname, join } from "node:path";
|
|
23
23
|
import { dumpInstanceContainerLogs } from "./container-logs.js";
|
|
24
24
|
import { ensureRunnerOnNetwork, netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
|
|
@@ -83,8 +83,48 @@ export const defaultSmokeDeps = {
|
|
|
83
83
|
supportsNoPublish: ctlSupportsNoPublish,
|
|
84
84
|
containerUser: runnerContainerUser,
|
|
85
85
|
diagnose: (instanceId) => dumpInstanceContainerLogs(instanceId),
|
|
86
|
+
fetchProductRoute: async ({ daemonPort, product, route, storeDir }) => {
|
|
87
|
+
// The daemon fronts the product proxy with the same auth guard as /api/*,
|
|
88
|
+
// so the secret travels when the store has one (source-pump's rule).
|
|
89
|
+
const secretFile = join(storeDir, "proxy-secret");
|
|
90
|
+
const secret = existsSync(secretFile) ? readFileSync(secretFile, "utf8").trim() : "";
|
|
91
|
+
const url = `http://localhost:${daemonPort}/products/${encodeURIComponent(product)}${route.startsWith("/") ? route : `/${route}`}`;
|
|
92
|
+
const res = await fetch(url, {
|
|
93
|
+
headers: secret ? { "x-norsk-proxy": secret } : {},
|
|
94
|
+
redirect: "manual",
|
|
95
|
+
signal: AbortSignal.timeout(10_000),
|
|
96
|
+
});
|
|
97
|
+
return { status: res.status, body: (await res.text()).slice(0, 200) };
|
|
98
|
+
},
|
|
86
99
|
log: (line) => console.log(`[smoke] ${line}`),
|
|
87
100
|
};
|
|
101
|
+
/** A manifest's `ui.sidebarEntries` are what ctl's product page renders as
|
|
102
|
+
* links, under /products/<name>/<route>. Nothing else checks that the product
|
|
103
|
+
* serves them, so a button that 404s ships silently — the shape 04c calls a
|
|
104
|
+
* declaration nothing checks. Image registrations only: the routes are served
|
|
105
|
+
* out of the image (the docs bundle is baked by `build:image`), so a dev-url
|
|
106
|
+
* run would fail on an artefact that is not under test. */
|
|
107
|
+
export async function assertManifestRoutesServed(opts) {
|
|
108
|
+
if (opts.entries.length === 0) {
|
|
109
|
+
opts.log("manifest declares no sidebar routes to check");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of opts.entries) {
|
|
113
|
+
const where = `/products/${opts.product}${entry.route}`;
|
|
114
|
+
const { status, body } = await opts.fetchRoute({
|
|
115
|
+
daemonPort: opts.daemonPort,
|
|
116
|
+
product: opts.product,
|
|
117
|
+
route: entry.route,
|
|
118
|
+
storeDir: opts.storeDir,
|
|
119
|
+
});
|
|
120
|
+
// A redirect is served: /docs 301s to /docs/, which is the link working.
|
|
121
|
+
if (status >= 400) {
|
|
122
|
+
throw new Error(`manifest sidebar entry "${entry.label}" points at ${where}, which the product does not serve ` +
|
|
123
|
+
`(${status}: ${body.replace(/\s+/g, " ").trim().slice(0, 120)})`);
|
|
124
|
+
}
|
|
125
|
+
opts.log(`sidebar entry "${entry.label}" -> ${where} (${status})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
88
128
|
function describeObservation(o) {
|
|
89
129
|
switch (o.kind) {
|
|
90
130
|
case "components":
|
|
@@ -230,6 +270,19 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
230
270
|
throw new Error(`product add registered '${added.name}', expected '${spec.product.name}'`);
|
|
231
271
|
}
|
|
232
272
|
registered = true;
|
|
273
|
+
if ("image" in spec.product.register) {
|
|
274
|
+
await assertManifestRoutesServed({
|
|
275
|
+
entries: added.manifest?.ui?.sidebarEntries ?? [],
|
|
276
|
+
product: spec.product.name,
|
|
277
|
+
daemonPort: ports.daemonPort,
|
|
278
|
+
storeDir,
|
|
279
|
+
fetchRoute: deps.fetchProductRoute,
|
|
280
|
+
log: (line) => deps.log(`${slug}: ${line}`),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
deps.log(`${slug}: dev-url registration — manifest routes not checked (they are served out of the image)`);
|
|
285
|
+
}
|
|
233
286
|
// Template.
|
|
234
287
|
let templateName;
|
|
235
288
|
if ("name" in spec.template) {
|