Repository navigation
[Hackathon] fix(cli): EPIPE guard forces exit 0 and overrides a failing verdict (pipefail CI goes green) #489
Description
Verified against (commit SHA or released version)
commit 988c035 (v0.14.0)
Discord Username / User ID
uisupersaiyan3
What does this improvement do?
When stdout's reader closes early, installBrokenPipeGuard calls exit(0) (interrupt.ts:361-370); in an armed lifetime it aborts with BrokenPipeInterruptError, whose exitCode is also 0 (interrupt.ts:167-168). The real exit code is only set later, after async telemetry (index.ts:409), so the forced exit(0) wins.
Result: a failing test wait / test run --wait / test rerun --wait exits 0 whenever its stdout exceeds the pipe buffer (~64 KiB) and the reader stops early: | head, | head -c, | grep -m1, | jq that errors out. GitHub Actions shell: bash runs with -eo pipefail, so the step goes green although tests failed. Large --output json results (big suites, long failure text) reach the threshold easily.
The comment calls 0 the "conventional SIGPIPE-equivalent", but a process killed by SIGPIPE exits 141, and before PR #185 this path exited 1. Swallowing the stack trace is right; erasing the command's verdict is not.
Details / implementation notes
End-to-end repro with the built CLI against a local mock API whose GET /runs/run_1 returns a failed run with a 300 KB error string:
$ testsprite test wait run_1 --output json > /dev/null ; exit=1
$ set -o pipefail; testsprite test wait run_1 --output json | cat > /dev/null ; exit=1
$ set -o pipefail; testsprite test wait run_1 --output json | head -c 100 >/dev/null ; exit=0 PIPESTATUS=0 0
stderr even prints {"error":{"code":"CLI_ERROR","message":"Run run_1 finished with status: failed",...}} in the last case, yet the exit code is 0. With small output there is no EPIPE and the exit stays 1, confirming the buffer threshold.
Why tests miss it: interrupt.test.ts:324 ("exits 0 on stdout EPIPE") and :336 (armed scope) exercise the guard in isolation; none has a non-zero verdict pending, so the override is never observed. The fix keeps the :324 success case green.
Proposed fix (no new dependency, no new flags):
- On stdout EPIPE, stop writing (destroy/neutralise stdout) and let the command's own exit code stand, or exit with
process.exitCodewhen it is already non-zero, else 0 (or 141 if maintainers prefer SIGPIPE semantics). - Give the armed
BrokenPipeInterruptErrorpath the same treatment so a tunnel run's failure verdict also survives.
Tests: disarmed EPIPE with a pending failure keeps exit 1; plain success plus EPIPE still exits 0; armed EPIPE during a failing run keeps the failure code after cleanup.
Not a duplicate: grepped issues/PRs for EPIPE, broken pipe, SIGPIPE, 141, pipefail, | head. Only PR #185 (which introduced the guard) matches. PR to follow once assigned.
Confirmations
- I have searched existing issues and this is not a duplicate.
- I have provided my Discord identity above for reward coordination.
Activity
zeshi-du commentedon Oct 20, 2026
Thanks @crypticsaiyan — real bug, and your analysis matches v0.15.0: the disarmed guard calls exit(0) (interrupt.ts:369), and the armed path aborts with an exitCode-0
BrokenPipeInterruptError, while the real exit code is applied later in index.ts after an async step. Accepted.Scope:
Comment /assign to claim it.
Lexiie commentedon Oct 20, 2026
/assign
testsprite-hob commentedon Oct 20, 2026
Assigned to @Lexiie. Thanks for taking this on. (0 more slots available.)
Your open assigned issues (3/3):
ci runagainst a local app fails on a--max-concurrency/--localerror the user never passed; past it, the gate writes no step outputs and ignores--fail-on-skipped#549 — [Hackathon] fix(ci):ci runagainst a local app fails on a--max-concurrency/--localerror the user never passed; past it, the gate writes no step outputs and ignores--fail-on-skippedcrypticsaiyan commentedon Oct 21, 2026
/assign
testsprite-hob commentedon Oct 21, 2026
Assigned to @crypticsaiyan. Thanks for taking this on. (0 more slots available.)
Your open assigned issues (3/3):
--output csvand--output ndjsonfor list commands #164 — [Hackathon] feat(cli): add--output csvand--output ndjsonfor list commandscrypticsaiyan commentedon Oct 21, 2026
PR #631 is up for this, within the scope above: changes only in interrupt.ts, a fails-before test for the non-zero verdict + EPIPE case, the success case kept at exit 0, and the design described in the PR body. End-to-end,
test wait <failed-run> --output json | head -c 100now exits 1 (0 on main). Details and a side-by-side check are in the PR.