#![cfg(feature = "jsonrpc-server")]
use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use axum::body::{Body, to_bytes};
use axum::http::{Request, header::CONTENT_TYPE};
use serde_json::{Value, json};
use tower::ServiceExt;
use a2a_rs::adapter::streaming::InMemoryStreamingHandler;
use a2a_rs::adapter::{InMemoryTaskStorage, JsonRpcAdapter, SimpleAgentInfo, jsonrpc_router};
use a2a_rs::application::{SendOptions, TaskService};
use a2a_rs::domain::{
A2AError, ContextId, Message, SendCompletion, Task, TaskId, TaskState, TaskStatus,
TaskStatusUpdateEvent,
};
use a2a_rs::port::{AsyncMessageHandler, AsyncStreamingHandler, AsyncTaskLifecycle};
#[derive(Clone)]
struct AsyncAgent {
storage: Arc<InMemoryTaskStorage>,
streaming: InMemoryStreamingHandler,
settles_to: Option<TaskState>,
delay: Duration,
}
#[async_trait]
impl AsyncMessageHandler for AsyncAgent {
async fn process_message(
&self,
task_id: &str,
message: &Message,
session_id: Option<&str>,
) -> Result<Task, A2AError> {
let id: TaskId = task_id.parse()?;
let ctx: ContextId = session_id.unwrap_or("ctx").parse()?;
if !self.storage.exists(&id).await? {
self.storage.create(&id, &ctx).await?;
}
let task = self
.storage
.update_status(&id, TaskState::Working, Some(message.clone()))
.await?;
if let Some(final_state) = self.settles_to {
let storage = self.storage.clone();
let streaming = self.streaming.clone();
let delay = self.delay;
let task_id = task_id.to_string();
let context_id = task.context_id.clone();
tokio::spawn(async move {
tokio::time::sleep(delay).await;
let id: TaskId = task_id.parse().expect("valid id");
storage
.update_status(&id, final_state, None)
.await
.expect("commit final state");
streaming
.broadcast_status_update(
&task_id,
TaskStatusUpdateEvent {
task_id: task_id.clone(),
context_id,
kind: "status-update".to_string(),
status: TaskStatus::new(final_state, None),
metadata: None,
},
)
.await
.expect("broadcast final state");
});
}
Ok(task)
}
}
fn service_for(settles_to: Option<TaskState>, delay: Duration) -> TaskService {
let storage = Arc::new(InMemoryTaskStorage::new());
let streaming = InMemoryStreamingHandler::new();
let agent = AsyncAgent {
storage: storage.clone(),
streaming: streaming.clone(),
settles_to,
delay,
};
TaskService::new(
agent,
(*storage).clone(),
(*storage).clone(),
SimpleAgentInfo::new("blocking-test".to_string(), "http://localhost".to_string()),
streaming,
storage.push_notifier(),
)
}
fn message() -> Message {
Message::user_text("hello".to_string(), "m1".to_string())
}
#[tokio::test]
async fn send_message_waits_for_an_async_agent_by_default() {
let service = service_for(Some(TaskState::Completed), Duration::from_millis(150));
let task = service
.send_message("t-default", &message(), None, SendOptions::default())
.await
.expect("send");
assert_eq!(
task.status.state,
::buffa::EnumValue::from(TaskState::Completed),
"a client that sent no configuration is owed a settled task"
);
}
#[tokio::test]
async fn an_interrupted_state_ends_the_wait() {
let service = service_for(Some(TaskState::InputRequired), Duration::from_millis(150));
let task = service
.send_message("t-input", &message(), None, SendOptions::default())
.await
.expect("send");
assert_eq!(
task.status.state,
::buffa::EnumValue::from(TaskState::InputRequired)
);
}
#[tokio::test]
async fn return_immediately_does_not_wait() {
let service = service_for(Some(TaskState::Completed), Duration::from_secs(30));
let started = Instant::now();
let task = service
.send_message(
"t-immediate",
&message(),
None,
SendOptions {
completion: SendCompletion::WhenCreated,
..Default::default()
},
)
.await
.expect("send");
assert_eq!(
task.status.state,
::buffa::EnumValue::from(TaskState::Working)
);
assert!(
started.elapsed() < Duration::from_secs(5),
"must not have waited on the agent"
);
}
#[tokio::test]
async fn the_wait_is_bounded_when_the_agent_never_settles() {
let service = service_for(None, Duration::ZERO).with_send_wait(Duration::from_millis(200));
let started = Instant::now();
let task = service
.send_message("t-hang", &message(), None, SendOptions::default())
.await
.expect("send must return, not error");
let elapsed = started.elapsed();
assert_eq!(
task.status.state,
::buffa::EnumValue::from(TaskState::Working),
"the unsettled task is the honest answer once the budget is spent"
);
assert!(
elapsed >= Duration::from_millis(150),
"should have actually waited, took {elapsed:?}"
);
assert!(
elapsed < Duration::from_secs(5),
"should have given up at the bound, took {elapsed:?}"
);
}
#[tokio::test]
async fn a_server_without_streaming_still_answers() {
let storage = Arc::new(InMemoryTaskStorage::new());
let agent = AsyncAgent {
storage: storage.clone(),
streaming: InMemoryStreamingHandler::new(),
settles_to: None,
delay: Duration::ZERO,
};
let adapter = Arc::new(JsonRpcAdapter::new(
agent,
(*storage).clone(),
(*storage).clone(),
SimpleAgentInfo::new("no-stream".to_string(), "http://localhost".to_string()),
));
let started = Instant::now();
let (_, body) = rpc_send(&adapter, send_params("t-nostream", None)).await;
assert_eq!(state_of(&body), "TASK_STATE_WORKING");
assert!(
started.elapsed() < Duration::from_secs(5),
"must not block on a stream it could not open"
);
}
#[tokio::test(start_paused = true)]
async fn the_default_wait_expires_before_the_default_client_timeout() {
const CLIENT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
let started = tokio::time::Instant::now();
let task = service_for(None, Duration::ZERO)
.send_message("t-default-bound", &message(), None, SendOptions::default())
.await
.expect("send");
let waited = started.elapsed();
assert_eq!(
task.status.state,
::buffa::EnumValue::from(TaskState::Working)
);
assert!(
waited < CLIENT_REQUEST_TIMEOUT,
"server waited {waited:?}, at or past the {CLIENT_REQUEST_TIMEOUT:?} client timeout — \
a slow agent would surface as a connection error, not a task"
);
assert!(
waited > Duration::from_secs(1),
"server returned after {waited:?} — the default budget has to be a real wait"
);
}
fn send_params(task_id: &str, return_immediately: Option<bool>) -> Value {
let mut params = json!({
"message": {
"messageId": "m1",
"role": "ROLE_USER",
"parts": [{ "text": "hello" }],
"taskId": task_id,
}
});
if let Some(flag) = return_immediately {
params["configuration"] = json!({ "returnImmediately": flag });
}
params
}
async fn rpc_send(adapter: &Arc<JsonRpcAdapter>, params: Value) -> (u16, Value) {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": params,
});
let req = Request::builder()
.method("POST")
.uri("/")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap();
let resp = jsonrpc_router(adapter.clone()).oneshot(req).await.unwrap();
let status = resp.status().as_u16();
let bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
(
status,
serde_json::from_slice(&bytes).unwrap_or(Value::Null),
)
}
fn state_of(body: &Value) -> &str {
body["result"]["task"]["status"]["state"]
.as_str()
.unwrap_or_else(|| panic!("no task state in {body}"))
}
fn streaming_adapter(settles_to: Option<TaskState>, delay: Duration) -> Arc<JsonRpcAdapter> {
let storage = Arc::new(InMemoryTaskStorage::new());
let streaming = InMemoryStreamingHandler::new();
let agent = AsyncAgent {
storage: storage.clone(),
streaming: streaming.clone(),
settles_to,
delay,
};
Arc::new(
JsonRpcAdapter::new(
agent,
(*storage).clone(),
(*storage).clone(),
SimpleAgentInfo::new("wire-test".to_string(), "http://localhost".to_string()),
)
.with_streaming_handler(streaming),
)
}
#[tokio::test]
async fn absent_configuration_waits_over_the_wire() {
let adapter = streaming_adapter(Some(TaskState::Completed), Duration::from_millis(150));
let (status, body) = rpc_send(&adapter, send_params("t-wire-default", None)).await;
assert_eq!(status, 200);
assert_eq!(state_of(&body), "TASK_STATE_COMPLETED");
}
#[tokio::test]
async fn explicit_false_waits_over_the_wire() {
let adapter = streaming_adapter(Some(TaskState::Completed), Duration::from_millis(150));
let (_, body) = rpc_send(&adapter, send_params("t-wire-false", Some(false))).await;
assert_eq!(state_of(&body), "TASK_STATE_COMPLETED");
}
#[tokio::test]
async fn return_immediately_true_is_read_from_the_wire() {
let adapter = streaming_adapter(Some(TaskState::Completed), Duration::from_secs(30));
let started = Instant::now();
let (_, body) = rpc_send(&adapter, send_params("t-wire-true", Some(true))).await;
assert_eq!(state_of(&body), "TASK_STATE_WORKING");
assert!(started.elapsed() < Duration::from_secs(5));
}