import { execFileSync, spawn } from 'node:child_process'; import { createConnection } from 'node:net'; import path from 'node:path'; import process from 'node:process'; const serverRoot = path.resolve(import.meta.dirname, '..'); const launcher = path.join(serverRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs'); const redisContainerName = `a-share-analysis-e2e-redis-${process.pid}`; let ownsRedisContainer = false; let redisPort = '6379'; const localRedisAvailable = await new Promise((resolve) => { const socket = createConnection({ host: '127.0.0.1', port: 6379 }); const finish = (available) => { socket.destroy(); resolve(available); }; socket.setTimeout(500, () => finish(false)); socket.once('connect', () => finish(true)); socket.once('error', () => finish(false)); }); if (!localRedisAvailable) { execFileSync( 'docker', [ 'run', '--rm', '--detach', '--name', redisContainerName, '--publish', '127.0.0.1::6379', 'redis:7.4.8-alpine3.21', ], { stdio: ['ignore', 'pipe', 'inherit'] }, ); ownsRedisContainer = true; redisPort = execFileSync('docker', ['port', redisContainerName, '6379/tcp'], { encoding: 'utf8', }) .trim() .split(':') .at(-1); } // 浏览器集成测试只启用 API 与禁用态 MySQL;Redis 故障会形成可验证的安全 503 响应, // 所有值均为公开测试夹具,不读取开发者的 *.env.local 或任何生产配置。 const child = spawn(process.execPath, [launcher, 'apps/api/src/main.ts'], { cwd: serverRoot, env: { PATH: process.env.PATH, APP_ENV: 'production', LOG_LEVEL: 'error', API_HOST: '127.0.0.1', API_PORT: '3000', REDIS_HOST: '127.0.0.1', REDIS_PORT: redisPort, REDIS_USERNAME: '', REDIS_PASSWORD: '', REDIS_DATABASE: '0', REDIS_TLS: 'false', MYSQL_ENABLED: 'false', MYSQL_HOST: '127.0.0.1', MYSQL_PORT: '3306', MYSQL_DATABASE: 'a_share_analysis_dev', MYSQL_USERNAME: '', MYSQL_PASSWORD: '', MYSQL_CHARSET: 'utf8mb4', MYSQL_COLLATION: 'utf8mb4_general_ci', MYSQL_TIMEZONE: '+08:00', AI_ENABLED: 'false', AI_BASE_URL: '', AI_API_KEY: '', AI_MODEL: '', AI_TIMEOUT_MS: '30000', }, stdio: 'inherit', }); const forwardSignal = (signal) => { if (ownsRedisContainer) { try { execFileSync('docker', ['stop', redisContainerName], { stdio: 'ignore' }); } catch { // Playwright 可能已因启动失败清理容器,重复清理保持幂等。 } } if (child.exitCode === null) child.kill(signal); }; process.once('SIGINT', () => forwardSignal('SIGINT')); process.once('SIGTERM', () => forwardSignal('SIGTERM')); child.once('exit', (code, signal) => { if (ownsRedisContainer) { try { execFileSync('docker', ['stop', redisContainerName], { stdio: 'ignore' }); } catch { // 容器已由信号处理清理时无需再次报告错误。 } } if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });