Skip to content

fix(run): report a non-zero exit code when the child dies from a signal#58

Open
exo-nikita wants to merge 1 commit into
mainfrom
claude/stasis-core-plugins-review-wxauv4
Open

fix(run): report a non-zero exit code when the child dies from a signal#58
exo-nikita wants to merge 1 commit into
mainfrom
claude/stasis-core-plugins-review-wxauv4

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

The bug

Both CLI launchers (stasis-core run and stasis run) wrap the user's program in a child process and propagate its exit code with:

const [code] = await once(child, 'close')
process.exitCode = code

When the child dies from a signal (SIGSEGV, SIGKILL, OOM-kill), the 'close' event yields code === null, so process.exitCode = null and the launcher exits 0 — a crashed program is reported as success. In CI this silently turns crashes into green builds.

Repro: an entry doing process.kill(process.pid, 'SIGKILL') run via stasis-core run --lock=ignore entry.js exited 0, while running it under node directly exits 137.

The fix

Destructure the signal from the 'close' event too, and apply the shell convention 128 + signo when code is null (e.g. 137 for SIGKILL, 139 for SIGSEGV), in both bins:

const [code, signal] = await once(child, 'close')
process.exitCode = code ?? 128 + (osConstants.signals[signal] ?? 0)

Test

Added a regression test to tests/stasis-core-cli.test.js: a tmp fixture whose entry SIGKILLs itself, run via stasis-core run --lock=ignore, asserting the reported status is exactly 137. (SIGKILL rather than SIGSEGV to avoid core-dump side effects.) No mirrored test in tests/cli.test.js — the stasis bin's run path needs the installed @exodus/stasis-core workspace dep to resolve its loader, which wasn't available in the environment this was authored in; the fix itself is applied to both bins and is the identical pattern covered by the stasis-core test.

Manual verification: the SIGKILL fixture exits 0 before the fix and 137 after; --version and the rest of tests/stasis-core-cli.test.js (10/10) still pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M16Lwt8cM1azwev94UP81X


Generated by Claude Code

Both CLI launchers propagated the child's exit code via
`const [code] = await once(child, 'close')`, but when the child is
killed by a signal (SIGSEGV, SIGKILL, OOM-kill) the 'close' event
yields code=null, so `process.exitCode = null` made the launcher exit
0 -- a crashed program was reported as success.

Destructure the signal too and apply the shell convention 128+signo
(e.g. 137 for SIGKILL) when code is null, in both stasis-core and
stasis bins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M16Lwt8cM1azwev94UP81X
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