import { spawn, type ChildProcess } from 'node:child_process'; import { once } from 'node:events'; import { createServer } from 'node:net'; import { join } from 'node:path'; export interface TestServerHandle { port: number; url: string; stop: () => Promise; } const E2E_DIR = join(__dirname, '..'); const START_SERVER_SCRIPT = join(E2E_DIR, 'helpers', 'start-test-server.js'); export async function startTestServer(retries = 3): Promise { for (let attempt = 1; attempt <= retries; attempt++) { const port = await allocatePort(); const child = spawn(process.execPath, [START_SERVER_SCRIPT], { cwd: E2E_DIR, env: { ...process.env, TEST_SERVER_PORT: String(port) }, stdio: 'pipe' }); child.stdout?.on('data', (chunk: Buffer | string) => { process.stdout.write(chunk.toString()); }); child.stderr?.on('data', (chunk: Buffer | string) => { process.stderr.write(chunk.toString()); }); try { await waitForServerReady(port, child); } catch (error) { await stopServer(child); if (attempt < retries) { console.log(`[E2E Server] Attempt ${attempt} failed, retrying...`); continue; } throw error; } return { port, url: `http://localhost:${port}`, stop: async () => { await stopServer(child); } }; } throw new Error('startTestServer: unreachable'); } async function allocatePort(): Promise { return await new Promise((resolve, reject) => { const probe = createServer(); probe.once('error', reject); probe.listen(0, '127.0.0.1', () => { const address = probe.address(); if (!address || typeof address === 'string') { probe.close(); reject(new Error('Failed to resolve an ephemeral test server port')); return; } const { port } = address; probe.close((error) => { if (error) { reject(error); return; } resolve(port); }); }); }); } async function waitForServerReady(port: number, child: ChildProcess, timeoutMs = 30_000): Promise { const readyUrl = `http://127.0.0.1:${port}/api/servers?limit=1`; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error(`Test server exited before becoming ready (exit code ${child.exitCode})`); } try { const response = await fetch(readyUrl); if (response.ok) { return; } } catch { // Server still starting. } await wait(250); } throw new Error(`Timed out waiting for test server on port ${port}`); } async function stopServer(child: ChildProcess): Promise { if (child.exitCode !== null) { return; } child.kill('SIGTERM'); const exited = await Promise.race([once(child, 'exit').then(() => true), wait(3_000).then(() => false)]); if (!exited && child.exitCode === null) { child.kill('SIGKILL'); await once(child, 'exit'); } } function wait(durationMs: number): Promise { return new Promise((resolve) => { setTimeout(resolve, durationMs); }); }