From 16bbead5a47797cd5823695e6edf5044247c7b4a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 29 Jul 2026 18:11:47 +0200 Subject: [PATCH] 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. --- .../agent/providers/jsonl-rpc-process.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts b/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts index 26e33043f..6d5928ac6 100644 --- a/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts +++ b/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts @@ -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 { describe, expect, test } from "vitest"; @@ -42,7 +45,6 @@ readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on("line return; } if (command.type === "hang") { - process.stderr.write("still waiting"); return; } 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({ launch: { command: process.execPath, @@ -61,6 +89,7 @@ function startProcess(): JsonlRpcProcess { env: { JSONL_RPC_TEST_VALUE: "resolved-env" }, }, logger: pino({ level: "silent" }), + ...(child ? { spawn: () => child } : {}), }); } @@ -127,10 +156,11 @@ describe("JsonlRpcProcess", () => { }); test("includes buffered stderr when a request times out", async () => { - const transport = startProcess(); + const child = createInMemoryChildProcess(); + const transport = startProcess({ child }); try { - await transport.request({ type: "echo", value: "ready" }); + child.stderr.write("still waiting"); await expect(transport.request({ type: "hang" }, 50)).rejects.toThrow( "JSONL RPC request timed out for hang\nstill waiting",