diff --git a/src/headless.ts b/src/headless.ts index eb75797697..b014e41ca1 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -900,9 +900,14 @@ async function runHeadlessOnce(options: HeadlessOptions, restartCount: number): if (internalProcess) { internalProcess.on('exit', (code: number | null) => { if (!completed) { - const msg = `[headless] Child process exited unexpectedly with code ${code ?? 'null'}\n` - process.stderr.write(msg) - exitCode = EXIT_ERROR + if (code === 0) { + process.stderr.write('[headless] Child exited cleanly (code 0) without terminal notification\n') + exitCode = EXIT_SUCCESS + } else { + const msg = `[headless] Child process exited unexpectedly with code ${code ?? 'null'}\n` + process.stderr.write(msg) + exitCode = EXIT_ERROR + } resolveCompletion() } }) diff --git a/src/tests/headless-v2-migration.test.ts b/src/tests/headless-v2-migration.test.ts index 1f233b7104..a3bb559daf 100644 --- a/src/tests/headless-v2-migration.test.ts +++ b/src/tests/headless-v2-migration.test.ts @@ -170,6 +170,12 @@ function handleEvent( } } +function handleChildExitWithoutTerminalNotification(code: number | null, state: EventHandlerState): void { + if (state.completed) return + state.completed = true + state.exitCode = code === 0 ? EXIT_SUCCESS : EXIT_ERROR +} + // ─── execution_complete event handling ────────────────────────────────────── test('execution_complete with status success triggers completion with EXIT_SUCCESS', () => { @@ -224,6 +230,33 @@ test('execution_complete ignored if already completed', () => { assert.equal(state.exitCode, EXIT_SUCCESS) }) +test('clean child exit without terminal notification is treated as success', () => { + const state: EventHandlerState = { completed: false, blocked: false, exitCode: -1, v2Enabled: true } + + handleChildExitWithoutTerminalNotification(0, state) + + assert.equal(state.completed, true) + assert.equal(state.exitCode, EXIT_SUCCESS) +}) + +test('nonzero child exit without terminal notification remains an error', () => { + const state: EventHandlerState = { completed: false, blocked: false, exitCode: -1, v2Enabled: true } + + handleChildExitWithoutTerminalNotification(1, state) + + assert.equal(state.completed, true) + assert.equal(state.exitCode, EXIT_ERROR) +}) + +test('null child exit without terminal notification remains an error', () => { + const state: EventHandlerState = { completed: false, blocked: false, exitCode: -1, v2Enabled: true } + + handleChildExitWithoutTerminalNotification(null, state) + + assert.equal(state.completed, true) + assert.equal(state.exitCode, EXIT_ERROR) +}) + // ─── v1 string-matching fallback ──────────────────────────────────────────── test('v1 fallback: terminal notification still triggers completion', () => {