mod common;
use std::time::Duration;
use a2a_protocol_client::{ClientBuilder, ClientError, EventStream};
use a2a_protocol_types::ErrorCode;
use common::{LAST_CHUNK, SSE_HEAD, chunk, params, rest_status_frame, serve, write};
const GUARD: Duration = Duration::from_secs(20);
fn aip193(code: u16, status: &str, reason: &str, message: &str) -> String {
format!(
r#"{{"error":{{"code":{code},"status":"{status}","message":"{message}","details":[{{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"{reason}","domain":"a2a-protocol.org","metadata":{{"timestamp":"2026-09-22T00:00:00Z"}}}}]}}}}"#
)
}
fn rest_client(url: &str) -> a2a_protocol_client::A2aClient {
ClientBuilder::new(url)
.with_protocol_binding("HTTP+JSON")
.build()
.expect("build")
}
async fn status_server(status: &'static str, body: String) -> String {
serve(move |mut s, _| {
let body = body.clone();
async move {
let resp = format!(
"HTTP/1.1 {status}\r\ncontent-type: application/json\r\n\
content-length: {}\r\n\r\n{body}",
body.len()
);
write(&mut s, &resp).await;
}
})
.await
}
async fn sse_server(frames: Vec<String>) -> String {
serve(move |mut s, _| {
let frames = frames.clone();
async move {
write(&mut s, SSE_HEAD).await;
for f in &frames {
write(&mut s, &chunk(f)).await;
}
write(&mut s, LAST_CHUNK).await;
}
})
.await
}
async fn next_err(stream: &mut EventStream) -> ClientError {
loop {
match tokio::time::timeout(GUARD, stream.next())
.await
.expect("guard")
{
Some(Ok(_)) => {}
Some(Err(e)) => return e,
None => panic!("the stream ended without the error"),
}
}
}
fn assert_protocol(err: &ClientError, code: ErrorCode, message: &str) {
match err {
ClientError::Protocol(e) => {
assert_eq!(e.code, code, "{err}");
assert_eq!(e.message, message, "{err}");
}
other => panic!("expected Protocol({code:?}), got {other:?}"),
}
}
#[tokio::test]
async fn subscribe_404_decodes_like_get_task_404() {
let body = aip193(404, "NOT_FOUND", "TASK_NOT_FOUND", "task not found");
let url = status_server("404 Not Found", body).await;
let client = rest_client(&url);
let unary = client
.get_task(a2a_protocol_types::TaskQueryParams {
tenant: None,
id: "x".into(),
history_length: None,
})
.await
.expect_err("404");
let stream = client.subscribe_to_task("x").await.expect_err("404");
assert_protocol(&unary, ErrorCode::TaskNotFound, "task not found");
assert_protocol(&stream, ErrorCode::TaskNotFound, "task not found");
}
#[tokio::test]
async fn stream_message_non_2xx_aip193_is_a_protocol_error() {
let body = aip193(
400,
"FAILED_PRECONDITION",
"UNSUPPORTED_OPERATION",
"task is terminal",
);
let url = status_server("400 Bad Request", body).await;
let err = rest_client(&url)
.stream_message(params())
.await
.expect_err("400");
assert_protocol(&err, ErrorCode::UnsupportedOperation, "task is terminal");
}
#[tokio::test]
async fn a_non_aip193_error_body_keeps_unexpected_status() {
let url = status_server("502 Bad Gateway", "upstream down".to_owned()).await;
let err = rest_client(&url)
.stream_message(params())
.await
.expect_err("502");
assert!(
matches!(err, ClientError::UnexpectedStatus { status: 502, .. }),
"{err:?}"
);
}
#[tokio::test]
async fn a2a_go_in_stream_aip193_frame_is_a_protocol_error() {
let url = sse_server(vec![
rest_status_frame("TASK_STATE_WORKING"),
format!(
"id: 4a0c7a52-1b0e-4c44-9f3a-0d7f3bba1c55\ndata: {}\n\n",
aip193(404, "NOT_FOUND", "TASK_NOT_FOUND", "task gone")
),
])
.await;
let mut stream = rest_client(&url)
.stream_message(params())
.await
.expect("stream");
let err = next_err(&mut stream).await;
assert_protocol(&err, ErrorCode::TaskNotFound, "task gone");
assert!(
tokio::time::timeout(GUARD, stream.next())
.await
.expect("guard")
.is_none(),
"an error frame ends the stream"
);
}
#[tokio::test]
async fn in_stream_aip193_with_standard_or_unknown_reasons() {
for (reason, code) in [
("INTERNAL_ERROR", ErrorCode::InternalError),
("INVALID_PARAMS", ErrorCode::InvalidParams),
("INVALID_REQUEST", ErrorCode::InvalidRequest),
("METHOD_NOT_FOUND", ErrorCode::MethodNotFound),
("PARSE_ERROR", ErrorCode::ParseError),
("UNAUTHENTICATED", ErrorCode::InternalError),
] {
let url = sse_server(vec![format!(
"data: {}\n\n",
aip193(500, "INTERNAL", reason, "boom")
)])
.await;
let mut stream = rest_client(&url)
.stream_message(params())
.await
.expect("stream");
let err = next_err(&mut stream).await;
assert_protocol(&err, code, "boom");
if let ClientError::Protocol(e) = &err {
let data = e.data.as_ref().expect("the original error is kept");
assert_eq!(data["error"]["details"][0]["reason"], reason, "{reason}");
}
}
}
#[tokio::test]
async fn own_server_event_error_frame_is_a_protocol_error() {
let lagged = a2a_protocol_types::A2aError::stream_lagged(3);
let lagged_json = serde_json::to_string(&lagged).expect("json");
let url = sse_server(vec![
rest_status_frame("TASK_STATE_WORKING"),
format!("event: error\ndata: {lagged_json}\n\n"),
])
.await;
let mut stream = rest_client(&url)
.stream_message(params())
.await
.expect("stream");
let err = next_err(&mut stream).await;
assert!(err.is_stream_lagged(), "the lag marker survives: {err:?}");
assert_eq!(err.dropped_event_count(), Some(3));
let url = sse_server(vec![
"event: error\ndata: {\"code\":-32001,\"message\":\"no such task\"}\n\n".to_owned(),
])
.await;
let mut stream = rest_client(&url)
.stream_message(params())
.await
.expect("stream");
let err = next_err(&mut stream).await;
assert_protocol(&err, ErrorCode::TaskNotFound, "no such task");
}
#[tokio::test]
async fn a_frame_that_is_neither_event_nor_error_stays_a_parse_failure() {
for frame in [
"data: {\"unknown\":{}}\n\n",
"data: {\"error\":\"a string, not an object\"}\n\n",
"event: error\ndata: not json\n\n",
] {
let url = sse_server(vec![frame.to_owned()]).await;
let mut stream = rest_client(&url)
.stream_message(params())
.await
.expect("stream");
let err = next_err(&mut stream).await;
assert!(
matches!(err, ClientError::Serialization(_)),
"{frame}: {err:?}"
);
}
}