test: skip the leak probe where the OS rejects the trigger, instead of failing CI

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 <hermes-agent@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8
This commit is contained in:
2026-07-27 07:14:41 +10:00
co-authored by Hermes Agent Claude Opus 5
parent 718f1f4196
commit f84d4c676b
+27 -5
View File
@@ -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));