From ff72feec960044b5b57d6efc40b5bb2848761010 Mon Sep 17 00:00:00 2001 From: nibzard Date: Thu, 30 Jul 2026 20:56:55 +0000 Subject: [PATCH] fix(tests): bound e2e dev-server teardown so afterAll can't hang CI The `.md suffix end-to-end` suite boots `next dev --turbopack` via `bunx` and tears it down with `server.kill(); await server.exited`. `kill()` only SIGTERMs the direct bunx child; the next-server/Turbopack grandchildren survive, so `.exited` never resolves and afterAll blocks until Bun's setDefaultTimeout(120000) -- failing the file and turning CI red on every run since PR #97 (the fragility was latent since the teardown landed in PR #81). PR #102 (timeout-minutes: 30) does not help: that bounds the GitHub job timeout, but the job finishes in ~3-6 min and the 120s is Bun's per-hook timeout, a different layer. Spawn the dev server detached so it leads its own process group, then tear the whole group down: SIGTERM, race `.exited` against a 3s budget, escalate to SIGKILL if it hasn't exited. The awaited exit is now bounded, so teardown can no longer hang regardless of how the child handles SIGTERM -- and the full tree is reaped, keeping the port/CPU free for the later Chromium tests. Validated: bun run typecheck, biome check, and bun test tests/e2e/llm-endpoints.test.ts (22 pass). Co-Authored-By: Claude --- tests/e2e/llm-endpoints.test.ts | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/e2e/llm-endpoints.test.ts b/tests/e2e/llm-endpoints.test.ts index ef388db9..92f2d2fc 100644 --- a/tests/e2e/llm-endpoints.test.ts +++ b/tests/e2e/llm-endpoints.test.ts @@ -94,21 +94,46 @@ async function waitForServer(): Promise { throw new Error(`Dev server did not become ready on port ${PORT}`); } +function killProcessGroup(pid: number, signal: NodeJS.Signals): void { + try { + process.kill(-pid, signal); + } catch { + try { + process.kill(pid, signal); + } catch {} + } +} + describe('.md suffix end-to-end', () => { beforeAll(async () => { server = Bun.spawn(['bunx', 'next', 'dev', '--turbopack', '-p', String(PORT)], { cwd: PROJECT_ROOT, stdout: 'ignore', stderr: 'ignore', + // detached puts the dev server at the head of its own process group, so + // teardown can signal the whole tree (next-server, Turbopack workers), + // not just the direct bunx child. + detached: true, }); await waitForServer(); }); afterAll(async () => { - // Wait for the process to fully exit so the dev server's CPU and port - // are released before later test files (Chromium renders) start. - server?.kill(); - await server?.exited; + // Tear down the whole process group so the dev server's CPU and port are + // released before later test files (Chromium renders) start. The awaited + // exit is bounded and escalates to SIGKILL, so a child that ignores or + // defers SIGTERM can never hang the hook past its timeout. + const pid = server?.pid; + if (!pid) return; + killProcessGroup(pid, 'SIGTERM'); + const stopped = await Promise.race([ + server.exited.then(() => true), + Bun.sleep(3000).then(() => false), + ]); + if (!stopped) { + killProcessGroup(pid, 'SIGKILL'); + await Promise.race([server.exited, Bun.sleep(2000)]); + } }); test('serves markdown with the index pointer at a .md-suffixed docs URL', async () => {