| 1 | import { readdirSync } from "node:fs"; |
| 2 | |
| 3 | // Union of the former Linux desktop unit commands, including pnpm's pretest. |
| 4 | // Dedicated entrypoints remain authoritative for tests needing asset loaders. |
| 5 | export const ciUnitScripts = [ |
| 6 | "test:terminal", "test:task-monitor", "test:mcp-app", "test:workspace", |
| 7 | "test:stream", "test:motion", "test:composer", "test:todo-visibility", |
| 8 | "test:app-lifecycle", "test:all", "test:transcript", "test:usage-stats", |
| 9 | "test:ci-contract", |
| 10 | ]; |
| 11 | |
| 12 | export function testPlan(scripts, discovered) { |
| 13 | const explicit = new Map(); |
| 14 | let discover = false; |
| 15 | function add(args) { |
| 16 | if (args[0] === "node" && args[1] === "scripts/run-tests.mjs") { |
| 17 | discover = true; |
| 18 | return; |
| 19 | } |
| 20 | // Keep Node and tsx invocation modes, including custom loaders, intact. |
| 21 | const testPath = args.find(arg => /^src\/.*\.(?:test\.tsx?|tsx)$/.test(arg)); |
| 22 | const key = testPath ?? JSON.stringify(args); |
| 23 | const previous = explicit.get(key); |
| 24 | if (previous && JSON.stringify(previous.args) !== JSON.stringify(args)) throw new Error(`conflicting test invocation: ${key}`); |
| 25 | explicit.set(key, { key, args, serial: key.endsWith("history-performance-benchmark.tsx") }); |
| 26 | } |
| 27 | function expand(name, stack = []) { |
| 28 | if (stack.includes(name) || typeof scripts[name] !== "string") throw new Error(`invalid script dependency: ${name}`); |
| 29 | for (const hook of [`pre${name}`, name, `post${name}`]) { |
| 30 | if (hook !== name && !scripts[hook]) continue; |
| 31 | const body = scripts[hook]; |
| 32 | for (const command of body.split(" && ")) { |
| 33 | // Fail closed when the command grammar changes; never silently omit work. |
| 34 | if (/["'|;&<>`$\n]/.test(command)) throw new Error(`unsupported CI test command: ${command}`); |
| 35 | const args = command.trim().split(/\s+/); |
| 36 | if (args[0] === "pnpm" && args.length === 2) expand(args[1], [...stack, name]); |
| 37 | else if (["tsx", "node", "tsc"].includes(args[0])) add(args); |
| 38 | else throw new Error(`unsupported CI test command: ${command}`); |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | for (const name of ciUnitScripts) expand(name); |
| 43 | if (!discover) throw new Error("CI must include discovery of new frontend tests"); |
| 44 | for (const file of discovered.sort()) { |
| 45 | const key = `src/__tests__/${file}`; |
| 46 | if (!explicit.has(key)) add(["tsx", "--import", "./scripts/css-stub-register.mjs", key]); |
| 47 | } |
| 48 | return [...explicit.values()]; |
| 49 | } |
| 50 | |
| 51 | export function discoveredTests(directory = "src/__tests__") { |
| 52 | return readdirSync(directory).filter(name => /\.test\.tsx?$/.test(name)); |
| 53 | } |
| 54 |