diff --git a/test/scripts/upgrade-survivor-probe-gateway.test.ts b/test/scripts/upgrade-survivor-probe-gateway.test.ts index 7f5f7d669187..024270779f1a 100644 --- a/test/scripts/upgrade-survivor-probe-gateway.test.ts +++ b/test/scripts/upgrade-survivor-probe-gateway.test.ts @@ -1,11 +1,11 @@ // Upgrade Survivor Probe Gateway tests cover upgrade survivor probe gateway script behavior. -import { spawn } from "node:child_process"; +import childProcess from "node:child_process"; import fs from "node:fs"; import { createServer as createHttpServer } from "node:http"; import { createServer as createTcpServer, type Server, type Socket } from "node:net"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createBoundedChildOutput } from "../helpers/bounded-child-output.js"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; @@ -35,7 +35,7 @@ function runProbe( nodeArgs: string[] = [], ): Promise { return new Promise((resolve) => { - const child = spawn(process.execPath, [...nodeArgs, probePath, ...args], { + const child = childProcess.spawn(process.execPath, [...nodeArgs, probePath, ...args], { env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"], }); @@ -58,7 +58,7 @@ function runProbe( clearTimeout(timer); resolve({ error, signal: null, status: null, stderr: stderr.text(), stdout: stdout.text() }); }); - child.on("exit", (status, signal) => { + child.on("close", (status, signal) => { clearTimeout(timer); resolve({ error: timedOut ? new Error(`probe timed out after ${timeout}ms`) : undefined, @@ -87,6 +87,33 @@ async function listen(server: Server): Promise { } describe("scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs", () => { + it("drains stderr after process exit before publishing the probe result", async () => { + const spawn = childProcess.spawn; + let exitObserved: + | { status: number | null; signal: NodeJS.Signals | null; stderrPaused: boolean } + | undefined; + const spawnSpy = vi + .spyOn(childProcess, "spawn") + .mockImplementationOnce((command, args, options) => { + const child = spawn(command, args, options); + child.stderr?.pause(); + child.once("exit", (status, signal) => { + exitObserved = { status, signal, stderrPaused: child.stderr?.isPaused() === true }; + child.stderr?.resume(); + }); + return child; + }); + try { + const result = await runProbe(["--timeout-ms", "1e3"]); + + expect(exitObserved).toEqual({ status: 1, signal: null, stderrPaused: true }); + expect(result).toMatchObject({ status: 1, signal: null, error: undefined }); + expect(result.stderr).toContain("invalid --timeout-ms: 1e3"); + } finally { + spawnSpy.mockRestore(); + } + }); + it("does not hard-code degraded ready allowlists into Docker survivor probes", () => { const script = fs.readFileSync(dockerSurvivorPath, "utf8"); @@ -108,7 +135,7 @@ describe("scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs", () => { "1e3", ]); - expect(timeoutResult.status).not.toBe(0); + expect(timeoutResult).toMatchObject({ status: 1, signal: null, error: undefined }); expect(timeoutResult.stderr).toContain("invalid --timeout-ms: 1e3"); const bodyLimitResult = await runProbe( @@ -119,7 +146,7 @@ describe("scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs", () => { }, ); - expect(bodyLimitResult.status).not.toBe(0); + expect(bodyLimitResult).toMatchObject({ status: 1, signal: null, error: undefined }); expect(bodyLimitResult.stderr).toContain( "invalid OPENCLAW_UPGRADE_SURVIVOR_PROBE_MAX_BODY_BYTES: 64bytes", );