diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index ccca85e0ba..fbe6c2ba71 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -2488,7 +2488,10 @@ async fn tokio_main() -> Result<()> { return run_authenticate(args).await; } + // Stdout is the ACP transport when buzz-acp is launched as an agent command. + // Keep every harness diagnostic on stderr so logging can never corrupt NDJSON. tracing_subscriber::fmt() + .with_writer(std::io::stderr) .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("buzz_acp=info")), ) diff --git a/crates/buzz-acp/tests/stdio_contract.rs b/crates/buzz-acp/tests/stdio_contract.rs new file mode 100644 index 0000000000..047607d574 --- /dev/null +++ b/crates/buzz-acp/tests/stdio_contract.rs @@ -0,0 +1,35 @@ +use std::process::Command; + +#[test] +fn startup_and_failure_diagnostics_never_enter_acp_stdout() { + let output = Command::new(env!("CARGO_BIN_EXE_buzz-acp")) + .env("BUZZ_RELAY_URL", "ws://127.0.0.1:1") + .env( + "BUZZ_PRIVATE_KEY", + "0000000000000000000000000000000000000000000000000000000000000001", + ) + .env("BUZZ_AGENT_COMMAND", "/definitely/missing/buzz-agent") + .env("RUST_LOG", "buzz_acp=info") + .output() + .expect("buzz-acp process should start"); + + assert!( + !output.status.success(), + "unreachable relay must fail startup" + ); + assert!( + output.stdout.is_empty(), + "ACP stdout contained diagnostics: {:?}", + String::from_utf8_lossy(&output.stdout) + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("buzz-acp starting:"), + "missing startup INFO: {stderr}" + ); + assert!( + stderr.contains("Error:") || stderr.contains("error"), + "missing failure diagnostic: {stderr}" + ); +}