use super::bridge;
use crate::test_support::Scratch;
use serde_json::{Value, json};
mod content;
mod pin;
mod refusals;
fn server(script: &str) -> Vec<String> {
vec!["/bin/sh".to_owned(), "-c".to_owned(), script.to_owned()]
}
fn conversing(answers: &[String], log: &std::path::Path) -> Vec<String> {
let mut said = String::new();
for answer in answers {
said.push_str("printf '%s\\n' '");
said.push_str(answer);
said.push_str("'; ");
}
server(&format!(
"read -r line; printf '%s\\n' '{}'; read -r line; \
read -r line; printf '%s\\n' \"$line\" > '{}'; {said}",
handshake(),
log.display()
))
}
fn handshake() -> String {
json!({"jsonrpc": "2.0", "id": 1, "result": {}}).to_string()
}
fn called(answers: &[String], input: &str) -> (crate::invocation::Capture, Value) {
let scratch = Scratch::new();
let log = scratch.join("call.json");
let capture = bridge("fetch", &conversing(answers, &log), input);
let sent = std::fs::read_to_string(&log).unwrap_or_default();
(capture, serde_json::from_str(&sent).unwrap_or(Value::Null))
}
fn answered(result: &str) -> crate::invocation::Capture {
called(&[result.to_owned()], "{}").0
}
fn result(body: Value) -> String {
json!({"jsonrpc": "2.0", "id": 2, "result": body}).to_string()
}
fn text(said: &str) -> String {
result(json!({"content": [{"type": "text", "text": said}]}))
}
#[test]
fn the_input_on_stdin_becomes_the_calls_arguments_verbatim() {
let input = json!({"url": "https://example.com/a", "max_length": 500});
let (capture, sent) = called(&[text("a page")], &input.to_string());
assert_eq!(sent["method"], "tools/call");
assert_eq!(sent["params"]["name"], "fetch");
assert_eq!(sent["params"]["arguments"], input);
assert_eq!(capture.stdout, "a page\n");
assert_eq!(capture.exit_code, 0);
}
#[test]
fn the_call_is_the_second_request_and_the_first_was_the_handshake() {
let (_, sent) = called(&[text("said")], "{}");
assert_eq!(sent["id"], 2, "the handshake spent the first id");
assert_eq!(sent["jsonrpc"], "2.0");
}
#[test]
fn a_notification_before_the_answer_is_not_the_answer() {
let logged = json!({"jsonrpc": "2.0", "method": "notifications/message",
"params": {"level": "info"}})
.to_string();
let (capture, _) = called(&[logged, text("after the log")], "{}");
assert_eq!(capture.stdout, "after the log\n");
assert_eq!(capture.exit_code, 0);
}
#[test]
fn prose_on_the_transport_is_not_an_answer_and_is_not_a_refusal() {
let scratch = Scratch::new();
let mut argv = conversing(&[text("the page")], &scratch.join("call.json"));
if let Some(script) = argv.last_mut() {
script.insert_str(0, "printf 'npm WARN EBADENGINE unsupported engine\n'; ");
}
let capture = bridge("fetch", &argv, "{}");
assert_eq!(capture.stdout, "the page\n");
assert_eq!(capture.exit_code, 0);
}
#[test]
fn a_server_that_ignores_end_of_file_is_stopped_anyway() {
let scratch = Scratch::new();
let mut argv = conversing(&[text("answered, then stayed")], &scratch.join("call.json"));
if let Some(script) = argv.last_mut() {
script.push_str("while :; do sleep 1; done");
}
let started = std::time::Instant::now();
let capture = bridge("fetch", &argv, "{}");
assert_eq!(capture.stdout, "answered, then stayed\n");
assert!(
started.elapsed() < std::time::Duration::from_secs(10),
"the bridge waited on a server that was never going to leave"
);
}
#[test]
fn a_helper_the_server_backgrounded_does_not_outlive_the_invocation() {
let scratch = Scratch::new();
let marker = scratch.join("helper-ran");
let mut argv = conversing(&[text("done")], &scratch.join("call.json"));
if let Some(script) = argv.last_mut() {
script.insert_str(0, &format!("(sleep 0.6; : > '{}') & ", marker.display()));
}
let capture = bridge("fetch", &argv, "{}");
assert_eq!(capture.stdout, "done\n");
std::thread::sleep(std::time::Duration::from_millis(1200));
assert!(
!marker.exists(),
"the helper outlived the invocation that started it"
);
}