test(tailscale): await worker readiness events (#125396)

* test(tailscale): await worker readiness events

Replace readiness polling in the shared worker fixture with an opt-in IPC waiter that settles on startup failure, exit, or test cancellation. Preserve compiled worker execution, immediate pre-readiness IPC loss, and existing worker exit and foreground-claim cleanup assertions.

* test(tailscale): distinguish cancellation and exit signals

Name the test-owned AbortSignal binding separately from the OS signal returned by child exit events so the readiness fixture passes the no-shadow lint rule.

* test(tailscale): retain failure status in readiness errors

Keep the foreground exit code or terminating signal in readiness errors, including silent failures, while retaining diagnostic output.
This commit is contained in:
Peter Steinberger
2026-09-26 08:54:39 -07:00
committed by GitHub
parent 2864f56579
commit 71ca294e23
+50 -13
View File
@@ -10,7 +10,8 @@ import {
} from "./tailscale-route-owner-protocol.js";
import { runTailscaleRouteOwner } from "./tailscale-route-owner.worker.js";
function spawnRouteOwnerFixture() {
function spawnRouteOwnerFixture(waitForReady: boolean, abortSignal: AbortSignal) {
abortSignal.throwIfAborted();
const workerUrl = resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.tailscaleRouteOwner);
const workerPath = fileURLToPath(workerUrl);
const fixturePath = fileURLToPath(
@@ -29,7 +30,47 @@ function spawnRouteOwnerFixture() {
);
const messages: TailscaleRouteOwnerMessage[] = [];
worker.on("message", (message: TailscaleRouteOwnerMessage) => messages.push(message));
return { messages, worker };
const ready = waitForReady
? new Promise<void>((resolve, reject) => {
const cleanup = () => {
worker.off("message", onMessage);
worker.off("error", onError);
worker.off("exit", onExit);
abortSignal.removeEventListener("abort", onAbort);
};
const onMessage = (message: TailscaleRouteOwnerMessage) => {
if (message.type === "ready") {
cleanup();
resolve();
} else if (message.type === "failed") {
const exitStatus = message.signal
? `signal ${message.signal}`
: `code ${message.code ?? "unknown"}`;
const output = message.stderr || message.stdout;
onError(new Error(`route owner failed (${exitStatus})${output ? `: ${output}` : ""}`));
}
};
const onError = (error: Error) => {
cleanup();
reject(error);
};
const onAbort = () => {
onError(new Error("route owner readiness canceled", { cause: abortSignal.reason }));
};
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
onError(
new Error(
`route owner exited before readiness (${signal ? `signal ${signal}` : `code ${code ?? "unknown"}`})`,
),
);
};
worker.on("message", onMessage);
worker.once("error", onError);
worker.once("exit", onExit);
abortSignal.addEventListener("abort", onAbort, { once: true });
})
: undefined;
return { messages, ready, worker };
}
describe("Tailscale route owner", () => {
@@ -70,15 +111,13 @@ describe("Tailscale route owner", () => {
);
});
it.runIf(process.platform !== "win32").each([false, true])(
it.runIf(process.platform !== "win32").for([false, true])(
"terminates the claim when the Gateway IPC owner disappears (ready=%s)",
async (waitForReady) => {
const { messages, worker } = spawnRouteOwnerFixture();
async (waitForReady, { signal: abortSignal }) => {
const { ready, worker } = spawnRouteOwnerFixture(waitForReady, abortSignal);
try {
if (waitForReady) {
await vi.waitFor(() => {
expect(messages).toContainEqual({ type: "ready" });
});
await ready;
}
const exit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve) => {
@@ -98,13 +137,11 @@ describe("Tailscale route owner", () => {
it.runIf(process.platform !== "win32")(
"terminates the claim before exiting on an interactive interrupt",
async () => {
const { messages, worker } = spawnRouteOwnerFixture();
async ({ signal: abortSignal }) => {
const { messages, ready, worker } = spawnRouteOwnerFixture(true, abortSignal);
let routePid: number | undefined;
try {
await vi.waitFor(() => {
expect(messages).toContainEqual({ type: "ready" });
});
await ready;
const spawned = messages.find((message) => message.type === "spawned");
if (!spawned) {
throw new Error("route owner did not report its claim process");