Skip to content

fix: treat signal-terminated subprocesses as failures in runCommand#429

Open
SEPURI-SAI-KRISHNA wants to merge 1 commit into
openai:mainfrom
SEPURI-SAI-KRISHNA:fix/signal-terminated-subprocess-status
Open

fix: treat signal-terminated subprocesses as failures in runCommand#429
SEPURI-SAI-KRISHNA wants to merge 1 commit into
openai:mainfrom
SEPURI-SAI-KRISHNA:fix/signal-terminated-subprocess-status

Conversation

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown

Problem

spawnSync returns status === null when a child process is terminated by a signal (e.g. SIGKILL from the OOM killer, or a manual kill). runCommand coerced that to 0 with result.status ?? 0, so runCommandChecked (status !== 0) and binaryAvailable treated the killed process as a success callers then proceeded with truncated or empty stdout as if the command had completed normally (e.g. a git diff that was killed mid-run would look empty/clean).

The existing if (result.signal) branch in formatCommandFailure shows this was meant to be reported as a failure, but the coercion made that path unreachable for the checked helpers.

Fix

Map a signal death to a non-zero status: result.status ?? (result.signal ? 1 : 0). Only the signal case changes; the signal field is still exposed separately and surfaced by formatCommandFailure (signal=SIGKILL).

Tests

Added cases to tests/process.test.mjs: real SIGKILL child reports non-zero status + preserved signal, runCommandChecked throws with signal=SIGKILL, clean exit still 0, non-zero exit preserved, and formatCommandFailure surfaces the signal. Full suite: 95 pass / 0 fail. tsc -p tsconfig.app-server.json passes.

Conflicts

No overlap with the other open process.mjs PRs — #415 doesn't touch this file, and #424 only changes the shell: line and terminateProcessTree (this PR changes runCommand's status mapping). taskkill's numeric exit codes (128/0) are unaffected.

@SEPURI-SAI-KRISHNA SEPURI-SAI-KRISHNA requested a review from a team July 4, 2026 16:29

@rajpratham1 rajpratham1 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small, correct fix for a subtle bug where signal-terminated processes were incorrectly treated as successful.

Why it's correct

Previously:

status: result.status ?? 0

For spawnSync(), if a child is terminated by a signal:

status === null
signal === "SIGKILL" (or similar)

The old code converted this to:

status = 0

meaning downstream code would interpret a killed process as a successful exit.

The new logic:

status: result.status ?? (result.signal ? 1 : 0)

correctly treats signal termination as a non-zero failure while still preserving the signal information.

Impact

This prevents incorrect success detection in helpers such as:

runCommandChecked
binaryAvailable
any future code checking status === 0

Without this fix, a process killed by:

SIGKILL
SIGTERM
SIGINT

could be mistaken for a successful execution.

Test coverage

The added tests are appropriate and cover:

✅ signal-terminated process returns non-zero status
✅ runCommandChecked() throws
✅ successful exit remains 0
✅ non-zero exit codes are preserved
✅ failure formatting still reports the signal

This provides good regression coverage.

Minor note

Using 1 as the synthesized exit status is reasonable since the actual termination signal is preserved separately in result.signal. Consumers that care about the exact cause can inspect signal rather than relying on the synthesized status.

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown
Author

@rajpratham1 Thanks for the review! Agreed on the synthesized 1 the real cause stays in result.signal and is surfaced by formatCommandFailure (signal=SIGKILL), so callers that need the exact cause can still get it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants