#!/usr/bin/env node import { spawn } from "node:child_process"; import { mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { getE2ESuite, listE2ESuites } from "./suite-registry.mjs"; const DEFAULT_E2E_DATABASE_URL = "postgres://bookorbit:bookorbit@localhost:5432/bookorbit_e2e"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const rootDir = join(scriptDir, "..", ".."); const resultsDir = join(rootDir, "test-results", "server"); function isCiEnvironment() { return (process.env.CI ?? "").toLowerCase() === "true"; } function trimLeadingSeparator(args) { if (args[0] === "--") { return args.slice(1); } return args; } function printUsage() { const usage = [ "Usage:", " pnpm run e2e:run -- [vitest args...]", "", "Examples:", " pnpm run e2e:run -- app-smoke", " pnpm run e2e:run -- all", " pnpm run e2e:run -- metadata-write", ' pnpm run e2e:run -- metadata-write --testNamePattern="concurrency hardening"', " pnpm run e2e:run -- metadata-write --shard=1/2", " pnpm run e2e:run -- scanner-scenarios --testNamePattern=book-per-folder-disc-folder-flattening", " pnpm run e2e:run -- --list", " pnpm run e2e:list", ]; console.log(usage.join("\n")); } function printSuiteList() { const suites = listE2ESuites(); console.log("Available e2e suites:"); console.log("- all [composite] - Run all suites sequentially"); for (const suite of suites) { const dbMode = suite.prepareDedicatedDatabase ? "dedicated-db" : "default-db"; const requirements = [dbMode, ...(suite.requiresDocker ? ["docker"] : [])].join(", "); console.log(`- ${suite.id} [${requirements}] - ${suite.description}`); } } async function runCommand(command, args, envOverrides = {}) { await new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: rootDir, env: { ...process.env, ...envOverrides }, stdio: "inherit", }); child.once("error", reject); child.once("exit", (code, signal) => { if (code === 0) { resolve(undefined); return; } const reason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`; reject(new Error(`Command failed (${reason}): ${command} ${args.join(" ")}`)); }); }); } async function main() { const cliArgs = trimLeadingSeparator(process.argv.slice(2)); if (cliArgs.length === 0 || cliArgs.includes("--help") || cliArgs.includes("-h")) { printUsage(); process.exit(cliArgs.length === 0 ? 1 : 0); } if (cliArgs.includes("--list")) { printSuiteList(); return; } const [suiteId, ...remainingArgs] = cliArgs; const vitestArgs = trimLeadingSeparator(remainingArgs); const suites = suiteId === "all" ? listE2ESuites() : [getE2ESuite(suiteId)]; if (suiteId !== "all" && !suites[0]) { console.error(`Unknown e2e suite "${suiteId}".`); printSuiteList(); process.exit(1); } if (suiteId === "all" && suites.length === 0) { console.error("No e2e suites are configured."); process.exit(1); } if (suiteId === "all" && process.env.JUNIT_OUTPUT) { console.error("JUNIT_OUTPUT is only supported when running a single suite. Unset JUNIT_OUTPUT or run one suite id."); process.exit(1); } mkdirSync(resultsDir, { recursive: true }); const e2eDatabaseUrl = process.env.E2E_DATABASE_URL ?? DEFAULT_E2E_DATABASE_URL; if (suites.some((suite) => suite.requiresDocker)) { console.log("Verifying Docker availability..."); await runCommand("docker", ["version", "--format", "{{.Server.Version}}"]); } if (!isCiEnvironment() && suites.some((suite) => suite.prepareDedicatedDatabase)) { console.log("Starting local PostgreSQL (dev compose)..."); await runCommand("pnpm", ["run", "db:up"]); } for (const [index, suite] of suites.entries()) { // E2E_DATABASE_URL is not in .env so pnpm's auto-loading cannot override it. // vitest.config.e2e.ts reads this and injects it as DATABASE_URL into all // test workers, ensuring NestJS never connects to the dev database. const runEnvironment = { E2E_DATABASE_URL: e2eDatabaseUrl }; if (suite.prepareDedicatedDatabase) { console.log(`Resetting and migrating dedicated e2e database for suite ${suite.id}...`); await runCommand("pnpm", ["run", "e2e:db:prepare"], { E2E_DATABASE_URL: e2eDatabaseUrl }); } const junitOutput = process.env.JUNIT_OUTPUT ?? suite.junitOutput; if (suites.length > 1) { console.log(`Running e2e suite [${index + 1}/${suites.length}]: ${suite.id}...`); } else { console.log(`Running e2e suite: ${suite.id}...`); } await runCommand( "pnpm", [ "--filter", "server", "exec", "vitest", "run", "--config", "vitest.config.e2e.ts", suite.vitestTarget, "--reporter=default", "--reporter=junit", `--outputFile.junit=${junitOutput}`, ...vitestArgs, ], runEnvironment, ); } } void main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });