import { test } from "node:test"; import assert from "node:assert/strict"; import { readdirSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { OFFSET_STEP, PORTS, PORT_BASES, portFor, portsForOffset, } from "./ports.mjs"; // Run with: // pnpm --filter yt-dlp-transcript-common test // // ports.mjs is the one copy of the port defaults. The places that CANNOT import // it (a package.json script's `${NAME:-N}`, the `--ports NAME:N` list handed to // queue-lock) and the ones that have not been pointed at it yet still spell a // number; this reads them as text and fails the moment one disagrees with the // table, or names a port the table does not declare. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const PACKAGES = ["", "common", "editor", "export", "homepage", "mcp", "umtool", "umtool/report-to-video"]; // A script's `${NAME:-N}` that is a number and NOT a port, named one by one so // every other spelled default is still read as a port: the editor's `start` // gives Node's thread pool 16 threads (envVars.ts, UV_THREADPOOL_SIZE). const NUMERIC_NOT_PORTS = new Set(["UV_THREADPOOL_SIZE"]); function scripts(dir: string): Record { const file = path.join(ROOT, dir, "package.json"); return (JSON.parse(readFileSync(file, "utf8")).scripts ?? {}) as Record; } // The playwright configs and every e2e helper: where a `process.env.X ?? N` // fallback can live. Walked, not listed, so a new helper is covered. function e2eSources(): string[] { const out: string[] = []; const walk = (dir: string) => { let names: string[]; try { names = readdirSync(dir); } catch { return; } for (const n of names) { if (n === "node_modules" || n.startsWith(".")) continue; const p = path.join(dir, n); if (statSync(p).isDirectory()) walk(p); else if (/\.(ts|mts|mjs|js)$/.test(n)) out.push(p); } }; for (const pkg of ["editor", "export", "homepage", "umtool"]) { for (const n of readdirSync(path.join(ROOT, pkg))) { if (/^playwright.*\.config\.ts$/.test(n)) out.push(path.join(ROOT, pkg, n)); } for (const d of ["e2e", "e2e-2origin"]) walk(path.join(ROOT, pkg, d)); } return out; } type Found = { where: string; name: string; port: number }; function found(): Found[] { const hits: Found[] = []; for (const pkg of PACKAGES) { for (const [script, line] of Object.entries(scripts(pkg))) { const where = `${pkg || "."}/package.json "${script}"`; for (const m of line.matchAll(/\$\{([A-Z0-9_]+):-(\d+)\}/g)) { if (NUMERIC_NOT_PORTS.has(m[1])) continue; hits.push({ where, name: m[1], port: Number(m[2]) }); } for (const m of line.matchAll(/--ports\s+(\S+)/g)) { for (const entry of m[1].split(",")) { const [name, port] = entry.split(":"); hits.push({ where: `${where} --ports`, name, port: Number(port) }); } } } } for (const file of e2eSources()) { const text = readFileSync(file, "utf8"); const where = path.relative(ROOT, file); // A chain (`process.env.EXPORT_E2E_PORT ?? process.env.PORT ?? 3020`) is the // FIRST name's default; the later names are fallbacks for a bare run. for (const m of text.matchAll( /process\.env\.([A-Z0-9_]*PORT)(?:\s*\?\?\s*process\.env\.[A-Z0-9_]+)*\s*\?\?\s*(\d+)/g, )) { hits.push({ where, name: m[1], port: Number(m[2]) }); } for (const m of text.matchAll(/PLAYWRIGHT_BASE_URL\s*\?\?\s*["'`]http:\/\/localhost:(\d+)/g)) { hits.push({ where, name: "PORT", port: Number(m[1]) }); } } return hits; } test("every spelled port default agrees with ports.mjs", () => { const hits = found(); // The scan is not vacuous: the editor's dev script and its queue-lock list // are both spelled today, and must be found. assert.ok(hits.some((h) => h.where.startsWith("editor/package.json") && h.name === "EDITOR_PORT")); assert.ok(hits.some((h) => h.where.endsWith("--ports") && h.name === "PORT")); const wrong = hits .filter((h) => PORT_BASES[h.name] !== h.port) .map((h) => h.name in PORT_BASES ? `${h.where}: ${h.name} defaults to ${h.port}, ports.mjs says ${PORT_BASES[h.name]}` : `${h.where}: ${h.name} is not declared in common/lib/ports.mjs`, ); assert.deepEqual(wrong, []); }); test("no two ports share a base, and a worktree block never overlaps the next", () => { const bases = Object.values(PORT_BASES); assert.equal(new Set(bases).size, bases.length); // The 30xx family must fit inside one OFFSET_STEP, or worktree #1's block // would reach into worktree #2's. const family = bases.filter((b) => b >= 3000 && b < 4000); assert.ok(Math.max(...family) - Math.min(...family) < OFFSET_STEP); }); test("portsForOffset adds the offset to every port and names the editor test URL", () => { const env = portsForOffset(300); for (const [name, base] of Object.entries(PORT_BASES)) { assert.equal(env[name], String(base + 300)); } assert.equal(env.PLAYWRIGHT_BASE_URL, `http://localhost:${PORT_BASES.PORT + 300}`); assert.equal(Object.keys(env).length, Object.keys(PORTS).length + 1); }); test("portFor: the env wins, else the base; an unknown name or a non-port throws", () => { assert.equal(portFor("PORT", {}), 3011); assert.equal(portFor("PORT", { PORT: "3611" }), 3611); assert.equal(portFor("PORT", { PORT: " " }), 3011); assert.throws(() => portFor("PROT", {}), /no port named PROT/); assert.throws(() => portFor("PORT", { PORT: "30x1" }), /is not a port/); });