//! Integration test: a family-emitting subcommand must survive its stdout //! reader closing early (the most natural CLI piping — `| head`, `| less`, a //! closed UI pane). Drives the built `aura` binary and closes the read end of //! its stdout pipe before the writer finishes, then asserts the EPIPE is //! handled cleanly, never a panic. use std::io::Read; use std::process::{Command, Stdio}; /// Path to the freshly-built `aura` binary (Cargo sets this env var for the test /// crate; the binary is named `aura` in `Cargo.toml`). const BIN: &str = env!("CARGO_BIN_EXE_aura"); /// A fresh, unique working directory so the spawned `aura sweep` writes its /// `./runs/runs.jsonl` into a throwaway dir, never dirtying the repo. Unique /// per test + per process; no external tempfile dependency (mirrors `cli_run.rs`). fn temp_cwd(name: &str) -> std::path::PathBuf { let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-{name}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("create temp cwd"); dir } /// Property: a family-emitting command whose stdout reader closes early exits on /// the EPIPE cleanly — it does NOT panic (`failed printing to stdout: Broken /// pipe`) and does NOT exit 101. `aura sweep` streams four family-member JSON /// lines via `println!`; when the reader closes stdout after the first line, the /// next write hits a closed pipe. Rust's runtime sets SIGPIPE to SIG_IGN at /// startup, so that write returns EPIPE and `println!` panics rather than the /// process terminating quietly — exactly the behaviour a CLI must not have. This /// is the contract: any aura subcommand can be piped into `| head` / `| less` / /// a UI pane that goes away, and it must finish quietly, not panic. /// /// Autonomous: constructs its own early-closing reader inline (no `head`, no /// shell, no fixture files) by spawning the binary with a piped stdout and /// dropping the read handle after a single short read, which closes the pipe's /// read end and makes the writer's next write hit EPIPE. #[test] fn family_emitting_command_survives_early_reader_close() { let cwd = temp_cwd("broken-pipe-sweep"); let mut child = Command::new(BIN) .arg("sweep") .current_dir(&cwd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("spawn aura sweep"); // Read just enough to guarantee the writer is mid-stream, then drop the read // handle. Dropping it closes the read end of the pipe; the next `println!` // in the writer then writes to a closed pipe and gets EPIPE. { let mut stdout = child.stdout.take().expect("child stdout piped"); let mut buf = [0u8; 64]; // One short read advances the stream; we deliberately ignore the rest. let _ = stdout.read(&mut buf); // `stdout` drops here, closing the read end of the pipe. } // Drain stderr so the child cannot block writing the panic message, then reap. let mut stderr = String::new(); if let Some(mut err) = child.stderr.take() { let _ = err.read_to_string(&mut stderr); } let status = child.wait().expect("reap aura sweep"); let _ = std::fs::remove_dir_all(&cwd); // The defect: stderr carries the broken-pipe panic. assert!( !stderr.contains("panicked"), "a closed stdout reader must not panic the writer; stderr was: {stderr:?}" ); assert!( !stderr.contains("Broken pipe"), "the broken pipe must be handled cleanly, not surfaced as an error; stderr was: {stderr:?}" ); // And the exit is the clean broken-pipe outcome, never the panic's 101. // A clean SIGPIPE termination is signal 13 (code is None); a deliberate // clean-exit code is anything but 101. The only forbidden outcome is the // panic abort. assert_ne!( status.code(), Some(101), "a closed stdout reader must not exit with the panic code 101; status: {status:?}" ); }