use super::*;
#[tokio::test]
async fn resubscribe_nonexistent_task_fails() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let params = TaskIdParams {
tenant: None,
id: "nonexistent".into(),
};
let err = handler.on_resubscribe(params, None).await.unwrap_err();
assert!(
matches!(err, a2a_protocol_server::ServerError::TaskNotFound(ref id) if id.0.as_str() == "nonexistent"),
"expected TaskNotFound for 'nonexistent', got {err:?}"
);
}
#[tokio::test]
async fn return_immediately_returns_pending_task() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let params = MessageSendParams {
tenant: None,
message: make_message("hello"),
configuration: Some(a2a_protocol_types::params::SendMessageConfiguration {
accepted_output_modes: vec!["text/plain".into()],
task_push_notification_config: None,
history_length: None,
return_immediately: Some(true),
}),
metadata: None,
};
let result = handler
.on_send_message(params, false, None)
.await
.expect("send message");
match result {
SendMessageResult::Response(SendMessageResponse::Task(task)) => {
assert_eq!(
task.status.state,
TaskState::Submitted,
"return_immediately should return Submitted task, got {:?}",
task.status.state
);
}
_ => panic!("expected Response(Task)"),
}
}
#[tokio::test]
async fn task_continuation_same_context_finds_stored_task() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let mut msg1 = make_message("first");
msg1.context_id = Some(a2a_protocol_types::task::ContextId::new("ctx-continuation"));
let result1 = handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg1,
configuration: None,
metadata: None,
},
false,
None,
)
.await
.expect("first send");
let task_id_1 = match result1 {
SendMessageResult::Response(SendMessageResponse::Task(t)) => t.id.clone(),
_ => panic!("expected task"),
};
let mut msg2 = make_message("second");
msg2.context_id = Some(a2a_protocol_types::task::ContextId::new("ctx-continuation"));
let result2 = handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg2,
configuration: None,
metadata: None,
},
false,
None,
)
.await
.expect("second send");
let task_id_2 = match result2 {
SendMessageResult::Response(SendMessageResponse::Task(t)) => t.id.clone(),
_ => panic!("expected task"),
};
assert_ne!(task_id_1, task_id_2, "second send should create a new task");
let list = handler
.on_list_tasks(
ListTasksParams {
tenant: None,
context_id: Some("ctx-continuation".into()),
status: None,
page_size: None,
page_token: None,
status_timestamp_after: None,
include_artifacts: None,
history_length: None,
},
None,
)
.await
.expect("list tasks");
assert_eq!(
list.tasks.len(),
2,
"should have exactly 2 tasks for the context, got {}",
list.tasks.len()
);
}
struct InputRequiredExecutor;
impl a2a_protocol_server::executor::AgentExecutor for InputRequiredExecutor {
fn execute<'a>(
&'a self,
ctx: &'a a2a_protocol_server::request_context::RequestContext,
queue: &'a dyn a2a_protocol_server::streaming::EventQueueWriter,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = a2a_protocol_types::A2aResult<()>> + Send + 'a>,
> {
Box::pin(async move {
for state in [TaskState::Working, TaskState::InputRequired] {
queue
.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: ctx.task_id.clone(),
context_id: ContextId::new(ctx.context_id.clone()),
status: TaskStatus::new(state),
metadata: None,
}))
.await?;
}
Ok(())
})
}
}
#[tokio::test]
async fn task_id_only_continuation_infers_context_from_task() {
let handler = RequestHandlerBuilder::new(InputRequiredExecutor)
.build()
.expect("build handler");
let mut msg1 = make_message("first");
msg1.context_id = Some(a2a_protocol_types::task::ContextId::new("ctx-infer"));
let result1 = handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg1,
configuration: None,
metadata: None,
},
false,
None,
)
.await
.expect("first send");
let task1 = match result1 {
SendMessageResult::Response(SendMessageResponse::Task(t)) => t,
_ => panic!("expected task"),
};
assert_eq!(task1.status.state, TaskState::InputRequired);
let mut msg2 = make_message("second");
msg2.task_id = Some(task1.id.clone());
let result2 = handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg2,
configuration: None,
metadata: None,
},
false,
None,
)
.await
.expect("taskId-only continuation must succeed");
let task2 = match result2 {
SendMessageResult::Response(SendMessageResponse::Task(t)) => t,
_ => panic!("expected task"),
};
assert_eq!(task2.id, task1.id, "continuation must reuse the task");
assert_eq!(
task2.context_id.0.as_str(),
"ctx-infer",
"contextId must be inferred from the referenced task"
);
}
#[tokio::test]
async fn task_id_only_unknown_task_returns_task_not_found() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let mut msg = make_message("hello");
msg.task_id = Some(TaskId::new("no-such-task"));
let err = handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg,
configuration: None,
metadata: None,
},
false,
None,
)
.await
.expect_err("unknown taskId must fail");
assert!(
matches!(err, a2a_protocol_server::ServerError::TaskNotFound(ref id) if id.0.as_str() == "no-such-task"),
"expected TaskNotFound, got {err:?}"
);
}
#[tokio::test]
async fn context_task_mismatch_rejected() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let mut msg1 = make_message("first");
msg1.context_id = Some(a2a_protocol_types::task::ContextId::new("ctx-mismatch"));
handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg1,
configuration: None,
metadata: None,
},
false,
None,
)
.await
.expect("first send");
let mut msg2 = make_message("second");
msg2.context_id = Some(a2a_protocol_types::task::ContextId::new("ctx-mismatch"));
msg2.task_id = Some(TaskId::new("wrong-task-id"));
let result = handler
.on_send_message(
MessageSendParams {
tenant: None,
message: msg2,
configuration: None,
metadata: None,
},
false,
None,
)
.await;
let err = result.expect_err("expected error for task_id mismatch");
assert!(
matches!(err, a2a_protocol_server::ServerError::InvalidParams(ref msg) if msg.contains("task") && msg.contains("match")),
"expected InvalidParams for task_id mismatch, got {err:?}"
);
}
#[tokio::test]
async fn interceptor_rejection_stops_processing() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.with_interceptor(RejectInterceptor)
.build()
.expect("build handler");
let result = handler
.on_send_message(make_send_params("hello"), false, None)
.await;
let err = result.expect_err("expected error from interceptor");
assert!(
matches!(err, a2a_protocol_server::ServerError::Protocol(ref a2a_err) if a2a_err.message.contains("rejected by interceptor")),
"expected Protocol error with 'rejected by interceptor' message, got {err:?}"
);
}
#[test]
fn server_error_to_a2a_error_mapping() {
use a2a_protocol_server::ServerError;
use a2a_protocol_types::error::ErrorCode;
let cases = vec![
(
ServerError::TaskNotFound(TaskId::new("t1")),
ErrorCode::TaskNotFound,
"t1",
),
(
ServerError::TaskNotCancelable(TaskId::new("t1")),
ErrorCode::TaskNotCancelable,
"t1",
),
(
ServerError::InvalidParams("bad".into()),
ErrorCode::InvalidParams,
"bad",
),
(
ServerError::PushNotSupported,
ErrorCode::PushNotificationNotSupported,
"",
),
(
ServerError::MethodNotFound("Foo".into()),
ErrorCode::MethodNotFound,
"Foo",
),
(
ServerError::Internal("oops".into()),
ErrorCode::InternalError,
"oops",
),
];
for (server_err, expected_code, expected_substr) in cases {
let display_msg = format!("{server_err}");
let a2a_err = server_err.to_a2a_error();
assert_eq!(
a2a_err.code, expected_code,
"mapping mismatch: display was '{display_msg}'"
);
if !expected_substr.is_empty() {
assert!(
a2a_err.message.contains(expected_substr),
"expected error message to contain '{expected_substr}', got '{}'",
a2a_err.message
);
}
}
}