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_server::builder::RequestHandlerBuilder;
use a2a_protocol_server::dispatch::JsonRpcDispatcher;
use a2a_protocol_server::executor::AgentExecutor;
use a2a_protocol_server::request_context::RequestContext;
use a2a_protocol_server::streaming::EventQueueWriter;
struct NoopExecutor;
impl AgentExecutor for NoopExecutor {
fn execute<'a>(
&'a self,
_ctx: &'a RequestContext,
_queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
async fn start_jsonrpc_server() -> std::net::SocketAddr {
let handler = Arc::new(RequestHandlerBuilder::new(NoopExecutor).build().unwrap());
let dispatcher = Arc::new(JsonRpcDispatcher::new(handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
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 post_streaming(
addr: std::net::SocketAddr,
body: &serde_json::Value,
) -> (u16, String, String) {
let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
.build_http::<Full<Bytes>>();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/"))
.header("content-type", "application/json")
.header("accept", "text/event-stream")
.header("a2a-version", "1.0")
.body(Full::new(Bytes::from(body.to_string())))
.unwrap();
let resp = client.request(req).await.unwrap();
let status = resp.status().as_u16();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_owned();
let bytes = resp.collect().await.unwrap().to_bytes();
(
status,
content_type,
String::from_utf8_lossy(&bytes).into_owned(),
)
}
fn assert_plain_json_error(
(status, content_type, body): &(u16, String, String),
id: &serde_json::Value,
code: ErrorCode,
) -> serde_json::Value {
assert_eq!(*status, 200, "body:\n{body}");
assert!(
content_type.starts_with("application/json"),
"the official TCK reads any text/event-stream answer as a successful \
stream; got content-type {content_type:?}, body:\n{body}"
);
let v: serde_json::Value = serde_json::from_str(body).unwrap();
assert_eq!(v["jsonrpc"], "2.0", "body:\n{body}");
assert_eq!(&v["id"], id, "the error echoes the request id");
assert!(v.get("result").is_none(), "body:\n{body}");
assert_eq!(
v["error"]["code"],
code.as_i32(),
"wrong error code; body:\n{body}"
);
v["error"].clone()
}
#[tokio::test]
async fn subscribe_to_unknown_task_is_a_plain_json_error() {
let addr = start_jsonrpc_server().await;
let id = serde_json::json!("sub-1");
let resp = post_streaming(
addr,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "SubscribeToTask",
"params": { "id": "no-such-task" }
}),
)
.await;
let err = assert_plain_json_error(&resp, &id, ErrorCode::TaskNotFound);
assert_eq!(err["data"][0]["reason"], "TASK_NOT_FOUND", "{err}");
}
#[tokio::test]
async fn subscribe_with_invalid_params_is_a_plain_json_error() {
let addr = start_jsonrpc_server().await;
let id = serde_json::json!(7);
let resp = post_streaming(
addr,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "SubscribeToTask",
"params": { "id": 42 }
}),
)
.await;
assert_plain_json_error(&resp, &id, ErrorCode::InvalidParams);
}
#[tokio::test]
async fn send_streaming_message_with_invalid_params_is_a_plain_json_error() {
let addr = start_jsonrpc_server().await;
let id = serde_json::json!("stream-1");
let resp = post_streaming(
addr,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "SendStreamingMessage",
"params": { "message": "not a message" }
}),
)
.await;
assert_plain_json_error(&resp, &id, ErrorCode::InvalidParams);
}
#[tokio::test]
async fn send_streaming_message_handler_error_is_a_plain_json_error() {
let addr = start_jsonrpc_server().await;
let id = serde_json::json!("stream-2");
let resp = post_streaming(
addr,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "SendStreamingMessage",
"params": { "message": {
"messageId": "m-1",
"role": "ROLE_USER",
"taskId": "no-such-task",
"parts": [{ "text": "hi" }]
}}
}),
)
.await;
assert_plain_json_error(&resp, &id, ErrorCode::TaskNotFound);
}
#[tokio::test]
async fn non_streaming_method_errors_stay_plain_json() {
let addr = start_jsonrpc_server().await;
let (status, content_type, body) = post_streaming(
addr,
&serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "GetTask",
"params": { "id": "no-such-task" }
}),
)
.await;
assert_eq!(status, 200);
assert!(
content_type.starts_with("application/json"),
"{content_type}"
);
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["error"]["code"], ErrorCode::TaskNotFound.as_i32());
}