mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-25 10:57:14 -06:00
`println!` panics when stdout is a closed pipe, so `wordcraft-cli commands --json | head` aborted with exit 101. Every stdout write now goes through a locked handle with `writeln!` and propagates `io::Error`; a BrokenPipe becomes `Failure::StdoutClosed`, which `main` treats as a normal exit 0. The MCP stdio server's BrokenPipe takes the same path, so a client that hangs up ends the server cleanly. Real errors still go to stderr with exit 1. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
39 lines
1.6 KiB
Rust
39 lines
1.6 KiB
Rust
//! A reader that closes stdout early (`wordcraft-cli commands --json | head`) must end the CLI
|
|
//! quietly with exit 0, never with a panic (#508).
|
|
#![cfg(unix)]
|
|
|
|
use std::io::{Read, Write};
|
|
use std::process::{Command, Stdio};
|
|
|
|
fn cli() -> Command {
|
|
Command::new(env!("CARGO_BIN_EXE_wordcraft-cli"))
|
|
}
|
|
|
|
#[test]
|
|
fn commands_json_into_a_closed_pipe_exits_quietly() {
|
|
let mut child = cli().args(["commands", "--json"]).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
|
// The listing is far larger than a pipe buffer: read a little, then hang up like `head -c`.
|
|
let mut stdout = child.stdout.take().unwrap();
|
|
let mut first = [0u8; 16];
|
|
stdout.read_exact(&mut first).unwrap();
|
|
drop(stdout);
|
|
let out = child.wait_with_output().unwrap();
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(out.status.success(), "status {:?}, stderr: {stderr}", out.status);
|
|
assert!(!stderr.contains("panicked"), "stderr: {stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_server_ends_cleanly_when_its_client_closes_stdout() {
|
|
let mut child = cli().arg("mcp").stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
|
drop(child.stdout.take());
|
|
let mut stdin = child.stdin.take().unwrap();
|
|
// The reply to this request has nowhere to go.
|
|
let _ = stdin.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n");
|
|
drop(stdin);
|
|
let out = child.wait_with_output().unwrap();
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(out.status.success(), "status {:?}, stderr: {stderr}", out.status);
|
|
assert!(!stderr.contains("panicked"), "stderr: {stderr}");
|
|
}
|