jcp-gateway: the stdio tests' awk server answers on any awk #234

Merged
dcharlot merged 1 commit from fix/gateway-test-server-any-awk into main 2026-10-13 09:31:15 -04:00

View file

@ -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<String> {
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::<Vec<_>>(), &[]);
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::<Vec<_>>(),
&["--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"}));