use super::*;
#[test]
fn utc_now_iso8601_returns_valid_iso_format() {
let ts = a2a_protocol_types::utc_now_iso8601();
assert!(ts.ends_with('Z'), "timestamp should end with Z: {ts}");
assert_eq!(ts.len(), 20, "expected 20-char ISO 8601 string: {ts}");
assert_eq!(
ts.as_bytes()[10],
b'T',
"expected T separator at position 10: {ts}"
);
assert_eq!(ts.as_bytes()[4], b'-', "expected dash at position 4: {ts}");
assert_eq!(ts.as_bytes()[7], b'-', "expected dash at position 7: {ts}");
assert_eq!(
ts.as_bytes()[13],
b':',
"expected colon at position 13: {ts}"
);
assert_eq!(
ts.as_bytes()[16],
b':',
"expected colon at position 16: {ts}"
);
for (i, ch) in ts.chars().enumerate() {
if [4, 7, 10, 13, 16, 19].contains(&i) {
continue; }
assert!(ch.is_ascii_digit(), "expected digit at position {i}: {ts}");
}
}
#[test]
fn a2a_error_internal_constructor() {
let err = A2aError::internal("something broke");
assert_eq!(err.code, ErrorCode::InternalError);
assert_eq!(err.message, "something broke");
}
#[test]
fn a2a_error_invalid_params_constructor() {
let err = A2aError::invalid_params("bad param");
assert_eq!(err.code, ErrorCode::InvalidParams);
assert_eq!(err.message, "bad param");
}
#[test]
fn a2a_error_unsupported_operation_constructor() {
let err = A2aError::unsupported_operation("not supported");
assert_eq!(err.code, ErrorCode::UnsupportedOperation);
assert_eq!(err.message, "not supported");
}
#[test]
fn a2a_error_parse_error_constructor() {
let err = A2aError::parse_error("bad json");
assert_eq!(err.code, ErrorCode::ParseError);
assert_eq!(err.message, "bad json");
}
#[test]
fn a2a_error_invalid_agent_response_constructor() {
let err = A2aError::invalid_agent_response("bad response");
assert_eq!(err.code, ErrorCode::InvalidAgentResponse);
assert_eq!(err.message, "bad response");
}
#[test]
fn a2a_error_extended_card_not_configured_constructor() {
let err = A2aError::extended_card_not_configured("no card");
assert_eq!(err.code, ErrorCode::ExtendedAgentCardNotConfigured);
assert_eq!(err.message, "no card");
}
#[test]
fn artifact_new_construction() {
let artifact = Artifact::new(
"test-artifact",
vec![Part::text("hello"), Part::text("world")],
);
assert_eq!(artifact.id.0, "test-artifact");
assert_eq!(artifact.parts.len(), 2);
assert!(artifact.name.is_none(), "name should default to None");
assert!(
artifact.description.is_none(),
"description should default to None"
);
assert!(
artifact.extensions.is_none(),
"extensions should default to None"
);
assert!(
artifact.metadata.is_none(),
"metadata should default to None"
);
}
#[test]
fn artifact_validate_rejects_empty_parts() {
let artifact = Artifact {
id: a2a_protocol_types::artifact::ArtifactId::new("empty"),
name: None,
description: None,
parts: vec![],
extensions: None,
metadata: None,
};
assert!(
artifact.validate().is_err(),
"empty parts should fail validation"
);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "Artifact parts must not be empty")]
fn artifact_new_empty_parts_panics_in_debug() {
let _artifact = Artifact::new("empty", vec![]);
}
#[test]
fn push_config_new_construction() {
let config = TaskPushNotificationConfig::new("task-42", "https://hooks.example.com/notify");
assert_eq!(config.task_id, "task-42");
assert_eq!(config.url, "https://hooks.example.com/notify");
assert!(config.id.is_none(), "id should default to None");
assert!(config.tenant.is_none(), "tenant should default to None");
assert!(config.token.is_none(), "token should default to None");
assert!(
config.authentication.is_none(),
"authentication should default to None"
);
}
#[tokio::test]
async fn executor_timeout_causes_failure() {
let handler = RequestHandlerBuilder::new(SlowExecutor)
.with_executor_timeout(Duration::from_millis(50))
.build()
.expect("build handler");
let result = handler
.on_send_message(make_send_params("slow"), false, None)
.await;
match result {
Ok(SendMessageResult::Response(SendMessageResponse::Task(task))) => {
assert_eq!(
task.status.state,
TaskState::Failed,
"timed-out task should be Failed"
);
}
Err(_) => {
}
_ => panic!("expected failed task or error from timeout"),
}
}
#[tokio::test]
async fn shutdown_cancels_in_flight_tasks() {
let handler = Arc::new(
RequestHandlerBuilder::new(SlowExecutor)
.build()
.expect("build handler"),
);
let result = handler
.on_send_message(make_send_params("slow-work"), true, None)
.await
.expect("send streaming message");
assert!(
matches!(result, SendMessageResult::Stream(_)),
"expected Stream variant from streaming send"
);
tokio::time::sleep(Duration::from_millis(50)).await;
handler.shutdown().await;
if let SendMessageResult::Stream(mut reader) = result {
let mut event_count = 0;
while let Some(_event) = reader.read().await {
event_count += 1;
if event_count > 100 {
panic!("stream did not close after shutdown");
}
}
}
}
#[tokio::test]
async fn get_extended_agent_card_configured_with_capability() {
use a2a_protocol_types::agent_card::AgentCapabilities;
let mut card = minimal_agent_card();
card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
let handler = RequestHandlerBuilder::new(EchoExecutor)
.with_agent_card(card)
.build()
.expect("build handler");
let card = handler
.on_get_extended_agent_card(None)
.await
.expect("get agent card");
assert_eq!(card.name, "Test Agent");
assert_eq!(card.version, "1.0.0");
}
#[tokio::test]
async fn get_extended_agent_card_without_capability_returns_unsupported() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.with_agent_card(minimal_agent_card()) .build()
.expect("build handler");
let err = handler.on_get_extended_agent_card(None).await.unwrap_err();
assert!(
matches!(
err,
a2a_protocol_server::ServerError::UnsupportedOperation(_)
),
"expected UnsupportedOperation when capability not set, got {err:?}"
);
}
#[tokio::test]
async fn get_extended_agent_card_unconfigured() {
let handler = RequestHandlerBuilder::new(EchoExecutor)
.build()
.expect("build handler");
let err = handler.on_get_extended_agent_card(None).await.unwrap_err();
assert!(
matches!(err, a2a_protocol_server::ServerError::Protocol(ref e) if e.code == a2a_protocol_types::error::ErrorCode::ExtendedAgentCardNotConfigured),
"expected ExtendedAgentCardNotConfigured error, got {err:?}"
);
}
#[test]
fn cors_config_permissive_returns_valid_config() {
let cors = a2a_protocol_server::dispatch::CorsConfig::permissive();
assert_eq!(cors.allow_origin, "*");
assert!(
!cors.allow_methods.is_empty(),
"permissive CORS should have at least one allowed method"
);
assert!(
!cors.allow_headers.is_empty(),
"permissive CORS should have at least one allowed header"
);
assert!(
cors.max_age_secs > 0,
"permissive CORS max_age_secs should be positive, got {}",
cors.max_age_secs
);
}
#[test]
fn cors_config_new_with_specific_origin() {
let cors = a2a_protocol_server::dispatch::CorsConfig::new("https://example.com");
assert_eq!(cors.allow_origin, "https://example.com");
}
#[tokio::test]
async fn event_queue_manager_with_capacity() {
let mgr = EventQueueManager::with_capacity(8);
let task_id = TaskId::new("cap-test");
let (writer, reader) = mgr.get_or_create(&task_id).await;
assert!(reader.is_some(), "first call should return a reader");
writer
.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: task_id.clone(),
context_id: ContextId::new("ctx"),
status: TaskStatus::new(TaskState::Working),
metadata: None,
}))
.await
.expect("write event");
let mut reader = reader.unwrap();
let event = reader.read().await;
assert!(event.is_some(), "should read back the written event");
assert_eq!(
mgr.active_count().await,
1,
"expected exactly one active queue"
);
}
#[tokio::test]
async fn event_queue_manager_destroy_all_clears() {
let mgr = EventQueueManager::new();
let t1 = TaskId::new("t1");
let t2 = TaskId::new("t2");
let t3 = TaskId::new("t3");
mgr.get_or_create(&t1).await;
mgr.get_or_create(&t2).await;
mgr.get_or_create(&t3).await;
assert_eq!(
mgr.active_count().await,
3,
"expected 3 active queues after creation"
);
mgr.destroy_all().await;
assert_eq!(
mgr.active_count().await,
0,
"destroy_all should clear all queues"
);
}
#[test]
fn request_context_with_stored_task() {
let msg = make_message("test");
let ctx = RequestContext::new(msg, TaskId::new("task-1"), "ctx-1".into());
assert!(
ctx.stored_task.is_none(),
"stored_task should initially be None"
);
let stored = a2a_protocol_types::task::Task {
id: TaskId::new("old-task"),
context_id: ContextId::new("ctx-1"),
status: TaskStatus::new(TaskState::Completed),
history: None,
artifacts: None,
metadata: None,
};
let ctx = ctx.with_stored_task(stored.clone());
assert!(
ctx.stored_task.is_some(),
"stored_task should be Some after with_stored_task"
);
assert_eq!(
ctx.stored_task.as_ref().unwrap().id,
TaskId::new("old-task"),
"stored task ID should match"
);
}
#[test]
fn request_context_with_metadata() {
let msg = make_message("test");
let ctx = RequestContext::new(msg, TaskId::new("task-1"), "ctx-1".into());
assert!(ctx.metadata.is_none(), "metadata should initially be None");
let meta = serde_json::json!({"key": "value"});
let ctx = ctx.with_metadata(meta.clone());
assert_eq!(
ctx.metadata,
Some(meta),
"metadata should match after with_metadata"
);
}
#[test]
fn call_context_with_extensions() {
let ctx = CallContext::new("test/method");
assert!(
ctx.extensions().is_empty(),
"extensions should initially be empty"
);
let ctx = ctx.with_extensions(vec!["urn:a2a:ext:foo".into(), "urn:a2a:ext:bar".into()]);
assert_eq!(
ctx.extensions().len(),
2,
"expected exactly 2 extensions after with_extensions"
);
assert_eq!(ctx.extensions()[0], "urn:a2a:ext:foo");
assert_eq!(ctx.extensions()[1], "urn:a2a:ext:bar");
}
#[test]
fn call_context_with_caller_identity() {
let ctx = CallContext::new("SendMessage").with_caller_identity("user@example.com".into());
assert_eq!(
ctx.caller_identity(),
Some("user@example.com"),
"caller_identity should match after with_caller_identity"
);
}