import { spawn, type ChildProcess } from 'node:child_process'; import { createServer } from 'node:net'; import type { TestProject } from 'vitest/node'; // Fixed test-only secret, >= 32 chars as required by nuxt-auth-utils. const SESSION_PASSWORD = 'integration-tests-only-not-for-prod-use!'; async function freePort(): Promise { return new Promise((resolve, reject) => { const server = createServer(); server.listen(0, '127.0.0.1', () => { const address = server.address(); if (address === null || typeof address === 'string') { reject(new Error('Could not determine a free port')); return; } server.close(() => resolve(address.port)); }); }); } function waitForLine(child: ChildProcess, pattern: RegExp): Promise { return new Promise((resolve, reject) => { let buffer = ''; const onData = (chunk: Buffer) => { buffer += chunk.toString(); for (const line of buffer.split('\n')) { const match = line.match(pattern); if (match) { child.stdout?.off('data', onData); resolve(match.slice(1)); return; } } }; child.stdout?.on('data', onData); child.on('exit', (code) => reject(new Error(`Child exited with code ${code} before being ready`)) ); child.on('error', reject); }); } function run(command: string, args: string[], env: NodeJS.ProcessEnv = {}) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, ...env } }); child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)) ); child.on('error', reject); }); } async function waitForHealth(baseUrl: string, timeoutMs = 60_000) { const deadline = Date.now() + timeoutMs; let lastError: unknown = new Error('server never became healthy'); while (Date.now() < deadline) { try { const res = await fetch(`${baseUrl}/api/health`); if (res.ok) return; lastError = new Error(`health responded with ${res.status}`); } catch (e) { lastError = e; } await new Promise((r) => setTimeout(r, 250)); } throw lastError; } function kill(child: ChildProcess) { return new Promise((resolve) => { if (child.exitCode !== null) return resolve(); const timer = setTimeout(() => { child.kill('SIGKILL'); }, 3_000); child.on('exit', () => { clearTimeout(timer); resolve(); }); child.kill('SIGTERM'); }); } export default async function globalSetup(project: TestProject) { // 1. Boot in-memory PGlite behind a Postgres wire protocol socket. // It runs in a child process: teardown under Bun exits with code 99 // (Emscripten), and isolating it keeps the vitest process clean. const dbHost = spawn('node', ['tests/integration/pglite-host.mjs'], { stdio: ['ignore', 'pipe', 'inherit'] }); const [databaseUrl] = await waitForLine(dbHost, /^PGLITE_READY (.+)$/); // 2. Apply all migrations against the socket (proven path from Phase 0). await run('bunx', ['prisma', 'migrate', 'deploy'], { DATABASE_URL: databaseUrl }); // 3. Build the Nuxt server once for all test files. if (process.env.SKIP_BUILD !== '1') { await run('bun', ['run', 'build']); } // 4. Spawn the built server against the WASM database. const port = await freePort(); const baseUrl = `http://127.0.0.1:${port}`; const server = spawn('node', ['.output/server/index.mjs'], { stdio: 'inherit', env: { ...process.env, NODE_ENV: 'production', PORT: String(port), DATABASE_URL: databaseUrl, NUXT_SESSION_PASSWORD: SESSION_PASSWORD } }); server.on('error', (e) => { throw e; }); try { await waitForHealth(baseUrl); } catch (e) { await kill(server); await kill(dbHost); throw e; } project.provide('baseUrl', baseUrl); project.provide('databaseUrl', databaseUrl); return async () => { await kill(server); await kill(dbHost); }; }