Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

138 lines
4.1 KiB
TypeScript

/**
* stopWorkerd Timer Cleanup Tests
*
* Regression coverage for a hygiene bug in WorkerdSandboxRunner.stopWorkerd():
* the SIGTERM-then-SIGKILL fallback timer was never cleared when the process
* exited cleanly. The `if (!exited)` guard prevented an actual signal from
* being sent, but the timer itself still held the Node event loop alive for
* up to 5 seconds past clean termination -- delaying terminateAll() and
* process shutdown.
*
* The fix extracted the exit/timer dance into `waitForProcessExit()` and
* clears the timer inside the exit listener.
*/
import type { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { WorkerdSandboxRunner, waitForProcessExit } from "../src/sandbox/runner.js";
interface FakeProc extends EventEmitter {
exitCode: number | null;
kill: ReturnType<typeof vi.fn>;
}
function makeFakeProc(): FakeProc {
const proc = new EventEmitter() as FakeProc;
proc.exitCode = null;
proc.kill = vi.fn();
return proc;
}
describe("waitForProcessExit", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("clears the SIGKILL timer when the process exits cleanly", async () => {
const fake = makeFakeProc();
const stopPromise = waitForProcessExit(fake as unknown as ChildProcess);
// SIGTERM was queued synchronously.
expect(fake.kill).toHaveBeenCalledWith("SIGTERM");
// Process exits well before the 5s timeout.
await vi.advanceTimersByTimeAsync(100);
fake.emit("exit");
await stopPromise;
// The kill timer must have been cleared -- nothing pending in the
// fake timer queue. This is the load-bearing assertion: without
// clearTimeout(), getTimerCount() would be 1 here and the timer
// would keep the real event loop alive for ~5s past termination.
expect(vi.getTimerCount()).toBe(0);
// Jump past the original 5s mark. SIGKILL must NOT be sent.
await vi.advanceTimersByTimeAsync(5_000);
expect(fake.kill).toHaveBeenCalledTimes(1);
expect(fake.kill).not.toHaveBeenCalledWith("SIGKILL");
});
it("sends SIGKILL when the process does not exit within the timeout", async () => {
const fake = makeFakeProc();
const stopPromise = waitForProcessExit(fake as unknown as ChildProcess);
expect(fake.kill).toHaveBeenCalledWith("SIGTERM");
// 5s elapses with no exit -- timer fires SIGKILL.
await vi.advanceTimersByTimeAsync(5_000);
expect(fake.kill).toHaveBeenCalledWith("SIGKILL");
// Now exit; stopPromise resolves.
fake.emit("exit");
await stopPromise;
});
it("respects a custom timeout", async () => {
const fake = makeFakeProc();
const stopPromise = waitForProcessExit(fake as unknown as ChildProcess, 1_000);
// At 999ms no SIGKILL.
await vi.advanceTimersByTimeAsync(999);
expect(fake.kill).not.toHaveBeenCalledWith("SIGKILL");
// At 1000ms SIGKILL fires.
await vi.advanceTimersByTimeAsync(1);
expect(fake.kill).toHaveBeenCalledWith("SIGKILL");
fake.emit("exit");
await stopPromise;
});
});
describe("stopWorkerd timer cleanup", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("clears the SIGKILL timer when the process exits cleanly", async () => {
const runner = new WorkerdSandboxRunner({ db: null as any });
const fake = makeFakeProc();
(runner as any).workerdProcess = fake;
const stopPromise = (runner as any).stopWorkerd() as Promise<void>;
expect(fake.kill).toHaveBeenCalledWith("SIGTERM");
await vi.advanceTimersByTimeAsync(100);
fake.emit("exit");
await stopPromise;
expect(vi.getTimerCount()).toBe(0);
await vi.advanceTimersByTimeAsync(5_000);
expect(fake.kill).not.toHaveBeenCalledWith("SIGKILL");
});
it("fast-paths when exitCode is already set", async () => {
const runner = new WorkerdSandboxRunner({ db: null as any });
const fake = makeFakeProc();
fake.exitCode = 0;
(runner as any).workerdProcess = fake;
await (runner as any).stopWorkerd();
expect(fake.kill).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
});
});