diff --git a/tests/e2e/run.mjs b/tests/e2e/run.mjs index 9d76dec..9cfc88e 100644 --- a/tests/e2e/run.mjs +++ b/tests/e2e/run.mjs @@ -6,7 +6,7 @@ * * 运行:node tests/e2e/run.mjs */ -import { spawn } from 'node:child_process'; +import { spawn as rawSpawn } from 'node:child_process'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { createRequire } from 'node:module'; @@ -25,6 +25,34 @@ const RawWebSocket = requireFromClient('ws'); const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '../..'); +/** + * 子进程登记表 + 兜底清理。 + * + * 为什么需要:本套件有 8 处 spawn(主 agent / TLS / ed25519 / hub / fs / monitor…), + * 各自的 finally 只做 `child.kill()`(SIGTERM),随后立刻 process.exit —— 被控端 + * 可能还没来得及退出就被父进程带走,**下一轮连跑时固定端口仍被占用**,表现为 + * 「连跑第二次偶发失败」(历史真机现象,重跑又绿,最难查)。 + * + * 做法:包一层 spawn,把所有子进程登记下来;进程退出时统一 SIGKILL。 + * 调用点无需改动(同名函数遮蔽),单点收口也不会漏掉将来新增的 spawn。 + */ +const liveChildren = new Set(); +function spawn(...args) { + const child = rawSpawn(...args); + liveChildren.add(child); + child.once('exit', () => liveChildren.delete(child)); + return child; +} +process.on('exit', () => { + for (const child of liveChildren) { + try { + child.kill('SIGKILL'); + } catch { + /* 已退出或不可杀:忽略 */ + } + } +}); + const PORT = 18765; const TLS_PORT = 18772; const ED25519_PORT = 18781;