use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use a2a_protocol_types::error::{A2aResult, ErrorCode};
use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
use a2a_protocol_types::method::Method;
use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
use a2a_protocol_server::builder::RequestHandlerBuilder;
use a2a_protocol_server::dispatch::JsonRpcDispatcher;
use a2a_protocol_server::executor::AgentExecutor;
use a2a_protocol_server::push::InMemoryPushConfigStore;
use a2a_protocol_server::request_context::RequestContext;
use a2a_protocol_server::streaming::EventQueueWriter;
struct EchoExecutor;
impl AgentExecutor for EchoExecutor {
fn execute<'a>(
&'a self,
ctx: &'a RequestContext,
queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move {
queue
.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: ctx.task_id.clone(),
context_id: ContextId::new(ctx.context_id.clone()),
status: TaskStatus::with_timestamp(TaskState::Completed),
metadata: None,
}))
.await?;
Ok(())
})
}
}
fn full_card() -> AgentCard {
AgentCard {
url: None,
name: "routing-completeness-agent".into(),
version: "1.0".into(),
description: "Agent advertising every capability".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: a2a_protocol_types::A2A_VERSION.into(),
tenant: None,
}],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none()
.with_streaming(true)
.with_push_notifications(true)
.with_extended_agent_card(true),
security_schemes: None,
security_requirements: None,
default_input_modes: vec!["text/plain".into()],
default_output_modes: vec!["text/plain".into()],
skills: vec![],
signatures: None,
}
}
async fn start_server() -> std::net::SocketAddr {
let handler = Arc::new(
RequestHandlerBuilder::new(EchoExecutor)
.with_agent_card(full_card())
.with_push_config_store(InMemoryPushConfigStore::new())
.allow_unauthenticated_extended_card()
.build()
.expect("build handler"),
);
let dispatcher = Arc::new(JsonRpcDispatcher::new(handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("local_addr");
tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let io = hyper_util::rt::TokioIo::new(stream);
let d = Arc::clone(&dispatcher);
tokio::spawn(async move {
let service = hyper::service::service_fn(move |req| {
let d = Arc::clone(&d);
async move { Ok::<_, std::convert::Infallible>(d.dispatch(req).await) }
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
addr
}
async fn call(addr: std::net::SocketAddr, method: &str) -> serde_json::Value {
let body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": {}
})
.to_string();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/"))
.header("content-type", "application/json")
.header(
a2a_protocol_types::A2A_VERSION_HEADER,
a2a_protocol_types::A2A_VERSION,
)
.body(Full::new(Bytes::from(body)))
.expect("build request");
let stream = tokio::net::TcpStream::connect(addr).await.expect("connect");
let io = hyper_util::rt::TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io)
.await
.expect("handshake");
tokio::spawn(async move {
let _ = conn.await;
});
let resp = sender.send_request(req).await.expect("send");
let bytes = resp.into_body().collect().await.expect("body").to_bytes();
serde_json::from_slice(&bytes).unwrap_or_else(|e| {
panic!(
"response was not JSON ({e}): {}",
String::from_utf8_lossy(&bytes)
)
})
}
fn error_code(v: &serde_json::Value) -> Option<i64> {
v.get("error")?.get("code")?.as_i64()
}
#[tokio::test]
async fn every_spec_method_is_routed() {
let addr = start_server().await;
let not_found = i64::from(ErrorCode::MethodNotFound.as_i32());
let mut unrouted = Vec::new();
for m in Method::ALL {
let resp = call(addr, m.wire_name()).await;
if error_code(&resp) == Some(not_found) {
unrouted.push(m.wire_name());
}
}
assert!(
unrouted.is_empty(),
"the ratified proto declares these methods but the JSON-RPC dispatcher \
answers MethodNotFound for them: {unrouted:?}"
);
}
#[tokio::test]
async fn nothing_outside_the_spec_set_is_routed() {
let addr = start_server().await;
let not_found = i64::from(ErrorCode::MethodNotFound.as_i32());
for bogus in [
"TotallyInventedMethod",
"",
"sendmessage",
"SendMessage ",
"message/send",
"message/stream",
"tasks/get",
"tasks/list",
"tasks/cancel",
"tasks/resubscribe",
"tasks/pushNotificationConfig/set",
] {
let resp = call(addr, bogus).await;
assert_eq!(
error_code(&resp),
Some(not_found),
"method {bogus:?} is not in the ratified proto but the dispatcher did \
not answer MethodNotFound; it answered: {resp}"
);
}
}
#[tokio::test]
async fn method_not_found_is_actually_distinguishable() {
let addr = start_server().await;
let routed = call(addr, Method::GetTask.wire_name()).await;
let unrouted = call(addr, "NoSuchMethodAnywhere").await;
let routed_code = error_code(&routed);
let unrouted_code = error_code(&unrouted);
assert_eq!(
unrouted_code,
Some(i64::from(ErrorCode::MethodNotFound.as_i32())),
"unrouted method did not answer MethodNotFound: {unrouted}"
);
assert_ne!(
routed_code, unrouted_code,
"a routed method and an unrouted one produced the same error code, so \
the completeness tests above cannot tell them apart: routed={routed}, \
unrouted={unrouted}"
);
}