fix(#3086): apply #2667 .cmd-shim gate to deps.spawn + surface errorCode in review lanes (#3142)

* fix(#3086): apply #2667 .cmd-shim gate to deps.spawn + surface errorCode in review lanes

deps.spawn used shell:false with a bare binary name — on Windows, npm-installed
CLIs (gemini, codex, etc.) are .cmd shims that CreateProcess cannot start,
producing ENOENT + empty stderr. The review path then wrote an empty err file
and emitted a generic 'failed or returned empty output' stub.

Two fixes:
1. deps.spawn: detect .cmd/.bat on win32 and mediate through cmd.exe /d /s /c
   (same gate as runWithTimeout #2667, same explicit argv array).
2. runSpawnLane: surface errorCode (ENOENT, ETIMEDOUT) in the err file so the
   stub explains WHY the lane produced nothing.

* chore(#3086): backfill changeset PR number 3142

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-07 07:43:14 -04:00
committed by GitHub
parent 7ab4556395
commit 4b66bf4560
4 changed files with 63 additions and 2 deletions

View File

@@ -663,3 +663,44 @@ describe('runner — orchestration', () => {
}
});
});
// #3086: spawn errors (ENOENT on Windows .cmd shims, ETIMEDOUT, etc.) must be
// surfaced in the err file so the stub reviewer output explains WHY the lane
// produced nothing, rather than silently dropping the error code.
describe('runner — #3086: spawn errorCode surfaced in err file', () => {
test('a spawn ENOENT writes the error code to the err file', async () => {
const p = plan('gemini');
const d = deps({
spawn: () => ({ status: null, stdout: '', stderr: '', errorCode: 'ENOENT' }),
});
await runLane(p, d, { repoRoot: ROOT });
const errContent = d.files[p.errPath] || '';
assert.ok(errContent.includes('ENOENT'),
`err file must include the spawn error code; got: ${errContent}`);
});
test('a spawn ETIMEDOUT writes the error code to the err file', async () => {
const p = plan('codex');
const d = deps({
spawn: () => ({ status: null, stdout: '', stderr: '', errorCode: 'ETIMEDOUT' }),
});
await runLane(p, d, { repoRoot: ROOT });
const errContent = d.files[p.errPath] || '';
assert.ok(errContent.includes('ETIMEDOUT'),
`err file must include the spawn error code; got: ${errContent}`);
});
test('a successful spawn with stderr does NOT add a spawn error marker', async () => {
const p = plan('gemini');
const d = deps({
spawn: () => ({ status: 0, stdout: '## Review\nok', stderr: 'some warning', errorCode: undefined }),
});
await runLane(p, d, { repoRoot: ROOT });
const errContent = d.files[p.errPath] || '';
assert.ok(!errContent.includes('[spawn error:'),
`err file must NOT contain a spawn error marker on success; got: ${errContent}`);
assert.ok(errContent.includes('some warning'),
'legitimate stderr should still be written');
});
});