From 7b5730910ea2ee5146f7fb121dded10cf61c8f4e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 20 Aug 2026 14:55:20 -0400 Subject: [PATCH] Write the oversized request with backpressure, so the refusal can be read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon fix landed but the test still failed on macOS and Windows, and the remaining fault was the client. It blasted a megabyte in a single write, so the moment the daemon capped the line and hung up, the write raised EPIPE — and Node destroys a socket on error, discarding the refusal that was already inbound. Ignoring the error was not enough: by then the socket was gone. It now writes in 64KB chunks and honours the return value of `write`, waiting for `drain` rather than pushing into a full buffer, and stops as soon as a reply arrives. That is what a real client does, and it leaves room for `data` to fire between chunks. Worth stating plainly: the daemon was right the whole way through this. It capped the line and refused, on every platform. Two layers of test artefact — treating a client-side EPIPE as fatal, then writing without backpressure — made correct behaviour look broken on the two platforms nobody had run. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN --- js/packages/cli/test/agentSecurity.test.ts | 32 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/js/packages/cli/test/agentSecurity.test.ts b/js/packages/cli/test/agentSecurity.test.ts index f7218fae..a53b4ab7 100644 --- a/js/packages/cli/test/agentSecurity.test.ts +++ b/js/packages/cli/test/agentSecurity.test.ts @@ -325,18 +325,41 @@ describe("second-audit findings", () => { const reply = await new Promise>((resolve, reject) => { const socket = createConnection(socketPath); let buf = ""; + // Stops the writer as soon as the daemon has answered. + let settled = false; const timer = setTimeout(() => { socket.destroy(); reject(new Error("timeout")); }, 10000); socket.on("connect", () => { - // No newline: without a cap this grows unbounded and is rescanned. - socket.write("x".repeat(1_000_000)); + // Written in chunks that respect backpressure, not as one megabyte. + // Blasting it meant the write hit EPIPE the moment the daemon hung + // up, and Node destroys a socket on error — discarding the refusal + // that was already inbound. The client never saw WHY it was refused, + // on macOS and Windows both. A real client writes with backpressure, + // and doing the same here lets `data` fire between chunks. + // No newline anywhere: without a cap this grows unbounded and is + // rescanned on every chunk, which is the thing being tested. + const TOTAL = 1_000_000; + const CHUNK = 64 * 1024; + let sent = 0; + const pump = (): void => { + while (sent < TOTAL && !socket.destroyed && !settled) { + const room = socket.write("x".repeat(Math.min(CHUNK, TOTAL - sent))); + sent += CHUNK; + if (!room) { + socket.once("drain", pump); + return; + } + } + }; + pump(); }); socket.on("data", (c) => { buf += c.toString(); const nl = buf.indexOf("\n"); if (nl >= 0) { + settled = true; clearTimeout(timer); socket.destroy(); resolve(JSON.parse(buf.slice(0, nl)) as Record); @@ -356,8 +379,9 @@ describe("second-audit findings", () => { reject(e); }); socket.on("close", () => { - // Only reaches here if the reply never arrived: `data` resolves and - // destroys the socket as soon as it sees a newline. + // Only reaches here if the reply never arrived: `data` sets settled + // and resolves as soon as it sees a newline. + if (settled) return; clearTimeout(timer); reject(new Error("the daemon closed the connection without replying")); });