Stop Windows server CI from flaking on stderr timeouts (#2613)

* test(server): make stderr timeout coverage deterministic

The timeout assertion raced Windows child-process scheduling and stderr pipe delivery. Buffer stderr before starting the timed request so the test isolates the transport behavior it intends to verify.

* test(server): synchronize stderr timeout test in parent

Use the injected process adapter to populate stderr through the parent-side stream before starting the timeout. This removes cross-pipe ordering from the test.
This commit is contained in:
Mohamed Boudra
2026-07-29 18:11:47 +02:00
committed by GitHub
parent ee3a9b013b
commit 16bbead5a4
@@ -1,3 +1,6 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import pino from "pino"; import pino from "pino";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
@@ -42,7 +45,6 @@ readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on("line
return; return;
} }
if (command.type === "hang") { if (command.type === "hang") {
process.stderr.write("still waiting");
return; return;
} }
if (command.type === "exit") { if (command.type === "exit") {
@@ -52,7 +54,33 @@ readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on("line
}); });
`; `;
function startProcess(): JsonlRpcProcess { interface InMemoryChildProcess extends ChildProcessWithoutNullStreams {
stdin: PassThrough;
stdout: PassThrough;
stderr: PassThrough;
}
interface StartProcessOptions {
child?: ChildProcessWithoutNullStreams;
}
function createInMemoryChildProcess(): InMemoryChildProcess {
const child = Object.assign(new EventEmitter(), {
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
exitCode: null,
signalCode: null,
}) as InMemoryChildProcess;
child.kill = ((signal?: NodeJS.Signals | number) => {
queueMicrotask(() => child.emit("exit", null, signal ?? null));
return true;
}) as ChildProcessWithoutNullStreams["kill"];
return child;
}
function startProcess(options: StartProcessOptions = {}): JsonlRpcProcess {
const child = options.child;
return new JsonlRpcProcess({ return new JsonlRpcProcess({
launch: { launch: {
command: process.execPath, command: process.execPath,
@@ -61,6 +89,7 @@ function startProcess(): JsonlRpcProcess {
env: { JSONL_RPC_TEST_VALUE: "resolved-env" }, env: { JSONL_RPC_TEST_VALUE: "resolved-env" },
}, },
logger: pino({ level: "silent" }), logger: pino({ level: "silent" }),
...(child ? { spawn: () => child } : {}),
}); });
} }
@@ -127,10 +156,11 @@ describe("JsonlRpcProcess", () => {
}); });
test("includes buffered stderr when a request times out", async () => { test("includes buffered stderr when a request times out", async () => {
const transport = startProcess(); const child = createInMemoryChildProcess();
const transport = startProcess({ child });
try { try {
await transport.request({ type: "echo", value: "ready" }); child.stderr.write("still waiting");
await expect(transport.request({ type: "hang" }, 50)).rejects.toThrow( await expect(transport.request({ type: "hang" }, 50)).rejects.toThrow(
"JSONL RPC request timed out for hang\nstill waiting", "JSONL RPC request timed out for hang\nstill waiting",