use clap::Command;
use rmcp::ServiceExt;
use rmcp::model::CallToolRequestParams;
use rmcp::service::RoleClient;
use tokio::io::duplex;
use tokio_util::sync::CancellationToken;
use brontes::__test_internal::BrontesServer;
#[derive(Clone)]
struct NoopClient;
impl rmcp::handler::client::ClientHandler for NoopClient {
fn get_info(&self) -> rmcp::model::ClientInfo {
rmcp::model::ClientInfo::default()
}
}
fn fixture_cli() -> Command {
Command::new("brontes-smoke")
.version("0.0.1")
.subcommand(Command::new("greet").about("Say hi"))
.subcommand(Command::new("status").about("Show status"))
}
#[tokio::test]
async fn stdio_tools_list_returns_walked_tree() {
let (client_io, server_io) = duplex(64 * 1024);
let (client_read, client_write) = tokio::io::split(client_io);
let (server_read, server_write) = tokio::io::split(server_io);
let cancel = CancellationToken::new();
let server_task = {
let cancel = cancel.clone();
tokio::spawn(async move {
let server = BrontesServer::new(fixture_cli(), brontes::Config::default())
.expect("construct server");
let running = server
.serve_with_ct((server_read, server_write), cancel)
.await
.expect("server start");
let _ = running.waiting().await;
})
};
let client = NoopClient
.serve_with_ct((client_read, client_write), cancel.clone())
.await
.expect("client start");
let list_result = client
.peer()
.list_tools(None)
.await
.expect("tools/list succeeds");
let names: Vec<String> = list_result
.tools
.iter()
.map(|t| t.name.to_string())
.collect();
assert!(
names.iter().any(|n| n == "brontes-smoke_greet"),
"missing greet tool; got {names:?}"
);
assert!(
names.iter().any(|n| n == "brontes-smoke_status"),
"missing status tool; got {names:?}"
);
let peer_info = client.peer_info().expect("server peer info available");
assert_eq!(peer_info.server_info.name, "brontes-smoke");
assert_eq!(peer_info.server_info.version, "0.0.1");
let _ = client.cancel().await;
cancel.cancel();
let _ = server_task.await;
}
#[tokio::test]
async fn stdio_call_tool_unknown_name_is_error() {
let (client_io, server_io) = duplex(64 * 1024);
let (client_read, client_write) = tokio::io::split(client_io);
let (server_read, server_write) = tokio::io::split(server_io);
let cancel = CancellationToken::new();
let server_task = {
let cancel = cancel.clone();
tokio::spawn(async move {
let server = BrontesServer::new(fixture_cli(), brontes::Config::default())
.expect("construct server");
let running = server
.serve_with_ct((server_read, server_write), cancel)
.await
.expect("server start");
let _ = running.waiting().await;
})
};
let client: rmcp::service::RunningService<RoleClient, NoopClient> = NoopClient
.serve_with_ct((client_read, client_write), cancel.clone())
.await
.expect("client start");
let result = client
.peer()
.call_tool(CallToolRequestParams::new("does-not-exist"))
.await;
assert!(result.is_err(), "calling unknown tool must error");
let _ = client.cancel().await;
cancel.cancel();
let _ = server_task.await;
}
#[tokio::test]
async fn cancellation_token_terminates_server_loop() {
let (client_io, server_io) = duplex(64 * 1024);
let (client_read, client_write) = tokio::io::split(client_io);
let (server_read, server_write) = tokio::io::split(server_io);
let cancel = CancellationToken::new();
let server_task = {
let cancel = cancel.clone();
tokio::spawn(async move {
let server = BrontesServer::new(fixture_cli(), brontes::Config::default())
.expect("construct server");
let running = server
.serve_with_ct((server_read, server_write), cancel)
.await
.expect("server start");
let _ = running.waiting().await;
})
};
let client = NoopClient
.serve_with_ct((client_read, client_write), cancel.clone())
.await
.expect("client start");
let _ = client.peer().list_tools(None).await;
cancel.cancel();
let _ = client.cancel().await;
let joined = tokio::time::timeout(std::time::Duration::from_secs(2), server_task).await;
assert!(
joined.is_ok(),
"server task did not exit within 2s of cancellation"
);
}