#!/usr/bin/env node // scripts/coverage/mutate.mjs — L3 of the coverage architecture (ADR-020). // // Driver for Stryker mutation testing. Discovers every package with a // stryker.config.json, then runs Stryker per-feature with the feature's // vitest config + a narrowed mutate scope (entities + use-cases by default, // per the shared base config). // // Usage: // pnpm mutate # run for every feature with a config // pnpm mutate -- --filter @repo/auth # run for one feature // pnpm mutate -- --filter @repo/auth --since main # incremental mode // pnpm mutate -- --json # machine-readable summary only // // This runs Stryker as a child process, not via dynamic import — Stryker's // runtime expects to own the test process and our wrapping logic keeps the // integration simple. Stryker handles concurrency internally. import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; const STRYKER_BIN = "node_modules/.bin/stryker"; /** * Walk packages/* and apps/* for stryker.config.json files. * Returns: [{ packageDir, packageName, configPath }] */ export function discoverStrykerConfigs(repoRoot) { const out = []; for (const root of ["packages", "apps"]) { const dir = path.join(repoRoot, root); if (!fs.existsSync(dir)) continue; for (const pkg of fs.readdirSync(dir)) { const configPath = path.join(dir, pkg, "stryker.config.json"); if (!fs.existsSync(configPath)) continue; const pkgJsonPath = path.join(dir, pkg, "package.json"); let packageName = `${root}/${pkg}`; if (fs.existsSync(pkgJsonPath)) { try { packageName = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).name ?? packageName; } catch { // fall through } } out.push({ packageDir: path.join(root, pkg), packageName, configPath, }); } } return out.sort((a, b) => a.packageDir.localeCompare(b.packageDir)); } function parseArgs(argv) { const out = { filter: null, since: null, json: false }; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === "--filter") out.filter = argv[++i]; else if (a === "--since") out.since = argv[++i]; else if (a === "--json") out.json = true; else if (a === "--help" || a === "-h") { console.log( "Usage: pnpm mutate [-- --filter ] [--since ] [--json]", ); process.exit(0); } } return out; } function main() { const args = parseArgs(process.argv); const repoRoot = process.cwd(); const all = discoverStrykerConfigs(repoRoot); const targets = args.filter ? all.filter( (c) => c.packageName === args.filter || c.packageDir.endsWith(args.filter), ) : all; if (targets.length === 0) { process.stderr.write( `[mutate] No stryker.config.json found${args.filter ? ` matching ${args.filter}` : ""}.\n` + `Each feature wanting L3 mutation needs a stryker.config.json. See docs/guides/coverage.md.\n`, ); process.exit(args.filter ? 1 : 0); } const strykerBinAbs = path.join(repoRoot, STRYKER_BIN); if (!fs.existsSync(strykerBinAbs)) { process.stderr.write( `[mutate] Stryker binary not found at ${STRYKER_BIN}. Run \`pnpm install\`.\n`, ); process.exit(1); } const results = []; for (const target of targets) { if (!args.json) { process.stderr.write( `\n[mutate] === ${target.packageName} (${target.packageDir}) ===\n`, ); } const strykerArgs = ["run", target.configPath]; if (args.since) { strykerArgs.push("--since", args.since); } const result = spawnSync(strykerBinAbs, strykerArgs, { cwd: path.join(repoRoot, target.packageDir), stdio: args.json ? "pipe" : "inherit", env: { ...process.env, FORCE_COLOR: args.json ? "0" : "1" }, }); results.push({ package: target.packageName, packageDir: target.packageDir, exitCode: result.status, status: result.status === 0 ? "pass" : "fail", }); // If a feature fails, continue to next. Surface failures at the end so // a single broken feature doesn't mask state for the others. } const anyFailed = results.some((r) => r.exitCode !== 0); if (args.json) { process.stdout.write( JSON.stringify( { status: anyFailed ? "fail" : "pass", results }, null, 2, ) + "\n", ); } else { process.stderr.write("\n[mutate] Summary:\n"); for (const r of results) { process.stderr.write( ` ${r.status === "pass" ? "✓" : "✗"} ${r.package}\n`, ); } } process.exit(anyFailed ? 1 : 0); } const invokedDirectly = import.meta.url === `file://${process.argv[1]}`; if (invokedDirectly) { main(); }