import { spawn } from 'node:child_process'; import { createConnection } from 'node:net'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; const serverRoot = path.resolve(import.meta.dirname, '..'); const launcher = path.join(serverRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs'); /** * E2E 只检查开发者已维护的本机 Redis,不安装、启动或停止任何外部服务。 * 失败信息固定指向项目要求的公开本地端点,避免泄漏可注入的测试地址。 */ export function checkLocalRedis({ host = '127.0.0.1', port = 6379, timeoutMs = 500 } = {}) { return new Promise((resolve, reject) => { const socket = createConnection({ host, port }); let settled = false; const finish = (error) => { if (settled) return; settled = true; socket.destroy(); if (error) reject(error); else resolve(); }; const unavailable = () => finish(new Error('本机 Redis 127.0.0.1:6379 不可达,请确认外部 Redis 服务启动后重试')); socket.setTimeout(timeoutMs, unavailable); socket.once('connect', () => finish()); socket.once('error', unavailable); }); } export async function runTestApi() { await checkLocalRedis(); // 浏览器集成测试只启用 API 与 Redis,其他依赖使用公开禁用态夹具;这里不会读取 // 开发者的 *.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', // API 配置要求始终声明本机 TDX 边界;E2E 不依赖 TDX 可达性,页面会验证不可用状态。 TDX_BASE_URL: 'http://127.0.0.1:17710', TDX_TIMEOUT_MS: '5000', REDIS_HOST: '127.0.0.1', REDIS_PORT: '6379', 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 (child.exitCode === null) child.kill(signal); }; process.once('SIGINT', () => forwardSignal('SIGINT')); process.once('SIGTERM', () => forwardSignal('SIGTERM')); child.once('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); }); } if ( process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)) ) { await runTestApi(); }