use super::*;
#[tokio::test]
async fn unicode_emoji_in_messages() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let result = handler
.on_send_message(
make_send_params("Hello \u{1F600} world \u{1F310}"),
false,
None,
)
.await
.expect("send unicode message");
match result {
SendMessageResult::Response(SendMessageResponse::Task(task)) => {
assert_eq!(
task.status.state,
TaskState::Completed,
"unicode message task should complete successfully"
);
}
_ => panic!("expected Response(Task)"),
}
}
#[tokio::test]
async fn unicode_in_context_id() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let mut msg = make_message("test");
msg.context_id = Some(ContextId::new("ctx-\u{1F680}-rocket"));
let params = MessageSendParams {
tenant: None,
message: msg,
configuration: None,
metadata: None,
};
let result = handler.on_send_message(params, false, None).await;
if let Err(ref e) = result {
panic!("unicode in context_id should be accepted, got {e:?}");
}
}
#[tokio::test]
async fn large_page_size_clamped() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
handler
.on_send_message(make_send_params("one"), false, None)
.await
.expect("send first");
handler
.on_send_message(make_send_params("two"), false, None)
.await
.expect("send second");
let params = ListTasksParams {
tenant: None,
context_id: None,
status: None,
page_size: Some(u32::MAX),
page_token: None,
status_timestamp_after: None,
include_artifacts: None,
history_length: None,
};
let result = handler
.on_list_tasks(params, None)
.await
.expect("list tasks");
assert_eq!(
result.tasks.len(),
2,
"expected exactly 2 tasks from list, got {}",
result.tasks.len()
);
}
#[tokio::test]
async fn duplicate_task_id_rejected() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let result = handler
.on_send_message(make_send_params("first"), false, None)
.await
.expect("send first");
let task_id = match result {
SendMessageResult::Response(SendMessageResponse::Task(t)) => t.id.0.clone(),
_ => panic!("expected task"),
};
let mut msg = make_message("duplicate");
msg.task_id = Some(TaskId::new(&task_id));
let params = MessageSendParams {
tenant: None,
message: msg,
configuration: None,
metadata: None,
};
let err = unwrap_send_err(handler.on_send_message(params, false, None).await);
let err_msg = format!("{err:?}");
assert!(
matches!(err, a2a_protocol_server::ServerError::InvalidParams(_)),
"duplicate task_id should be rejected with InvalidParams, got {err_msg}"
);
}
#[tokio::test]
async fn push_config_per_task_limit() {
let store = InMemoryPushConfigStore::new();
for i in 0..100 {
let mut config = TaskPushNotificationConfig::new("task-limit", "https://hooks.example.com");
config.id = Some(format!("cfg-{i}"));
let result = store.set(config).await;
let saved = result.unwrap_or_else(|e| panic!("config {i} should succeed, got {e:?}"));
assert_eq!(
saved.id.as_deref(),
Some(format!("cfg-{i}").as_str()),
"saved config should preserve explicit ID"
);
}
let mut config = TaskPushNotificationConfig::new("task-limit", "https://hooks.example.com");
config.id = Some("cfg-overflow".into());
let err = store.set(config).await.unwrap_err();
assert_eq!(
err.code,
ErrorCode::InvalidParams,
"101st config should be rejected with InvalidParams, got {:?}",
err.code
);
assert!(
err.message.contains("limit exceeded"),
"error message should mention 'limit exceeded', got: {}",
err.message
);
}
#[tokio::test]
async fn oversized_event_rejected() {
use a2a_protocol_server::streaming::event_queue::new_in_memory_queue_with_options;
let (writer, _reader) =
new_in_memory_queue_with_options(8, 128, std::time::Duration::from_secs(5));
let big_text = "x".repeat(500);
let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: TaskId::new("big-task"),
context_id: ContextId::new("big-ctx"),
status: TaskStatus {
state: TaskState::Working,
message: Some(Message {
id: MessageId::new("m1"),
role: MessageRole::Agent,
parts: vec![Part::text(&big_text)],
task_id: None,
context_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
}),
timestamp: None,
},
metadata: None,
});
let err = writer.write(event).await.unwrap_err();
assert_eq!(
err.code,
ErrorCode::InternalError,
"oversized event should be rejected with InternalError, got {:?}",
err.code
);
assert!(
err.message.contains("exceeds maximum"),
"error should mention 'exceeds maximum', got: {}",
err.message
);
}
#[test]
fn builder_rejects_zero_timeout() {
let result = RequestHandlerBuilder::new(EchoExecutor)
.with_executor_timeout(Duration::ZERO)
.build();
assert!(result.is_err(), "zero timeout should be rejected");
let err = result.unwrap_err();
let err_msg = format!("{err:?}");
assert!(
matches!(err, a2a_protocol_server::ServerError::InvalidParams(_)),
"expected InvalidParams for zero timeout, got {err_msg}"
);
}
#[test]
fn builder_rejects_empty_interfaces() {
let mut card = minimal_agent_card();
card.supported_interfaces.clear();
let result = RequestHandlerBuilder::new(EchoExecutor)
.with_agent_card(card)
.build();
assert!(result.is_err(), "empty interfaces should be rejected");
let err = result.unwrap_err();
let err_msg = format!("{err:?}");
assert!(
matches!(err, a2a_protocol_server::ServerError::InvalidParams(_)),
"expected InvalidParams for empty interfaces, got {err_msg}"
);
}
#[test]
fn builder_accepts_valid_timeout() {
let result = RequestHandlerBuilder::new(EchoExecutor)
.with_executor_timeout(Duration::from_secs(30))
.build();
if let Err(ref e) = result {
panic!("valid timeout should be accepted, got {e:?}");
}
}