From 4e9aa8b58b913d0b248cd6ce38f98b69c791eaf4 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:29:46 -0400 Subject: [PATCH] jcp-gateway: the stdio tests' awk server answers on any awk The two tests that drive the gateway binary in front of an awk server passed in CI's Linux image and on macOS, but on a host whose awk is mawk (Debian's and Ubuntu's default) both failed at their 120 s deadline: mawk buffers piped input in blocks, so the server never saw the initialize request until the pipe closed, and the gateway, correctly, waited for its reply. The test asks the awk what it is and runs mawk with `-W interactive`, which reads the pipe a line at a time; other awks, which reject or ignore that option, are left as they are. Co-Authored-By: Claude Opus 5.5 --- .../crates/jcp-gateway/tests/stdio_binary.rs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) 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"})); -- 2.54.0 (Apple Git-157)