'use strict'; /** * Tests for tests/helpers/process-seam.cjs (the spawnSync-based subprocess * seam) and its runMsdTools adapter in tests/helpers.cjs. * * Contract: .msd/phase/test-3055-process-seam-module/40-design.md * Matrix: .msd/phase/test-3055-process-seam-module/50-test-matrix.md * * Rows 1-24 exercise the seam directly. Rows 25-35 exercise the * `runMsdTools` adapter's contract-parity guarantee for its 136 callers. * * Assertions are on typed fields only (outcome/exitCode/timedOut/signal/ * killed/code) or on structured JSON a fixture prints to stdout — never on * raw stdout/stderr text via .includes()/assert.match(), per CONTRIBUTING * "Prohibited: Raw Text Matching on Test Outputs". */ const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { createTempDir, cleanup, runMsdTools, TOOLS_PATH } = require('./helpers.cjs'); const processSeam = require('./helpers/process-seam.cjs'); const { runNode, runGit, runHook, OUTCOME, toSeamResult } = processSeam; const { SEAM_DEFAULT_TIMEOUT_MS, } = require('./helpers/timeouts.cjs'); /** * `FIXTURE_SLEEPER` in this file sleeps 5000ms by default. Both constants * below are comfortably under that, chosen only to reliably fire TIMED_OUT * with margin — the exact numbers are not independently meaningful beyond * "far under 5000ms", and are kept as two distinct pre-existing values (not * consolidated to one) so this migration does not silently change any * test's timing behavior. `SEAM_TIGHT_TIMEOUT_MS` (200) is the SMALLER / * tighter of the two; `SEAM_SHORT_TIMEOUT_MS` (300) is the LARGER of the * two — the names alone don't convey that ordering, hence spelling it out * here. */ const SEAM_TIGHT_TIMEOUT_MS = 200; const SEAM_SHORT_TIMEOUT_MS = 300; /** * Generous headroom for a fixture in this file that exits quickly or * synchronously (not exercising the timeout boundary itself). */ const SEAM_GENEROUS_TIMEOUT_MS = 5000; // ---- fixture sources ------------------------------------------------- // argv: [exitCode?, stdoutPayload?, stderrPayload?] const FIXTURE_EXIT = [ "const code = Number(process.argv[2] || '0');", 'const stdoutPayload = process.argv[3];', 'const stderrPayload = process.argv[4];', "if (stdoutPayload) console.log(stdoutPayload);", "if (stderrPayload) console.error(stderrPayload);", 'process.exitCode = code;', ].join('\n'); // argv: [sleepMs, stdoutMarker?, stderrMarker?] — writes markers via a // synchronous fd write (never buffered console.log) so partial output // survives a kill even when the child never reaches a clean exit. const FIXTURE_SLEEPER = [ "const fs = require('fs');", "const sleepMs = Number(process.argv[2] || '0');", 'const stdoutMarker = process.argv[3];', 'const stderrMarker = process.argv[4];', "if (stdoutMarker) fs.writeSync(1, stdoutMarker + '\\n');", "if (stderrMarker) fs.writeSync(2, stderrMarker + '\\n');", 'setTimeout(() => {}, sleepMs);', ].join('\n'); // Echoes received argv back as JSON — proves argv arrives literal/unmodified. const FIXTURE_ECHO_ARGV = [ 'console.log(JSON.stringify({ argv: process.argv.slice(2) }));', ].join('\n'); // Echoes stdin back as JSON. const FIXTURE_ECHO_STDIN = [ "let data = '';", "process.stdin.on('data', (chunk) => { data += chunk; });", "process.stdin.on('end', () => {", ' console.log(JSON.stringify({ received: data, hadData: data.length > 0 }));', '});', ].join('\n'); // Kills its own process with SIGKILL — simulates an external kill (OOM // killer, `process.kill(pid, 'SIGKILL')` from outside) that spawnSync does // NOT populate `result.error` for on this runtime. const FIXTURE_SUICIDE = [ "process.kill(process.pid, 'SIGKILL');", 'setTimeout(() => {}, 5000);', ].join('\n'); // argv: [exitCode?] — writes well past the 1MB default maxBuffer, then // attempts a clean exit with exitCode (which the overflow kill preempts). const FIXTURE_OVERFLOW = [ "const exitCode = Number(process.argv[2] || '0');", 'process.exitCode = exitCode;', 'for (let i = 0; i < 300; i += 1) {', " process.stdout.write('x'.repeat(10000));", '}', ].join('\n'); const BUFFER_OVERFLOW_CODES = ['ENOBUFS', 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER']; function writeFixture(dir, name, source) { const fixturePath = path.join(dir, name); fs.writeFileSync(fixturePath, source); return fixturePath; } /** * Monkeypatch processSeam.runNode for the duration of `block`, restoring the * original in `finally`. Standalone helper (no test-context access), per * CONTRIBUTING's try/finally carve-out and the CLAUDE.md IO-fault-injection * pattern (save original, override, restore in finally). */ function withMockedRunNode(impl, block) { const original = processSeam.runNode; let callCount = 0; processSeam.runNode = (...args) => { callCount += 1; return impl(callCount, ...args); }; try { block(() => callCount); } finally { processSeam.runNode = original; } } describe('process-seam', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir('process-seam-'); }); afterEach(() => { cleanup(tmpDir); }); test('exit 0 reports EXITED with the child stdout', () => { const fixture = writeFixture(tmpDir, 'exit.cjs', FIXTURE_EXIT); const result = runNode([fixture, '0', JSON.stringify({ ok: true })]); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.exitCode, 0); assert.equal(result.timedOut, false); assert.equal(result.signal, null); assert.equal(result.killed, false); assert.equal(result.code, null); assert.equal(typeof result.stdout, 'string'); assert.deepStrictEqual(JSON.parse(result.stdout.trim()), { ok: true }); }); test('non-zero exit is EXITED, not a failure-to-run', () => { const fixture = writeFixture(tmpDir, 'exit.cjs', FIXTURE_EXIT); const result = runNode([fixture, '5', '', 'boom']); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.exitCode, 5); assert.equal(result.timedOut, false); assert.ok(result.stderr.length > 0); }); test('empty stderr with non-zero exit keeps exitCode', () => { const fixture = writeFixture(tmpDir, 'exit.cjs', FIXTURE_EXIT); const result = runNode([fixture, '9']); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.exitCode, 9); assert.equal(result.stderr, ''); assert.notEqual(result.code, undefined); assert.equal(result.code, null); }); test('a child that overruns is TIMED_OUT as data, not a throw', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '5000'], { timeoutMs: SEAM_SHORT_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.TIMED_OUT); assert.equal(result.timedOut, true); assert.equal(result.killed, true); assert.equal(result.exitCode, null); }); test('timedOut does not depend on signal presence (Windows)', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '5000'], { timeoutMs: SEAM_SHORT_TIMEOUT_MS }); // The assertion below is intentionally the whole point of this test: it // proves timedOut alone, without ever branching on result.signal. See // 40-design.md row 6 — signal is null on Windows and must not be // load-bearing for this flag. assert.equal(result.timedOut, true); }); test('a timeout still returns string stdout/stderr (partial content is platform-dependent)', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const marker = JSON.stringify({ partial: true }); const result = runNode([fixture, '5000', marker], { timeoutMs: SEAM_SHORT_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.TIMED_OUT); assert.equal(result.timedOut, true); assert.equal(typeof result.stdout, 'string'); assert.equal(typeof result.stderr, 'string'); // spawnSync preserves partial child output on a timeout on darwin, but discards // it on Linux (verified on node 22 and 24). The seam passes through whatever // spawnSync gives it, so the cross-platform contract is only that these are // strings — the partial content itself is asserted where it is actually available. if (process.platform === 'darwin') { assert.ok(result.stdout.length > 0, 'darwin preserves partial stdout on timeout'); assert.deepStrictEqual(JSON.parse(result.stdout.trim()), { partial: true }); } }); test('maxBuffer overflow is not misreported as exit 1', () => { const fixture = writeFixture(tmpDir, 'overflow.cjs', FIXTURE_OVERFLOW); const result = runNode([fixture, '0']); assert.equal(result.outcome, OUTCOME.BUFFER_OVERFLOW); assert.notEqual(result.exitCode, 1); assert.equal(result.exitCode, null); assert.ok(BUFFER_OVERFLOW_CODES.includes(result.code)); }); test('a missing binary is SPAWN_FAILED, not a timeout', () => { // git exists on the test host; a nonexistent cwd makes the OS-level // spawn itself fail with ENOENT (uv_spawn), the same failure class as a // missing binary, without requiring the seam to expose a raw command // parameter callers could point at an arbitrary executable name. const result = runGit(['status'], { cwd: path.join(tmpDir, 'does-not-exist') }); assert.equal(result.outcome, OUTCOME.SPAWN_FAILED); assert.equal(result.code, 'ENOENT'); assert.equal(result.exitCode, null); assert.equal(result.timedOut, false); }); // Regression for the Windows CI failure on PR #3066: a `status === null` // catch-all previously misclassified this as TIMED_OUT (the process never // even started), which drove the adapter into a pointless retry. An // oversized argv errors at the OS spawn boundary before the child exists // at all — E2BIG on Linux/macOS, ENAMETOOLONG on Windows — and must // classify as SPAWN_FAILED regardless of which errno the platform uses. test('an oversized argv is SPAWN_FAILED, not TIMED_OUT, cross-platform', () => { const fixture = writeFixture(tmpDir, 'echo-argv.cjs', FIXTURE_ECHO_ARGV); const oversizedArg = 'x'.repeat(4 * 1024 * 1024); const result = runNode([fixture, oversizedArg]); assert.equal(result.outcome, OUTCOME.SPAWN_FAILED); assert.equal(result.timedOut, false); assert.equal(typeof result.code, 'string'); assert.ok(result.code.length > 0); }); test('omitting timeoutMs still bounds the call', () => { const fixture = writeFixture(tmpDir, 'exit.cjs', FIXTURE_EXIT); const withDefault = runNode([fixture, '0']); const withExplicitDefault = runNode([fixture, '0'], { timeoutMs: SEAM_DEFAULT_TIMEOUT_MS }); // Omitting timeoutMs must resolve to the same bounded code path as // explicitly passing the documented default — never a distinct // "unbounded" branch. assert.deepStrictEqual( { outcome: withDefault.outcome, exitCode: withDefault.exitCode, timedOut: withDefault.timedOut }, { outcome: withExplicitDefault.outcome, exitCode: withExplicitDefault.exitCode, timedOut: withExplicitDefault.timedOut } ); }); test('child finishing just under the bound is EXITED', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '50'], { timeoutMs: SEAM_GENEROUS_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.timedOut, false); }); test('at-the-bound child yields one deterministic outcome', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '300'], { timeoutMs: SEAM_SHORT_TIMEOUT_MS }); // Either outcome is acceptable at the exact bound (OS/scheduler // jitter decides which side of the race wins) — what must never happen // is an outcome outside the pair, or fields inconsistent with whichever // branch fired. This is the non-flaky formulation of "at the limit". assert.ok([OUTCOME.EXITED, OUTCOME.TIMED_OUT].includes(result.outcome)); if (result.outcome === OUTCOME.EXITED) { assert.equal(result.timedOut, false); assert.notEqual(result.exitCode, null); } else { assert.equal(result.timedOut, true); assert.equal(result.exitCode, null); } }); test('toSeamResult classifies a raced status+ETIMEDOUT as EXITED, not TIMED_OUT', () => { // Synthetic reproduction of the exact-bound race: spawnSync's timer // fired (error.code === 'ETIMEDOUT') just as the child finished on its // own (status: 0). Evidence (a real status) must outrank the attached // error — the old discrimination order checked error.code first and // reported TIMED_OUT with exitCode: 0, an incoherent shape. const result = toSeamResult({ status: 0, error: { code: 'ETIMEDOUT' }, signal: null, stdout: '', stderr: '', }); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.timedOut, false); assert.equal(result.exitCode, 0); }); test('child overrunning the bound is TIMED_OUT', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '5000'], { timeoutMs: SEAM_TIGHT_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.TIMED_OUT); }); test('invalid timeoutMs is rejected, not coerced to unbounded', () => { const fixture = writeFixture(tmpDir, 'exit.cjs', FIXTURE_EXIT); for (const invalid of [0, -5, NaN, 'abc', Infinity]) { assert.throws(() => runNode([fixture, '0'], { timeoutMs: invalid }), TypeError); } }); test('input is delivered to the child on stdin', () => { const fixture = writeFixture(tmpDir, 'echo-stdin.cjs', FIXTURE_ECHO_STDIN); const result = runNode([fixture], { input: 'hello-stdin' }); assert.equal(result.outcome, OUTCOME.EXITED); assert.deepStrictEqual(JSON.parse(result.stdout.trim()), { received: 'hello-stdin', hadData: true, }); }); test('omitted input does not close stdin as empty string', () => { const fixture = writeFixture(tmpDir, 'echo-stdin.cjs', FIXTURE_ECHO_STDIN); const result = runNode([fixture]); assert.equal(result.outcome, OUTCOME.EXITED); assert.deepStrictEqual(JSON.parse(result.stdout.trim()), { received: '', hadData: false, }); }); test('caller cannot override encoding into Buffers', () => { const fixture = writeFixture(tmpDir, 'exit.cjs', FIXTURE_EXIT); const result = runNode([fixture, '0', 'marker'], { encoding: 'buffer' }); assert.equal(typeof result.stdout, 'string'); assert.equal(Buffer.isBuffer(result.stdout), false); }); test('argv metacharacters are not interpreted by a shell', () => { const fixture = writeFixture(tmpDir, 'echo-argv.cjs', FIXTURE_ECHO_ARGV); const hostileArgv = [';', '&&', '$(ls)', '`ls`', '| cat', '> /tmp/x']; const result = runNode([fixture, ...hostileArgv]); assert.equal(result.outcome, OUTCOME.EXITED); assert.deepStrictEqual(JSON.parse(result.stdout.trim()).argv, hostileArgv); }); test('flag-shaped argv values are not re-parsed', () => { const fixture = writeFixture(tmpDir, 'echo-argv.cjs', FIXTURE_ECHO_ARGV); const flagLikeArgv = ['--weird', '--timeoutMs=1', '-x']; const result = runNode([fixture, ...flagLikeArgv]); assert.deepStrictEqual(JSON.parse(result.stdout.trim()).argv, flagLikeArgv); }); test('long and unicode argv survive the seam', () => { const fixture = writeFixture(tmpDir, 'echo-argv.cjs', FIXTURE_ECHO_ARGV); const longArgv = ['x'.repeat(5000), '日本語テスト', '🚀emoji🚀', 'café']; const result = runNode([fixture, ...longArgv]); assert.deepStrictEqual(JSON.parse(result.stdout.trim()).argv, longArgv); }); test('a custom killSignal is reported, not normalized', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '5000'], { timeoutMs: SEAM_TIGHT_TIMEOUT_MS, killSignal: 'SIGINT' }); assert.equal(result.outcome, OUTCOME.TIMED_OUT); if (process.platform !== 'win32') { assert.equal(result.signal, 'SIGINT'); } }); test('timeout with stderr reports one outcome, keeps both fields', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const result = runNode([fixture, '5000', '', 'err-marker'], { timeoutMs: SEAM_SHORT_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.TIMED_OUT); assert.equal(typeof result.stdout, 'string'); assert.equal(typeof result.stderr, 'string'); // spawnSync preserves partial child output on a timeout on darwin, but discards // it on Linux (verified on node 22 and 24). The seam passes through whatever // spawnSync gives it, so the cross-platform contract is only that these are // strings — the partial content itself is asserted where it is actually available. if (process.platform === 'darwin') { assert.ok(result.stderr.length > 0, 'darwin preserves partial stderr on timeout'); } }); test('overflow is not masked by an exit code', () => { const fixture = writeFixture(tmpDir, 'overflow.cjs', FIXTURE_OVERFLOW); const result = runNode([fixture, '7']); assert.equal(result.outcome, OUTCOME.BUFFER_OVERFLOW); assert.equal(result.exitCode, null); }); test('consecutive timeouts do not share state', () => { const fixture = writeFixture(tmpDir, 'sleeper.cjs', FIXTURE_SLEEPER); const first = runNode([fixture, '5000'], { timeoutMs: SEAM_TIGHT_TIMEOUT_MS }); const second = runNode([fixture, '5000'], { timeoutMs: SEAM_TIGHT_TIMEOUT_MS }); assert.equal(first.outcome, OUTCOME.TIMED_OUT); assert.equal(second.outcome, OUTCOME.TIMED_OUT); }); test('OUTCOME enum keys are locked', () => { assert.equal(Object.isFrozen(OUTCOME), true); assert.deepStrictEqual(Object.keys(OUTCOME).sort(), [ 'BUFFER_OVERFLOW', 'EXITED', 'KILLED', 'SPAWN_FAILED', 'TIMED_OUT', ]); assert.deepStrictEqual(OUTCOME, { EXITED: 'exited', KILLED: 'killed', TIMED_OUT: 'timed_out', BUFFER_OVERFLOW: 'buffer_overflow', SPAWN_FAILED: 'spawn_failed', }); assert.throws(() => { OUTCOME.EXITED = 'nope'; }, TypeError); }); test('a child killed by an external signal is KILLED, not EXITED', (t) => { if (process.platform === 'win32') { t.skip('signal semantics differ on win32 — see design doc row on Windows signals'); return; } const fixture = writeFixture(tmpDir, 'suicide.cjs', FIXTURE_SUICIDE); const result = runNode([fixture], { timeoutMs: SEAM_GENEROUS_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.KILLED); assert.equal(result.killed, true); assert.equal(result.timedOut, false); assert.equal(result.signal, 'SIGKILL'); assert.equal(result.exitCode, null); }); // Smoke coverage for runHook's export (matches the invocation shape used by // tests/read-guard.test.cjs:34 and tests/workflow-guard.test.cjs:28 — // process.execPath + [HOOK_PATH, ...args] with a JSON stdin payload). test('runHook invokes a real hook via process.execPath', () => { const hookPath = path.join(__dirname, '..', 'hooks', 'msd-read-guard.js'); const payload = JSON.stringify({ tool_name: 'Read', tool_input: {} }); const env = { ...process.env, CLAUDE_SESSION_ID: '', CLAUDECODE: '', CLAUDE_CODE_ENTRYPOINT: '', CLAUDE_CODE_SSE_PORT: '', CLAUDE_PROJECT_DIR: '', }; const result = runHook(hookPath, [], { input: payload, env, timeoutMs: SEAM_GENEROUS_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(typeof result.stdout, 'string'); }); }); describe('runHook interpreter option', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir('process-seam-interpreter-'); }); afterEach(() => { cleanup(tmpDir); }); test('omitting interpreter still spawns via process.execPath', () => { const hookPath = writeFixture(tmpDir, 'hook.cjs', FIXTURE_ECHO_ARGV); const result = runHook(hookPath, ['a', 'b']); assert.equal(result.outcome, OUTCOME.EXITED); assert.deepStrictEqual(JSON.parse(result.stdout.trim()).argv, ['a', 'b']); }); // bash availability is checked, never assumed — a Windows host or a // node-only container may not have bash on PATH. function isBashAvailable() { if (process.platform === 'win32') return false; const probeResult = runHook('-c', ['exit 0'], { interpreter: 'bash' }); return probeResult.outcome !== OUTCOME.SPAWN_FAILED; } const bashAvailable = isBashAvailable(); test('interpreter: bash runs a bash script and reports EXITED', (t) => { if (!bashAvailable) { t.skip('bash is not available on this host'); return; } const scriptPath = path.join(tmpDir, 'hook.sh'); fs.writeFileSync( scriptPath, [ '#!/usr/bin/env bash', 'echo \'{"ok":true}\'', 'exit 0', ].join('\n') ); const result = runHook(scriptPath, [], { interpreter: 'bash' }); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.exitCode, 0); assert.deepStrictEqual(JSON.parse(result.stdout.trim()), { ok: true }); }); test('interpreter is not forwarded into spawnSync options', () => { const hookPath = writeFixture(tmpDir, 'hook.cjs', FIXTURE_EXIT); // If `interpreter` leaked into spawnOptions, spawnSync would receive an // unexpected string-valued option alongside a valid timeoutMs; the // seam's contract-validation for timeoutMs must still pass through // untouched and the call must complete without throwing. const result = runHook(hookPath, ['0'], { interpreter: process.execPath, timeoutMs: SEAM_GENEROUS_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.EXITED); assert.equal(result.exitCode, 0); }); test('interpreter: bash on a script past timeoutMs is TIMED_OUT', (t) => { if (!bashAvailable) { t.skip('bash is not available on this host'); return; } const scriptPath = path.join(tmpDir, 'sleeper.sh'); fs.writeFileSync( scriptPath, [ '#!/usr/bin/env bash', 'sleep 5', ].join('\n') ); const result = runHook(scriptPath, [], { interpreter: 'bash', timeoutMs: SEAM_SHORT_TIMEOUT_MS }); assert.equal(result.outcome, OUTCOME.TIMED_OUT); assert.equal(result.timedOut, true); assert.equal(result.exitCode, null); }); }); describe('runMsdTools adapter (process-seam parity)', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir('process-seam-adapter-'); }); afterEach(() => { cleanup(tmpDir); }); test('adapter returns the legacy success shape', () => { const result = runMsdTools(['--help'], tmpDir); assert.equal(result.success, true); assert.equal(result.exitCode, 0); assert.equal(typeof result.output, 'string'); }); test('adapter returns the legacy failure shape', () => { const result = runMsdTools(['this-is-not-a-real-command'], tmpDir); assert.equal(result.success, false); assert.equal(typeof result.error, 'string'); assert.ok(result.error.length > 0); assert.equal(typeof result.exitCode, 'number'); assert.notEqual(result.exitCode, 0); }); test('adapter reproduces the empty-stderr diagnostic verbatim', () => { withMockedRunNode( () => ({ outcome: OUTCOME.EXITED, exitCode: 9, stdout: '', stderr: '', timedOut: false, signal: null, killed: false, code: null, }), () => { const result = runMsdTools(['a', 'b'], tmpDir); const expected = `Command failed: ${process.execPath} ${TOOLS_PATH} a b [stderr: (empty) exit:9]`; assert.equal(result.success, false); assert.equal(result.error, expected); assert.equal(result.exitCode, 9); } ); }); test('adapter still retries once on kill', () => { withMockedRunNode( (callCount) => (callCount === 1 ? { outcome: OUTCOME.TIMED_OUT, exitCode: null, stdout: '', stderr: '', timedOut: true, signal: 'SIGTERM', killed: true, code: 'ETIMEDOUT', } : { outcome: OUTCOME.EXITED, exitCode: 0, stdout: 'ok\n', stderr: '', timedOut: false, signal: null, killed: false, code: null, }), (getCallCount) => { const result = runMsdTools(['x'], tmpDir); assert.equal(result.success, true); assert.equal(result.output, 'ok'); assert.equal(getCallCount(), 2); } ); }); test('adapter retries once on KILLED, mirroring TIMED_OUT', () => { withMockedRunNode( (callCount) => (callCount === 1 ? { outcome: OUTCOME.KILLED, exitCode: null, stdout: '', stderr: '', timedOut: false, signal: 'SIGKILL', killed: true, code: null, } : { outcome: OUTCOME.EXITED, exitCode: 0, stdout: 'ok\n', stderr: '', timedOut: false, signal: null, killed: false, code: null, }), (getCallCount) => { const result = runMsdTools(['x'], tmpDir); assert.equal(result.success, true); assert.equal(result.output, 'ok'); assert.equal(getCallCount(), 2); } ); }); test('adapter throws resource-starvation when KILLED persists after retry', () => { withMockedRunNode( () => ({ outcome: OUTCOME.KILLED, exitCode: null, stdout: 'partial-out', stderr: 'partial-err', timedOut: false, signal: 'SIGKILL', killed: true, code: null, }), (getCallCount) => { const expected = `[runMsdTools: resource-starvation / subprocess-kill after retry] ` + `msd-tools was killed before completion ` + `(signal=SIGKILL, code=null, killed=true). ` + `This indicates host OOM or scheduler contention, not a product bug. ` + `stdout=partial-out stderr=partial-err`; assert.throws( () => runMsdTools(['x'], tmpDir), (err) => err instanceof Error && err.message === expected ); assert.equal(getCallCount(), 2); } ); }); test('adapter still throws resource-starvation after retry', () => { withMockedRunNode( () => ({ outcome: OUTCOME.TIMED_OUT, exitCode: null, stdout: 'partial-out', stderr: 'partial-err', timedOut: true, signal: 'SIGTERM', killed: true, code: 'ETIMEDOUT', }), () => { const expected = `[runMsdTools: resource-starvation / subprocess-kill after retry] ` + `msd-tools was killed before completion ` + `(signal=SIGTERM, code=ETIMEDOUT, killed=true). ` + `This indicates host OOM or scheduler contention, not a product bug. ` + `stdout=partial-out stderr=partial-err`; assert.throws( () => runMsdTools(['x'], tmpDir), (err) => err instanceof Error && err.message === expected ); } ); }); test('adapter retries exactly once, not twice', () => { withMockedRunNode( () => ({ outcome: OUTCOME.TIMED_OUT, exitCode: null, stdout: '', stderr: '', timedOut: true, signal: 'SIGTERM', killed: true, code: 'ETIMEDOUT', }), (getCallCount) => { assert.throws(() => runMsdTools(['x'], tmpDir)); assert.equal(getCallCount(), 2); } ); }); test('adapter still accepts a shell-style string', () => { const result = runMsdTools('--help', tmpDir); assert.equal(result.success, true); assert.equal(result.exitCode, 0); }); test('adapter still accepts an argv array', () => { const result = runMsdTools(['--help'], tmpDir); assert.equal(result.success, true); assert.equal(result.exitCode, 0); }); test('adapter preserves env override precedence', () => { withMockedRunNode( (_callCount, _args, options) => { // Capture the merged env the adapter built, then return a fast // EXITED result — this test is about the merge, not msd-tools. withMockedRunNode.capturedEnv = options.env; return { outcome: OUTCOME.EXITED, exitCode: 0, stdout: '', stderr: '', timedOut: false, signal: null, killed: false, code: null, }; }, () => { runMsdTools(['x'], tmpDir, { MSD_SESSION_KEY: 'override-value' }); // TEST_ENV_BASE defaults MSD_SESSION_KEY to '' — the caller's env // argument must win over it. assert.equal(withMockedRunNode.capturedEnv.MSD_SESSION_KEY, 'override-value'); } ); }); // Legacy-shape contract: the SEAM reports exitCode: null for // BUFFER_OVERFLOW (asserted above, at the seam level), but a real caller // (tests/context-predicates-query.test.cjs) asserts // `typeof r.exitCode === 'number'` on the ADAPTER's legacy shape, matching // the pre-seam execFileSync helper's `err.status ?? 1`. This is the // adapter-level contract, deliberately distinct from the seam-level one. test('adapter does not retry a buffer overflow, and coerces exitCode to a number', () => { withMockedRunNode( () => ({ outcome: OUTCOME.BUFFER_OVERFLOW, exitCode: null, stdout: 'x'.repeat(20), stderr: '', timedOut: false, signal: 'SIGTERM', killed: true, code: 'ENOBUFS', }), (getCallCount) => { const result = runMsdTools(['x'], tmpDir); assert.equal(getCallCount(), 1); assert.equal(result.success, false); assert.equal(typeof result.exitCode, 'number'); assert.equal(result.exitCode, 1); } ); }); // Same legacy-shape contract as the buffer-overflow test above, for // SPAWN_FAILED (the Windows CI regression case: an oversized argv). test('adapter does not retry a spawn failure, and coerces exitCode to a number', () => { withMockedRunNode( () => ({ outcome: OUTCOME.SPAWN_FAILED, exitCode: null, stdout: '', stderr: '', timedOut: false, signal: null, killed: false, code: 'ENOENT', }), (getCallCount) => { const result = runMsdTools(['x'], tmpDir); assert.equal(getCallCount(), 1); assert.equal(result.success, false); assert.equal(typeof result.exitCode, 'number'); assert.equal(result.exitCode, 1); } ); }); }); describe('#3271: hook fan-out timeout class', () => { const { PROBE_TIMEOUT_MS: PROBE, HOOK_FANOUT_TIMEOUT_MS: HOOK_FANOUT, INSTALL_TIMEOUT_MS: INSTALL, } = require('./helpers/timeouts.cjs'); test('a hook fan-out is bounded above a bare probe and below a full install', () => { // The ordering IS the claim: a hook that shells out several times is heavier // than reading back a version string and lighter than running bin/install.js. // CI recorded a Windows timeout at exactly the probe bound (PR #3285, // windows-latest node 22 shard 2/3) while every other lane passed the same // commit — the bound was sized for the wrong class. assert.ok(PROBE < HOOK_FANOUT, `probe ${PROBE}ms must be under hook fan-out ${HOOK_FANOUT}ms`); assert.ok(HOOK_FANOUT < INSTALL, `hook fan-out ${HOOK_FANOUT}ms must be under install ${INSTALL}ms`); }); test('the fan-out bound clears the duration that actually timed out', () => { // Observed: 15040ms, censored at the 15000ms probe bound, so the real need is // unknown and above it. A bound that merely matched the observation would be // the same defect again. const OBSERVED_TIMEOUT_MS = 15040; assert.ok( HOOK_FANOUT >= OBSERVED_TIMEOUT_MS * 3, `hook fan-out ${HOOK_FANOUT}ms must clear the censored ${OBSERVED_TIMEOUT_MS}ms observation with real margin`, ); }); });