// Kills every process global-setup.ts spawned and removes the throwaway containers it started. // Reads .e2e-state.json rather than relying on in-memory state from global-setup.ts, so teardown // is correct even if it's ever invoked independently — see global-setup.ts's doc comment on the // zombie-listener gotcha this is specifically guarding against. import { execSync } from "node:child_process"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { STATE_FILE, type E2eState } from "./config"; function killPid(pid: number) { try { if (process.platform === "win32") { // shell:true spawned bun via cmd.exe — killing just the parent PID leaves the real bun // process running; /T kills the whole tree. execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" }); } else { process.kill(-pid, "SIGKILL"); // negative pid: whole process group (spawned with shell) } } catch { // already dead — fine } } export default async function globalTeardown() { if (!existsSync(STATE_FILE)) return; const state = JSON.parse(readFileSync(STATE_FILE, "utf-8")) as E2eState; for (const pid of state.pids) killPid(pid); for (const container of state.containers) { try { execSync(`docker rm -f ${container}`, { stdio: "ignore" }); } catch { // fine } } rmSync(STATE_FILE, { force: true }); console.log("[global-teardown] cleaned up"); }