fix(pi-shell): mapped BrokenPipe stdout errors to silent fd exit code 141

- Updated `run_fd_sync` to handle `io::ErrorKind::BrokenPipe` by returning exit code `141` without writing an error message.
- Added a regression test that verifies `fd` exits with `141` and empty stderr when stdout is closed early (e.g. `fd ... | head` scenario).
- Recorded the broken-pipe behavior fix in `packages/coding-agent/CHANGELOG.md`.
This commit is contained in:
can1357
2026-07-15 10:09:41 +02:00
parent e4bbe34f61
commit e542f762ca
2 changed files with 30 additions and 1 deletions
+29 -1
View File
@@ -647,6 +647,10 @@ fn run_fd_sync(
0
}
},
// A closed downstream reader (`fd … | head`) surfaces as BrokenPipe on
// stdout writes. Real fd dies silently from SIGPIPE; mirror that with
// exit 141 (128+SIGPIPE) and no diagnostic.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => 141,
Err(err) => {
let _ = writeln!(stderr, "fd: {err}");
2
@@ -1650,7 +1654,7 @@ mod tests {
use brush_core::openfiles::OpenFile;
use clap::Parser;
use super::{FdCli, cancel_heartbeat, search};
use super::{FdCli, cancel_heartbeat, run_fd_sync, search};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
@@ -1803,4 +1807,28 @@ mod tests {
assert!(err.is_empty(), "stderr should stay clean on success: {err:?}");
let _ = fs::remove_dir_all(&tree);
}
#[test]
fn broken_pipe_on_stdout_is_silent_and_exits_141() {
// Regression: `fd … | head` printed "fd: Broken pipe (os error 32)"
// when the downstream builtin closed the read end early. Real fd dies
// silently from SIGPIPE; the builtin must map BrokenPipe to exit 141
// with no stderr diagnostic.
let tree = seeded_tree("epipe");
let (reader, writer) = std::io::pipe().expect("pipe");
drop(reader); // downstream reader (e.g. `head`) already exited
let mut stdout = OpenFile::from(writer);
let (stderr_capture, stderr_file) = capture_file("stderr-epipe");
let mut stderr = OpenFile::from(stderr_file);
let cancelled = AtomicBool::new(false);
let argv = ["fd", "haystack", tree.to_str().expect("utf8 path")]
.map(std::ffi::OsString::from)
.to_vec();
let code = run_fd_sync(argv, tree.clone(), &mut stdout, &mut stderr, &cancelled);
drop(stderr);
assert_eq!(code, 141, "BrokenPipe must map to 128+SIGPIPE");
let err = read_all(&stderr_capture);
assert!(err.is_empty(), "stderr must stay clean on a broken pipe: {err:?}");
let _ = fs::remove_dir_all(&tree);
}
}
+1
View File
@@ -14,6 +14,7 @@
### Fixed
- Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)).
- Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd.
## [16.5.2] - 2026-07-14