Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
Expand Down
33 changes: 33 additions & 0 deletions src/tests/headless-v2-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading