import { spawn, ChildProcess } from 'child_process'; import { join } from 'path'; export interface WebsiteServerHarnessOptions { port?: number; env?: Record; cwd?: string; } export class WebsiteServerHarness { private process: ChildProcess | null = null; private logs: string[] = []; private port: number; constructor(options: WebsiteServerHarnessOptions = {}) { this.port = options.port || 3000; } async start(): Promise { return new Promise((resolve, reject) => { const cwd = join(process.cwd(), 'apps/website'); // Use 'npm run dev' or 'npm run start' depending on environment // For integration tests, 'dev' is often easier if we don't want to build first, // but 'start' is more realistic for SSR. // Assuming 'npm run dev' for now as it's faster for local tests. this.process = spawn('npm', ['run', 'dev', '--', '-p', this.port.toString()], { cwd, env: { ...process.env, PORT: this.port.toString(), ...((this.process as unknown as { env: Record })?.env || {}), }, shell: true, }); this.process.stdout?.on('data', (data) => { const str = data.toString(); this.logs.push(str); if (str.includes('ready') || str.includes('started') || str.includes('Local:')) { resolve(); } }); this.process.stderr?.on('data', (data) => { const str = data.toString(); this.logs.push(str); console.error(`[Website Server Error] ${str}`); }); this.process.on('error', (err) => { reject(err); }); this.process.on('exit', (code) => { if (code !== 0 && code !== null) { console.error(`Website server exited with code ${code}`); } }); // Timeout after 30 seconds setTimeout(() => { reject(new Error('Website server failed to start within 30s')); }, 30000); }); } async stop(): Promise { if (this.process) { this.process.kill(); this.process = null; } } getLogs(): string[] { return this.logs; } getLogTail(lines: number = 60): string { return this.logs.slice(-lines).join(''); } hasErrorPatterns(): boolean { const errorPatterns = [ 'uncaughtException', 'unhandledRejection', 'Error: ', ]; return this.logs.some(log => errorPatterns.some(pattern => log.includes(pattern))); } }