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

131 lines
4.0 KiB
TypeScript

/**
* Exit Handler Tests
*
* Regression coverage for a race in WorkerdSandboxRunner.restart(): the
* exit handler used to capture `this` and unconditionally mutate
* `this.workerdProcess` / `this.healthy` whenever it fired. A late exit
* from a previously-spawned workerd could therefore null out the handle
* of a freshly-spawned workerd and mark it unhealthy.
*
* The fix is an identity guard inside `makeWorkerdExitHandler`: if
* `host.workerdProcess` no longer points at the proc the handler was
* bound to, the handler is a stale survivor from a prior workerd and
* must short-circuit.
*/
import type { ChildProcess } from "node:child_process";
import { describe, it, expect } from "vitest";
import { makeWorkerdExitHandler } from "../src/sandbox/runner.js";
import type { ExitHandlerHost } from "../src/sandbox/runner.js";
interface FakeHost extends ExitHandlerHost {
scheduleRestartCalls: number;
}
function makeHost(overrides: Partial<ExitHandlerHost> = {}): FakeHost {
const host: FakeHost = {
workerdProcess: null,
healthy: false,
shuttingDown: false,
intentionalStop: false,
scheduleRestartCalls: 0,
scheduleRestart() {
host.scheduleRestartCalls++;
},
...overrides,
};
return host;
}
// The handler only ever uses `proc` as an identity reference (===
// comparison against host.workerdProcess), so any object will do.
function fakeProc(label: string): ChildProcess {
return { __label: label } as unknown as ChildProcess;
}
describe("makeWorkerdExitHandler", () => {
it("is a no-op when host has moved on to a newer workerd", () => {
const procA = fakeProc("A");
const procB = fakeProc("B");
const host = makeHost({ workerdProcess: procB, healthy: true });
makeWorkerdExitHandler(host, procA)(1, null);
// procB is still installed and untouched -- the stale handler
// must not null it out or mark it unhealthy.
expect(host.workerdProcess).toBe(procB);
expect(host.healthy).toBe(true);
expect(host.scheduleRestartCalls).toBe(0);
});
it("nulls out the handle and schedules a restart on crash exit", () => {
const procA = fakeProc("A");
const host = makeHost({ workerdProcess: procA, healthy: true });
makeWorkerdExitHandler(host, procA)(1, null);
expect(host.workerdProcess).toBeNull();
expect(host.healthy).toBe(false);
expect(host.scheduleRestartCalls).toBe(1);
});
it("clears intentionalStop and skips restart for intentional stops", () => {
const procA = fakeProc("A");
const host = makeHost({
workerdProcess: procA,
healthy: true,
intentionalStop: true,
});
makeWorkerdExitHandler(host, procA)(0, null);
// Handle/health still get cleared (the process is gone), but
// the intentional-stop flag must be consumed and no restart
// must be scheduled -- otherwise every plugin reload would
// trigger a phantom crash-restart cycle.
expect(host.workerdProcess).toBeNull();
expect(host.healthy).toBe(false);
expect(host.intentionalStop).toBe(false);
expect(host.scheduleRestartCalls).toBe(0);
});
it("skips restart while the runner is shutting down", () => {
const procA = fakeProc("A");
const host = makeHost({
workerdProcess: procA,
healthy: true,
shuttingDown: true,
});
makeWorkerdExitHandler(host, procA)(1, "SIGTERM");
expect(host.workerdProcess).toBeNull();
expect(host.healthy).toBe(false);
expect(host.scheduleRestartCalls).toBe(0);
});
it("schedules a restart when killed by a signal even if code is null", () => {
const procA = fakeProc("A");
const host = makeHost({ workerdProcess: procA, healthy: true });
makeWorkerdExitHandler(host, procA)(null, "SIGKILL");
expect(host.workerdProcess).toBeNull();
expect(host.healthy).toBe(false);
expect(host.scheduleRestartCalls).toBe(1);
});
it("does not schedule a restart on a clean exit (code 0, no signal)", () => {
const procA = fakeProc("A");
const host = makeHost({ workerdProcess: procA, healthy: true });
makeWorkerdExitHandler(host, procA)(0, null);
expect(host.workerdProcess).toBeNull();
expect(host.healthy).toBe(false);
expect(host.scheduleRestartCalls).toBe(0);
});
});