@norskvideo/ctl-dev-kit 0.2.77 → 0.2.79

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.
@@ -13,6 +13,7 @@
13
13
  // directory below is not a free choice.
14
14
  import type { GeneratedFile, ShapeContext } from "../create-product.ts";
15
15
  import { EXAMPLE_ID } from "./components.ts";
16
+ import { TEST_SETUP_TS, UI_TEST_DEV_DEPENDENCIES, VITEST_CONFIG_TS } from "./ui-tests.ts";
16
17
 
17
18
  /** The tar ships workflow.yml at its root, so Studio knows the workflow by this name. */
18
19
  export const WORKFLOW_NAME = "workflow";
@@ -39,6 +40,7 @@ function packageJson(ctx: ShapeContext): string {
39
40
  dev: "bunx vite build --watch",
40
41
  clean: `rm -rf ../dashboards/${WORKFLOW_NAME}`,
41
42
  typecheck: "bunx tsc --noEmit",
43
+ test: "vitest run",
42
44
  },
43
45
  dependencies: {
44
46
  react: "^19.0.0",
@@ -51,6 +53,7 @@ function packageJson(ctx: ShapeContext): string {
51
53
  "@vitejs/plugin-react": "^6.0.1",
52
54
  typescript: "^5.7.2",
53
55
  vite: "^8.0.0",
56
+ ...UI_TEST_DEV_DEPENDENCIES,
54
57
  },
55
58
  },
56
59
  null,
@@ -209,6 +212,38 @@ export default function App({ env }: { env: Env }) {
209
212
  }
210
213
  `;
211
214
 
215
+ // A stand-in for the component's state socket: what a dashboard test drives
216
+ // instead of a running Studio.
217
+ const APP_TEST_TSX = `import { act, render, screen } from "@testing-library/react";
218
+ import { afterEach, expect, test, vi } from "vitest";
219
+ import App from "./App.tsx";
220
+
221
+ class FakeSocket {
222
+ static last: FakeSocket | undefined;
223
+ onmessage: ((event: { data: string }) => void) | null = null;
224
+ constructor(readonly url: string) {
225
+ FakeSocket.last = this;
226
+ }
227
+ close() {}
228
+ deliver(message: unknown) {
229
+ this.onmessage?.({ data: JSON.stringify(message) });
230
+ }
231
+ }
232
+
233
+ afterEach(() => {
234
+ vi.unstubAllGlobals();
235
+ });
236
+
237
+ test("shows the component's state as it arrives", () => {
238
+ vi.stubGlobal("WebSocket", FakeSocket);
239
+ render(<App env={{ apiBasePath: "/api", wsBasePath: "/ws" }} />);
240
+ expect(screen.getByText(/waiting for the component's first state/)).toBeInTheDocument();
241
+
242
+ act(() => FakeSocket.last?.deliver({ type: "state-update", state: { ticks: 3 } }));
243
+ expect(screen.getByText("3 ticks")).toBeInTheDocument();
244
+ });
245
+ `;
246
+
212
247
  const MAIN_TSX = `import React from "react";
213
248
  import ReactDOM from "react-dom/client";
214
249
  import App from "./App.tsx";
@@ -263,9 +298,12 @@ export function dashboardFiles(ctx: ShapeContext): GeneratedFile[] {
263
298
  { path: "dashboard/package.json", content: packageJson(ctx) },
264
299
  { path: "dashboard/tsconfig.json", content: TSCONFIG },
265
300
  { path: "dashboard/vite.config.ts", content: VITE_CONFIG_TS },
301
+ { path: "dashboard/vitest.config.ts", content: VITEST_CONFIG_TS },
266
302
  { path: "dashboard/index.html", content: indexHtml(ctx) },
267
303
  { path: "dashboard/src/main.tsx", content: MAIN_TSX },
268
304
  { path: "dashboard/src/App.tsx", content: APP_TSX },
305
+ { path: "dashboard/src/App.test.tsx", content: APP_TEST_TSX },
306
+ { path: "dashboard/src/test/setup.ts", content: TEST_SETUP_TS },
269
307
  { path: "dashboard/src/api.ts", content: API_TS },
270
308
  { path: "dashboard/src/index.css", content: INDEX_CSS },
271
309
  { path: "backend/src/lib/dashboards.ts", content: BACKEND_LIB_DASHBOARDS_TS },
@@ -13,6 +13,7 @@
13
13
  // three independent edits, and why the dev script builds dist before starting
14
14
  // the backend that serves it.
15
15
  import type { GeneratedFile, ShapeContext } from "../create-product.ts";
16
+ import { TEST_SETUP_TS, UI_TEST_DEV_DEPENDENCIES, VITEST_CONFIG_TS } from "./ui-tests.ts";
16
17
 
17
18
  export const CONFIG_SCREEN_URL = "/configure";
18
19
 
@@ -31,6 +32,7 @@ function packageJson(ctx: ShapeContext): string {
31
32
  build: "bunx vite build",
32
33
  clean: "rm -rf dist",
33
34
  typecheck: "bunx tsc --noEmit",
35
+ test: "vitest run",
34
36
  },
35
37
  dependencies: {
36
38
  [`${ctx.scope}/shared`]: "workspace:*",
@@ -44,6 +46,7 @@ function packageJson(ctx: ShapeContext): string {
44
46
  "@vitejs/plugin-react": "^6.0.1",
45
47
  typescript: "^5.7.2",
46
48
  vite: "^8.0.0",
49
+ ...UI_TEST_DEV_DEPENDENCIES,
47
50
  },
48
51
  },
49
52
  null,
@@ -141,6 +144,18 @@ export default function App() {
141
144
  `;
142
145
  }
143
146
 
147
+ function appTestTsx(ctx: ShapeContext): string {
148
+ return `import { render, screen } from "@testing-library/react";
149
+ import { expect, test } from "vitest";
150
+ import App from "./App.tsx";
151
+
152
+ test("names the product", () => {
153
+ render(<App />);
154
+ expect(screen.getByRole("heading", { name: "${ctx.name}" })).toBeInTheDocument();
155
+ });
156
+ `;
157
+ }
158
+
144
159
  const INDEX_CSS = `:root {
145
160
  color-scheme: light dark;
146
161
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
@@ -157,9 +172,12 @@ export function frontendFiles(ctx: ShapeContext): GeneratedFile[] {
157
172
  { path: "frontend/package.json", content: packageJson(ctx) },
158
173
  { path: "frontend/tsconfig.json", content: TSCONFIG },
159
174
  { path: "frontend/vite.config.ts", content: VITE_CONFIG_TS },
175
+ { path: "frontend/vitest.config.ts", content: VITEST_CONFIG_TS },
160
176
  { path: "frontend/index.html", content: indexHtml(ctx) },
161
177
  { path: "frontend/src/main.tsx", content: MAIN_TSX },
162
178
  { path: "frontend/src/App.tsx", content: appTsx(ctx) },
179
+ { path: "frontend/src/App.test.tsx", content: appTestTsx(ctx) },
180
+ { path: "frontend/src/test/setup.ts", content: TEST_SETUP_TS },
163
181
  { path: "frontend/src/index.css", content: INDEX_CSS },
164
182
  ];
165
183
  }
@@ -0,0 +1,44 @@
1
+ // The test runner the two screen workspaces (frontend/, dashboard/) share:
2
+ // vitest under jsdom with testing-library, run by the root `test:unit`, so the
3
+ // product's one test command covers the screens too.
4
+
5
+ export const UI_TEST_DEV_DEPENDENCIES = {
6
+ "@testing-library/jest-dom": "^6.9.1",
7
+ "@testing-library/react": "^16.3.2",
8
+ jsdom: "^29.0.1",
9
+ vitest: "^4.1.3",
10
+ };
11
+
12
+ // Like vite.config.ts, kept out of the tsconfig `include`: it imports the
13
+ // bundler.
14
+ export const VITEST_CONFIG_TS = `import { createRequire } from "node:module";
15
+ import { dirname } from "node:path";
16
+ import react from "@vitejs/plugin-react";
17
+ import { defineConfig } from "vitest/config";
18
+
19
+ // One React instance: alias to the pair react-dom loads, so testing-library
20
+ // cannot pull in a second copy (every hook then throws).
21
+ const require = createRequire(import.meta.url);
22
+
23
+ export default defineConfig({
24
+ plugins: [react()],
25
+ resolve: {
26
+ alias: { react: dirname(require.resolve("react")), "react-dom": dirname(require.resolve("react-dom")) },
27
+ dedupe: ["react", "react-dom"],
28
+ },
29
+ test: {
30
+ include: ["src/**/*.test.{ts,tsx}"],
31
+ environment: "jsdom",
32
+ setupFiles: ["./src/test/setup.ts"],
33
+ },
34
+ });
35
+ `;
36
+
37
+ // Without vitest globals, testing-library cannot register its own cleanup, so
38
+ // renders would pile up across tests in one document.
39
+ export const TEST_SETUP_TS = `import "@testing-library/jest-dom/vitest";
40
+ import { cleanup } from "@testing-library/react";
41
+ import { afterEach } from "vitest";
42
+
43
+ afterEach(cleanup);
44
+ `;
@@ -158,6 +158,9 @@ shared core.
158
158
 
159
159
  function claudeTail(ctx: ShapeContext): string {
160
160
  const workspaceNotes = featureNotes(ctx);
161
+ const screens = (["dashboard", "frontend"] as const).filter((f) => ctx.features.has(f));
162
+ const screenTests =
163
+ screens.length === 0 ? "" : `, then the vitest suites of ${screens.map((s) => `\`${s}/\``).join(" and ")} (jsdom)`;
161
164
  return `${workspaceNotes}## Getting norsk-ctl + the dev loop
162
165
 
163
166
  \`norsk-ctl\` (the daemon/CLI) is **not** an npm dep — it's the released binary,
@@ -285,7 +288,7 @@ refreshed \`~/.norsk-ctl/product-templates/<name>/compose.yml\` carries the pins
285
288
  Byte-snapshots (\`tests/unit/workflow-snapshot.test.ts\`) pin the emitted wire
286
289
  bytes; \`UPDATE_SNAPSHOTS=1\` refreshes deliberately.
287
290
 
288
- - \`bun run test\` / \`test:unit\` — shared/backend units + tests/unit scaffolds.
291
+ - \`bun run test\` / \`test:unit\` — shared/backend units + tests/unit scaffolds${screenTests}.
289
292
  - \`bun run test:image\` — image smoke test (needs Docker + \`bun run build:image\`):
290
293
  runs the built container and asserts the surface \`product add\` probes.
291
294
  - \`bun run check:drift\` — the shared-conventions gate; CI runs it plus
@@ -322,13 +325,17 @@ function rootPackageJson(ctx: ShapeContext): string {
322
325
  lint: "biome check .",
323
326
  "lint:fix": "biome check --write .",
324
327
  test: "bun run test:unit",
325
- "test:unit": `bun test shared/src/ backend/src/ ${hasComponents ? "components/src/ " : ""}tests/unit/`,
328
+ "test:unit": [
329
+ `bun test shared/src/ backend/src/ ${hasComponents ? "components/src/ " : ""}tests/unit/`,
330
+ ...(hasDashboard ? ["bun run --cwd dashboard test"] : []),
331
+ ...(hasFrontend ? ["bun run --cwd frontend test"] : []),
332
+ ].join(" && "),
326
333
  "test:image": "bun test tests/image/",
327
334
  "test:smoke": "NORSK_CTL_ENV=test bun test tests/smoke.spec.ts",
328
335
  "check:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/check-drift.ts",
329
336
  "docs:check":
330
337
  "bun run node_modules/@norskvideo/ctl-dev-kit/docs/path-existence.ts && bun run node_modules/@norskvideo/ctl-dev-kit/docs/light-pages.ts && bun run node_modules/@norskvideo/ctl-dev-kit/docs/journeys.ts",
331
- "docs:manual": `bun run node_modules/@norskvideo/ctl-dev-kit/doc-guide/prose-bundle.js --brand "${ctx.name}" --title "${ctx.name} — Documentation" --link "norsk.video=https://norsk.video/" --out docs/generated/bundle docs/handover.md docs/operating.md docs/known-limitations.md`,
338
+ "docs:manual": `bun run node_modules/@norskvideo/ctl-dev-kit/doc-guide/prose-bundle.js --brand "${ctx.name}" --title "${ctx.name} — Documentation" --link "norsk.video=https://norsk.video/" --out docs/generated/bundle docs/handover.md docs/administering.md docs/operating.md docs/known-limitations.md`,
332
339
  "docs:handover": "bash scripts/docs-handover.sh",
333
340
  "docs:handover:check": "bash scripts/docs-handover.sh --check",
334
341
  // The docs SITE (website/) — a standalone package, so its install is its
@@ -2044,7 +2051,8 @@ norsk-ctl and the pinned Norsk SDK:
2044
2051
  Two channels, deliberately:
2045
2052
 
2046
2053
  - **The bundle the image serves.** \`bun run docs:manual\` builds it from
2047
- \`docs/handover.md\`, \`docs/operating.md\` and \`docs/known-limitations.md\`;
2054
+ \`docs/handover.md\`, \`docs/administering.md\`, \`docs/operating.md\` and
2055
+ \`docs/known-limitations.md\`;
2048
2056
  the image build runs
2049
2057
  it, so every image serves them at /docs at the version that is running
2050
2058
  (linked from the product's page in norsk-ctl). \`bun run docs:handover\`
@@ -2070,6 +2078,7 @@ served at /docs at the version that is running.
2070
2078
  | Document | Audience | What it is |
2071
2079
  | ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------- |
2072
2080
  | \`docs/handover.md\` | whoever gets this product running | How norsk-ctl arrives, the one command that adds this product, and the bundle route for platform teams |
2081
+ | \`docs/administering.md\` | the server's administrator | What runs on the server and where, how to reach it, update it, back it up and remove it |
2073
2082
  | \`docs/operating.md\` | operators | What a running deployment looks like from the operator's chair: the controls, the checks, the signs |
2074
2083
  | \`docs/known-limitations.md\` | operators | What the product does not do yet, with the consequence and the workaround |
2075
2084
 
@@ -2113,7 +2122,7 @@ the Administrator's hat._
2113
2122
  | Evaluator | \`/docs/\` | \`/docs/guides/\` | [norsk-ctl's manual](https://ctl.norsk.video/), to run it for real |
2114
2123
  | Builder | \`/docs/guides/\` | \`/docs/how-to/\` | \`/docs/reference/\`, for the settings they just used |
2115
2124
  | Integrator | \`/docs/reference/\` | \`/docs/concepts/\` | [norsk-ctl's CLI and API reference](https://ctl.norsk.video/) |
2116
- | Administrator | \`handover.md\` | \`operating.md\`, \`known-limitations.md\` | [norsk-ctl's manual](https://ctl.norsk.video/), for everything about the machine |
2125
+ | Administrator | \`handover.md\` | \`administering.md\`, \`operating.md\`, \`known-limitations.md\` | [norsk-ctl's manual](https://ctl.norsk.video/), for everything about the machine |
2117
2126
 
2118
2127
  Site paths resolve under website/src/content/docs; a bare name resolves under
2119
2128
  docs/. Each route is at most five pages and ends in a hand-off, which is the
@@ -2218,6 +2227,122 @@ and its health check at /healthz are for._
2218
2227
  `;
2219
2228
  }
2220
2229
 
2230
+ // The server admin's page: the product running under norsk-ctl on the
2231
+ // customer's own machine (gameplay-football247's dev-ops guide, generalised).
2232
+ // norsk-ctl's server install is the same everywhere, so its half is written
2233
+ // out; the product's half carries a marker per section.
2234
+ function administeringMd(ctx: ShapeContext): string {
2235
+ return `# Administering ${ctx.name}
2236
+
2237
+ For whoever looks after the server ${ctx.name} runs on: what is installed
2238
+ where, how to reach it, how to update it, and how to remove it. Getting it
2239
+ running the first time is in handover.md; running the product day to day is in
2240
+ operating.md.
2241
+
2242
+ ## What runs on the server
2243
+
2244
+ ${ctx.name} runs under **norsk-ctl**, Norsk's service manager.
2245
+
2246
+ | Process / container | What it is |
2247
+ |---|---|
2248
+ | \`norsk-ctl\` (systemd service) | The manager: launches and supervises ${ctx.name}, serves its API |
2249
+ | \`norsk-proxy\`, \`norsk-proxy-oauth2\` | The HTTPS entry point and its login |
2250
+ | \`norsk-product-${ctx.name}\` | The product's control plane |
2251
+ | \`<instance>-<service>-1\` | One per service of a launched instance |
2252
+
2253
+ <!-- ctl-docs-template: fill me -->
2254
+ _Name this product's instance containers and say what each one does (the
2255
+ starter's are Studio, the workflow and its dashboard, and the media engine)._
2256
+
2257
+ ## Where things are
2258
+
2259
+ | Path | Contents |
2260
+ |---|---|
2261
+ | \`/etc/norsk-ctl/\` | norsk-ctl's configuration (\`config.yaml\`), licences, TLS certificate |
2262
+ | \`/var/lib/norsk-ctl/\` | norsk-ctl's state: database, product registry, templates, proxy |
2263
+ | \`/var/log/norsk-ctl/\` | norsk-ctl's logs |
2264
+ | \`/opt/norsk-ctl/bin/\` | norsk-ctl binaries, one per version; \`/usr/local/bin/norsk-ctl\` points at the current one |
2265
+ | \`/etc/systemd/system/norsk-ctl.service\` | The service unit |
2266
+ | \`/etc/sysctl.d/90-norsk-tuning.conf\` | Network buffer tuning for media |
2267
+ <!-- path-allow -->
2268
+
2269
+ <!-- ctl-docs-template: fill me -->
2270
+ _Add the instance's working directory (uploaded media, settings) and anything
2271
+ else this product keeps on disk._
2272
+
2273
+ norsk-ctl runs as the user \`norsk\`. Run its CLI as that user, with its
2274
+ configuration:
2275
+
2276
+ \`\`\`sh
2277
+ sudo -u norsk env NORSK_CTL_CONFIG_DIR=/etc/norsk-ctl NORSK_CTL_STATE_DIR=/var/lib/norsk-ctl \\
2278
+ NORSK_CTL_LOG_DIR=/var/log/norsk-ctl norsk-ctl instance list
2279
+ \`\`\`
2280
+
2281
+ ## Network
2282
+
2283
+ <!-- ctl-docs-template: fill me -->
2284
+ | Port | Open to | What |
2285
+ |---|---|---|
2286
+ | 22/tcp | administrators | SSH |
2287
+ | _443/tcp_ | _operators_ | _The web page (or loopback only: see below)_ |
2288
+ | _the product's inputs and outputs_ | _the senders and receivers_ | _one row per media port_ |
2289
+
2290
+ ## Reaching the web page
2291
+
2292
+ The web page is norsk-ctl's proxy. On a server that must not expose it, bind it
2293
+ to loopback (norsk-ctl's \`bindAddress\` setting) and reach it through an SSH
2294
+ tunnel:
2295
+
2296
+ \`\`\`sh
2297
+ ssh -L 8443:127.0.0.1:443 <you>@<server> # 443: the proxy's port, if unchanged
2298
+ # then browse https://localhost:8443
2299
+ \`\`\`
2300
+
2301
+ A self-signed certificate is accepted once in the browser.
2302
+
2303
+ ## Installing and updating
2304
+
2305
+ <!-- ctl-docs-template: fill me -->
2306
+ _The release this product ships and the one command that installs or updates
2307
+ it, with the settings it takes. Without a release kit: norsk-ctl's installer,
2308
+ then \`norsk-ctl product add\` (handover.md); a new image is \`norsk-ctl product
2309
+ reload\`; norsk-ctl itself is \`norsk-ctl upgrade --version <v>\`, as root._
2310
+
2311
+ ## Checking it is healthy
2312
+
2313
+ \`\`\`sh
2314
+ systemctl status norsk-ctl # the manager
2315
+ journalctl -u norsk-ctl -f # its log
2316
+ docker ps # every container above, Up
2317
+ \`\`\`
2318
+
2319
+ <!-- ctl-docs-template: fill me -->
2320
+ _What the product's own page shows when it is healthy, and which container's
2321
+ log to read first when it is not._
2322
+
2323
+ ## Removing it
2324
+
2325
+ <!-- ctl-docs-template: fill me -->
2326
+ _How to remove the product, and norsk-ctl with it, and what is left behind.
2327
+ Say plainly what is deleted (uploaded media) and how to keep it._
2328
+
2329
+ ## Backups
2330
+
2331
+ norsk-ctl's state worth keeping is \`/etc/norsk-ctl\` and \`/var/lib/norsk-ctl\`,
2332
+ plus each instance's working directory.
2333
+
2334
+ <!-- ctl-docs-template: fill me -->
2335
+ _Anything else of this product's that is worth keeping._
2336
+
2337
+ ## Licence
2338
+
2339
+ ${ctx.name} runs under a Norsk licence that entitles it, staged in
2340
+ \`/etc/norsk-ctl/licenses/\`. The product takes its licence when it is added
2341
+ (\`norsk-ctl product add --license-file\`), so a new licence goes in with the
2342
+ next \`product add\`.
2343
+ `;
2344
+ }
2345
+
2221
2346
  const KNOWN_LIMITATIONS_MD = `# Known limitations
2222
2347
 
2223
2348
  What this product does not do yet, or does with a caveat an operator must know
@@ -2298,6 +2423,7 @@ export const turnkey: ShapeModule = {
2298
2423
  { path: "docs/README.md", content: docsReadmeMd(ctx) },
2299
2424
  { path: "docs/handover.md", content: handoverMd(ctx) },
2300
2425
  { path: "docs/operating.md", content: operatingMd(ctx) },
2426
+ { path: "docs/administering.md", content: administeringMd(ctx) },
2301
2427
  { path: "docs/known-limitations.md", content: KNOWN_LIMITATIONS_MD },
2302
2428
  { path: "docs/personas.md", content: personasMd(ctx) },
2303
2429
  { path: ".github/workflows/build-docs.yml", content: ctx.canon.buildDocs },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.77",
3
+ "version": "0.2.79",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",
@@ -44,16 +44,22 @@
44
44
  "@norskvideo/norsk-studio-builder": "1.30.0-2026-10-12-34c09bda",
45
45
  "@norskvideo/norsk-studio-built-ins": "1.30.0-2026-10-12-34c09bda",
46
46
  "@playwright/test": "^1.59.0",
47
+ "@testing-library/jest-dom": "^6.9.1",
48
+ "@testing-library/react": "^16.3.2",
47
49
  "@types/bun": "latest",
48
50
  "@types/cors": "^2.8.17",
49
51
  "@types/express": "5",
50
52
  "@types/node": "^22.10.0",
51
53
  "@types/react": "^19.0.10",
52
54
  "@types/react-dom": "^19.0.3",
55
+ "@vitejs/plugin-react": "^6.0.1",
53
56
  "cors": "^2.8.5",
54
57
  "express": "5",
58
+ "jsdom": "^29.0.1",
55
59
  "react": "^19.0.0",
56
60
  "react-dom": "^19.0.0",
61
+ "vite": "^8.0.0",
62
+ "vitest": "^4.1.3",
57
63
  "yaml": "^2.8.0",
58
64
  "zod": "4.4.3"
59
65
  },