// Shared OCaml HTTP server lifecycle for Playwright tests. // Starts sx_server.exe --http on a random port, waits for ready. // One instance serves all tests in a spec file. const { spawn } = require('child_process'); const path = require('path'); const PROJECT_ROOT = path.resolve(__dirname, '../..'); class SiteServer { constructor() { this.port = 49152 + Math.floor(Math.random() * 16000); this.proc = null; this._stderr = ''; } async start() { const serverBin = path.join(PROJECT_ROOT, 'hosts/ocaml/_build/default/bin/sx_server.exe'); this.proc = spawn(serverBin, ['--http', String(this.port)], { cwd: PROJECT_ROOT, env: { ...process.env, SX_PROJECT_DIR: PROJECT_ROOT, OCAMLRUNPARAM: 'b' }, stdio: ['ignore', 'pipe', 'pipe'], }); this.proc.stderr.on('data', chunk => { this._stderr += chunk.toString(); }); this.proc.stdout.on('data', () => {}); await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Server did not start within 30s\n' + this._stderr.slice(-500))), 30000); this.proc.stderr.on('data', () => { if (this._stderr.includes('Listening on port')) { clearTimeout(timeout); resolve(); } }); this.proc.on('error', err => { clearTimeout(timeout); reject(err); }); this.proc.on('exit', code => { clearTimeout(timeout); reject(new Error('Server exited: ' + code)); }); }); } get baseUrl() { return `http://localhost:${this.port}`; } stop() { if (this.proc && !this.proc.killed) { this.proc.kill('SIGTERM'); setTimeout(() => { if (this.proc && !this.proc.killed) this.proc.kill('SIGKILL'); }, 2000); } } } module.exports = { SiteServer, PROJECT_ROOT };