Files
msd-core/tests/process-seam.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

849 lines
32 KiB
JavaScript

'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`,
);
});
});