From f84d4c676b3f9d4048b88f6b5f563771471b8ff9 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Mon, 27 Jul 2026 07:14:41 +1000 Subject: [PATCH] test: skip the leak probe where the OS rejects the trigger, instead of failing CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regression test I added broke CI with `spawn E2BIG`, and the reason matters more than the fix: the env-only trigger is NOT portable, and the review that proposed it was measured only on darwin (the reviewer said so). Delivering the trigger needs a single env var of ~2n bytes where n is the spread-throw threshold (~130k elements here). Linux caps ONE env string at MAX_ARG_STRLEN = 32 * PAGE_SIZE = 131072 bytes, so execve rejects it outright. Encoding tricks do not rescue it: empty items cost 1 byte each, but 120k empty items do not reach the throw threshold, and the threshold itself moves with stack size — the margin does not exist. buildCliArgs has exactly one spread (verified), so there is no alternative env-only trigger. So the test now runs where the OS allows the trigger (macOS/BSD) and SKIPS LOUDLY where it does not, printing the platform and byte count. It never passes vacuously: it already asserts the requests actually return 500 before trusting the counter, and E2BIG is detected both as a synchronous throw and as an async 'error' event. Consequence, stated plainly rather than buried: CI runs on Linux, so CI does NOT exercise this test. The fix is protected on macOS dev machines only. That is weaker than I would like and is why #197 (portable in-process fault injection) matters — it is the difference between this class of bug being guarded and merely having been checked once by hand. Verified after the change: macOS, fixed -> test RUNS and passes, 450/0 macOS, bug restored -> test goes RED ("got 3"), 449/1 Linux CI -> skips with a printed reason Co-Authored-By: Hermes Agent Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8 --- test-features.mjs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/test-features.mjs b/test-features.mjs index f455766..fec94a7 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -1139,12 +1139,34 @@ test("integration: a synchronous pre-spawn throw must not leak stats.activeReque const n = ltSpreadThrowCount(); assert.ok(n, "could not find an element count whose spread throws — adjust ltSpreadThrowCount"); const dir = ltMkdir(); const fake = ltFake(dir); - const { child, buf } = ltBoot({ - CLAUDE_BIN: fake, CLAUDE_PROXY_PORT: "39370", - CLAUDE_ALLOWED_TOOLS: Array(n).fill("x").join(","), - }, dir); + // PORTABILITY: delivering the trigger needs an env var of ~2n bytes. Linux caps a SINGLE + // env string at MAX_ARG_STRLEN (32 * PAGE_SIZE = 131072), and n is ~130k here, so execve + // rejects it with E2BIG — this test cannot run on Linux, which includes CI. It runs on + // macOS/BSD. Skipping loudly rather than silently: a vacuous pass would be worse than no + // test, and the fix it guards is genuinely unprotected on CI (tracked in #197). + let child, buf; try { - assert.ok(await ltWait(() => buf.out.includes("listening on"), 20000), `did not start: ${buf.err.slice(0, 200)}`); + ({ child, buf } = ltBoot({ + CLAUDE_BIN: fake, CLAUDE_PROXY_PORT: "39370", + CLAUDE_ALLOWED_TOOLS: Array(n).fill("x").join(","), + }, dir)); + } catch (e) { + if (e && (e.code === "E2BIG" || /E2BIG/.test(String(e.message)))) { + console.log(` (SKIPPED on ${process.platform}: env of ${2 * n}B exceeds this OS's per-string execve limit — see #197)`); + _ltRm(dir, { recursive: true, force: true }); + return; + } + throw e; + } + let spawnErr = null; + child.on("error", e => { spawnErr = e; }); // E2BIG can arrive here instead of throwing + try { + const up = await ltWait(() => buf.out.includes("listening on") || spawnErr, 20000); + if (spawnErr && (spawnErr.code === "E2BIG" || /E2BIG/.test(String(spawnErr.message)))) { + console.log(` (SKIPPED on ${process.platform}: env of ${2 * n}B exceeds this OS's per-string execve limit — see #197)`); + return; + } + assert.ok(up && !spawnErr, `did not start: ${spawnErr ? spawnErr.message : buf.err.slice(0, 200)}`); const req = { model: "haiku", messages: [{ role: "user", content: "leak-probe" }] }; const codes = []; for (let i = 0; i < 3; i++) codes.push(await ltPostStatus(39370, req));