diff --git a/jcp/jcp-rs/crates/jcp-gateway/tests/stdio_binary.rs b/jcp/jcp-rs/crates/jcp-gateway/tests/stdio_binary.rs index 8677d904e..82b306373 100644 --- a/jcp/jcp-rs/crates/jcp-gateway/tests/stdio_binary.rs +++ b/jcp/jcp-rs/crates/jcp-gateway/tests/stdio_binary.rs @@ -6,7 +6,7 @@ //! //! The server is awk, answering only the lines that carry an `id`, as an MCP //! server answers requests and never notifications. It runs on every host -//! this crate builds on. +//! this crate builds on, whichever awk the host has ([`awk_server`]). use serde_json::{json, Value}; use std::io::{BufRead, BufReader, Read, Write}; @@ -18,6 +18,33 @@ use std::time::Duration; /// call takes, on a host at a load of 14 to 260. const LINE_DEADLINE: Duration = Duration::from_secs(120); +/// The command line of an awk server running `script` over a pipe. +/// +/// An awk reading a pipe must hand each line to the script as it arrives, or +/// the server never answers a request until its client closes the pipe. mawk, +/// the default awk on Debian and Ubuntu hosts, buffers piped input in blocks +/// unless it is run with `-W interactive`; the other awks read lines as they +/// come and reject or ignore that option. So the awk is asked what it is: a +/// host whose awk is mawk once failed both tests at their deadline, while the +/// same tests passed where the awk was another. +fn awk_server(script: &str) -> Vec { + const AWK: &str = "/usr/bin/awk"; + let is_mawk = Command::new(AWK) + .args(["-W", "version"]) + .stdin(Stdio::null()) + .output() + .map(|o| { + String::from_utf8_lossy(&o.stdout).contains("mawk") || String::from_utf8_lossy(&o.stderr).contains("mawk") + }) + .unwrap_or(false); + let mut argv = vec![AWK.to_string()]; + if is_mawk { + argv.extend(["-W".to_string(), "interactive".to_string()]); + } + argv.push(script.to_string()); + argv +} + /// An MCP-ish server in awk: a request (a line with an `id`) gets one /// response carrying that id; a line without one gets nothing. const ANSWERS_REQUESTS_ONLY: &str = r#"/"id"/ { @@ -111,7 +138,7 @@ fn call(id: i64) -> Value { /// without waiting for a reply, or it never reads another line. #[test] fn the_binary_serves_calls_after_the_initialized_notification() { - let mut g = Gateway::spawn("smoke", &["/usr/bin/awk", ANSWERS_REQUESTS_ONLY], &[]); + let mut g = Gateway::spawn("smoke", &awk_server(ANSWERS_REQUESTS_ONLY).iter().map(String::as_str).collect::>(), &[]); g.send(&json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}})); let init = g.next("the initialize response"); assert_eq!(init["id"], json!(1), "{init}"); @@ -166,7 +193,11 @@ const TALKS_MID_CALL: &str = r#"/"method":"tools\/call"/ { /// the call's id that ends its span. #[test] fn the_binary_relays_what_a_server_sends_in_the_middle_of_a_call() { - let mut g = Gateway::spawn("chatty", &["/usr/bin/awk", TALKS_MID_CALL], &["--upstream-self-contained"]); + let mut g = Gateway::spawn( + "chatty", + &awk_server(TALKS_MID_CALL).iter().map(String::as_str).collect::>(), + &["--upstream-self-contained"], + ); g.send(&json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}})); assert_eq!(g.next("the initialize response")["id"], json!(1)); g.send(&json!({"jsonrpc":"2.0","method":"notifications/initialized"}));