use a2a_protocol_types::*;
#[test]
fn protocol_constants() {
assert_eq!(A2A_VERSION, "1.0.0");
assert_eq!(A2A_CONTENT_TYPE, "application/a2a+json");
assert_eq!(A2A_VERSION_HEADER, "A2A-Version");
}
#[test]
fn deeply_nested_metadata_roundtrip() {
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
use a2a_protocol_types::task::ContextId;
let mut nested = serde_json::json!("leaf");
for _ in 0..50 {
nested = serde_json::json!({ "inner": nested });
}
let msg = Message {
id: MessageId::new("msg-nested"),
role: MessageRole::User,
parts: vec![Part::text("test")],
task_id: None,
context_id: Some(ContextId::new("ctx-nested")),
reference_task_ids: None,
extensions: None,
metadata: Some(nested.clone()),
};
let json = serde_json::to_string(&msg).expect("serialize deeply nested");
let back: Message = serde_json::from_str(&json).expect("deserialize deeply nested");
assert_eq!(back.metadata, Some(nested));
}
#[test]
fn agent_card_full_roundtrip() {
let card = AgentCard {
url: None,
name: "Test Agent".into(),
description: "A test agent for validation".into(),
version: "1.0.0".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:8080".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
default_input_modes: vec!["text/plain".into(), "application/json".into()],
default_output_modes: vec!["text/plain".into()],
skills: vec![AgentSkill {
id: "skill-1".into(),
name: "echo".into(),
description: "Echoes input".into(),
tags: vec!["demo".into()],
examples: Some(vec!["Hello!".into()]),
input_modes: Some(vec!["text/plain".into()]),
output_modes: Some(vec!["text/plain".into()]),
security_requirements: None,
}],
capabilities: AgentCapabilities::none()
.with_streaming(true)
.with_push_notifications(true)
.with_extended_agent_card(true),
provider: Some(AgentProvider {
organization: "Test Corp".into(),
url: "https://example.com".into(),
}),
icon_url: Some("https://example.com/icon.png".into()),
documentation_url: Some("https://example.com/docs".into()),
security_schemes: None,
security_requirements: None,
signatures: None,
};
let json = serde_json::to_string_pretty(&card).expect("serialize agent card");
let back: AgentCard = serde_json::from_str(&json).expect("deserialize agent card");
assert_eq!(back.name, "Test Agent");
assert_eq!(back.supported_interfaces.len(), 1);
assert_eq!(back.supported_interfaces[0].protocol_binding, "JSONRPC");
assert_eq!(back.supported_interfaces[0].url, "http://localhost:8080");
assert_eq!(back.skills.len(), 1);
assert_eq!(back.skills[0].id, "skill-1");
assert_eq!(back.skills[0].name, "echo");
assert!(back.provider.is_some());
let provider = back.provider.as_ref().unwrap();
assert_eq!(provider.organization, "Test Corp");
assert_eq!(provider.url, "https://example.com");
assert!(json.contains("\"defaultInputModes\""));
assert!(json.contains("\"defaultOutputModes\""));
assert!(json.contains("\"supportedInterfaces\""));
assert!(json.contains("\"protocolBinding\""));
assert!(json.contains("\"protocolVersion\""));
assert!(json.contains("\"pushNotifications\""));
assert!(json.contains("\"extendedAgentCard\""));
assert!(json.contains("\"iconUrl\""));
assert!(json.contains("\"documentationUrl\""));
}
#[test]
fn security_scheme_api_key_roundtrip() {
let scheme = SecurityScheme::ApiKey(ApiKeySecurityScheme {
location: ApiKeyLocation::Header,
name: "X-API-Key".into(),
description: None,
});
let json = serde_json::to_string(&scheme).expect("serialize");
let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
match back {
SecurityScheme::ApiKey(ref s) => {
assert_eq!(s.location, ApiKeyLocation::Header);
assert_eq!(s.name, "X-API-Key");
}
_ => panic!("expected ApiKey variant"),
}
}
#[test]
fn security_scheme_http_bearer_roundtrip() {
let scheme = SecurityScheme::Http(HttpAuthSecurityScheme {
scheme: "bearer".into(),
bearer_format: Some("JWT".into()),
description: None,
});
let json = serde_json::to_string(&scheme).expect("serialize");
assert!(json.contains("\"bearerFormat\""));
let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
match back {
SecurityScheme::Http(ref h) => {
assert_eq!(h.scheme, "bearer");
assert_eq!(h.bearer_format.as_deref(), Some("JWT"));
}
_ => panic!("expected Http variant"),
}
}
#[test]
fn security_scheme_oauth2_roundtrip() {
let scheme = SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
flows: OAuthFlows::AuthorizationCode(AuthorizationCodeFlow {
authorization_url: "https://example.com/auth".into(),
token_url: "https://example.com/token".into(),
refresh_url: None,
scopes: [("read".to_owned(), "Read access".to_owned())]
.into_iter()
.collect(),
pkce_required: Some(true),
}),
oauth2_metadata_url: None,
description: None,
}));
let json = serde_json::to_string(&scheme).expect("serialize");
assert!(json.contains("\"authorizationCode\""));
assert!(json.contains("\"authorizationUrl\""));
assert!(json.contains("\"tokenUrl\""));
assert!(json.contains("\"pkceRequired\""));
let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
match back {
SecurityScheme::OAuth2(ref o) => match &o.flows {
OAuthFlows::AuthorizationCode(ac) => {
assert_eq!(ac.authorization_url, "https://example.com/auth");
assert_eq!(ac.token_url, "https://example.com/token");
assert_eq!(ac.pkce_required, Some(true));
assert_eq!(
ac.scopes.get("read").map(String::as_str),
Some("Read access")
);
}
_ => panic!("expected AuthorizationCode flow"),
},
_ => panic!("expected OAuth2 variant"),
}
}
#[test]
fn security_scheme_openid_connect_roundtrip() {
let scheme = SecurityScheme::OpenIdConnect(OpenIdConnectSecurityScheme {
open_id_connect_url: "https://example.com/.well-known/openid-configuration".into(),
description: None,
});
let json = serde_json::to_string(&scheme).expect("serialize");
assert!(json.contains("\"openIdConnectUrl\""));
let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
match back {
SecurityScheme::OpenIdConnect(ref o) => {
assert_eq!(
o.open_id_connect_url,
"https://example.com/.well-known/openid-configuration"
);
}
_ => panic!("expected OpenIdConnect variant"),
}
}
#[test]
fn security_scheme_mutual_tls_roundtrip() {
let scheme = SecurityScheme::MutualTls(MutualTlsSecurityScheme { description: None });
let json = serde_json::to_string(&scheme).expect("serialize");
let back: SecurityScheme = serde_json::from_str(&json).expect("deserialize");
match back {
SecurityScheme::MutualTls(ref m) => {
assert!(m.description.is_none());
}
_ => panic!("expected MutualTls variant"),
}
}
#[test]
fn push_config_roundtrip() {
let config = TaskPushNotificationConfig::new("task-1", "https://example.com/webhook");
let json = serde_json::to_string(&config).expect("serialize");
assert!(json.contains("\"taskId\""));
assert!(json.contains("\"url\""));
let back: TaskPushNotificationConfig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.task_id, "task-1");
assert_eq!(back.url, "https://example.com/webhook");
}
#[test]
fn push_config_with_auth_roundtrip() {
let config = TaskPushNotificationConfig {
tenant: None,
id: Some("config-1".into()),
task_id: "task-1".into(),
url: "https://example.com/webhook".into(),
token: Some("shared-secret".into()),
authentication: Some(AuthenticationInfo {
scheme: "bearer".into(),
credentials: "my-token".into(),
}),
};
let json = serde_json::to_string(&config).expect("serialize");
assert!(json.contains("\"authentication\""));
assert!(json.contains("\"scheme\""));
assert!(json.contains("\"credentials\""));
let back: TaskPushNotificationConfig = serde_json::from_str(&json).expect("deserialize");
let auth = back
.authentication
.as_ref()
.expect("authentication should be present");
assert_eq!(auth.scheme, "bearer");
assert_eq!(auth.credentials, "my-token");
}
#[test]
fn stream_response_status_update_roundtrip() {
use a2a_protocol_types::events::*;
use a2a_protocol_types::task::*;
let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: TaskId::new("t1"),
context_id: ContextId::new("ctx1"),
status: TaskStatus {
state: TaskState::Working,
message: Some(Message {
id: MessageId::new("status-msg"),
role: message::MessageRole::Agent,
parts: vec![message::Part::text("Processing...")],
task_id: None,
context_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
}),
timestamp: Some("2026-01-01T00:00:00Z".into()),
},
metadata: None,
});
let json = serde_json::to_string(&event).expect("serialize");
let back: StreamResponse = serde_json::from_str(&json).expect("deserialize");
match back {
StreamResponse::StatusUpdate(ref e) => {
assert_eq!(e.task_id, TaskId::new("t1"));
assert_eq!(e.context_id, ContextId::new("ctx1"));
assert_eq!(e.status.state, TaskState::Working);
assert!(e.status.message.is_some());
assert_eq!(e.status.timestamp.as_deref(), Some("2026-01-01T00:00:00Z"));
}
_ => panic!("expected StatusUpdate"),
}
}
#[test]
fn stream_response_artifact_update_roundtrip() {
use a2a_protocol_types::events::*;
use a2a_protocol_types::task::*;
let event = StreamResponse::ArtifactUpdate(TaskArtifactUpdateEvent {
task_id: TaskId::new("t1"),
context_id: ContextId::new("ctx1"),
artifact: Artifact {
id: ArtifactId::new("art-1"),
name: Some("output.txt".into()),
description: Some("The output file".into()),
parts: vec![message::Part::text("content")],
extensions: None,
metadata: None,
},
append: Some(true),
last_chunk: Some(false),
metadata: Some(serde_json::json!({"chunk": 1})),
});
let json = serde_json::to_string(&event).expect("serialize");
assert!(json.contains("\"append\""));
assert!(json.contains("\"lastChunk\""));
assert!(json.contains("\"artifactId\""));
let back: StreamResponse = serde_json::from_str(&json).expect("deserialize");
match back {
StreamResponse::ArtifactUpdate(e) => {
assert_eq!(e.append, Some(true));
assert_eq!(e.last_chunk, Some(false));
assert_eq!(e.metadata, Some(serde_json::json!({"chunk": 1})));
}
_ => panic!("expected ArtifactUpdate"),
}
}
#[test]
fn stream_response_task_roundtrip() {
use a2a_protocol_types::events::*;
use a2a_protocol_types::task::*;
let event = StreamResponse::Task(Task {
id: TaskId::new("t1"),
context_id: ContextId::new("ctx1"),
status: TaskStatus::new(TaskState::Completed),
history: Some(vec![]),
artifacts: Some(vec![]),
metadata: None,
});
let json = serde_json::to_string(&event).expect("serialize");
let back: StreamResponse = serde_json::from_str(&json).expect("deserialize");
match back {
StreamResponse::Task(ref t) => {
assert_eq!(t.id, TaskId::new("t1"));
assert_eq!(t.context_id, ContextId::new("ctx1"));
assert_eq!(t.status.state, TaskState::Completed);
}
_ => panic!("expected Task variant"),
}
}
#[test]
fn stream_response_message_roundtrip() {
use a2a_protocol_types::events::*;
let event = StreamResponse::Message(message::Message {
id: MessageId::new("msg-1"),
role: message::MessageRole::Agent,
parts: vec![message::Part::text("hello")],
task_id: None,
context_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
});
let json = serde_json::to_string(&event).expect("serialize");
let back: StreamResponse = serde_json::from_str(&json).expect("deserialize");
match back {
StreamResponse::Message(ref m) => {
assert_eq!(m.id, MessageId::new("msg-1"));
assert_eq!(m.role, message::MessageRole::Agent);
assert_eq!(m.parts.len(), 1);
}
_ => panic!("expected Message variant"),
}
}
#[test]
fn jsonrpc_request_roundtrip() {
let req = JsonRpcRequest {
jsonrpc: jsonrpc::JsonRpcVersion,
method: "SendMessage".into(),
params: Some(serde_json::json!({"test": true})),
id: Some(serde_json::json!(1)),
};
let json = serde_json::to_string(&req).expect("serialize");
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(json.contains("\"method\":\"SendMessage\""));
let back: JsonRpcRequest = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.method, "SendMessage");
assert_eq!(back.id, Some(serde_json::json!(1)));
}
#[test]
fn jsonrpc_id_variants() {
let id: jsonrpc::JsonRpcId = Some(serde_json::json!(42));
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "42");
let id: jsonrpc::JsonRpcId = Some(serde_json::json!("req-1"));
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"req-1\"");
let id: jsonrpc::JsonRpcId = None;
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "null");
}
#[test]
fn jsonrpc_error_response_roundtrip() {
let resp = JsonRpcErrorResponse::new(
Some(serde_json::json!(1)),
JsonRpcError::new(-32600, "Invalid Request"),
);
let json = serde_json::to_string(&resp).expect("serialize");
assert!(json.contains("\"error\""));
assert!(json.contains("-32600"));
let back: JsonRpcErrorResponse = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.error.code, -32600);
}
#[test]
fn list_tasks_params_all_fields() {
use a2a_protocol_types::params::ListTasksParams;
let params = ListTasksParams {
tenant: Some("tenant-1".into()),
context_id: Some("ctx-1".into()),
status: Some(a2a_protocol_types::task::TaskState::Working),
page_size: Some(25),
page_token: Some("page-2".into()),
status_timestamp_after: Some("2026-01-01T00:00:00Z".into()),
include_artifacts: Some(true),
history_length: Some(10),
};
let json = serde_json::to_string(¶ms).expect("serialize");
assert!(json.contains("\"tenant\""));
assert!(json.contains("\"contextId\""));
assert!(json.contains("\"pageSize\""));
assert!(json.contains("\"pageToken\""));
assert!(json.contains("\"statusTimestampAfter\""));
assert!(json.contains("\"includeArtifacts\""));
assert!(json.contains("\"historyLength\""));
let back: ListTasksParams = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.page_size, Some(25));
assert_eq!(back.history_length, Some(10));
}
#[test]
fn a2a_error_display() {
let err = A2aError::new(ErrorCode::TaskNotFound, "task xyz not found");
let display = err.to_string();
assert!(display.contains("task xyz not found"), "display: {display}");
}
#[test]
fn a2a_error_with_data() {
let err = A2aError::with_data(
ErrorCode::InvalidParams,
"bad field",
serde_json::json!({"field": "name", "reason": "too long"}),
);
assert!(err.data.is_some());
let data = err.data.unwrap();
assert_eq!(data["field"], "name");
}
#[test]
fn a2a_error_convenience_constructors() {
let e1 = A2aError::parse_error("bad json");
assert_eq!(e1.code, ErrorCode::ParseError);
let e2 = A2aError::invalid_params("missing field");
assert_eq!(e2.code, ErrorCode::InvalidParams);
let e3 = A2aError::internal("server error");
assert_eq!(e3.code, ErrorCode::InternalError);
let e4 = A2aError::task_not_found(a2a_protocol_types::task::TaskId::new("t1"));
assert_eq!(e4.code, ErrorCode::TaskNotFound);
let e5 = A2aError::task_not_cancelable(a2a_protocol_types::task::TaskId::new("t1"));
assert_eq!(e5.code, ErrorCode::TaskNotCancelable);
}
#[test]
fn send_message_response_task_variant() {
use a2a_protocol_types::responses::SendMessageResponse;
use a2a_protocol_types::task::*;
let resp = SendMessageResponse::Task(Task {
id: TaskId::new("t1"),
context_id: ContextId::new("ctx"),
status: TaskStatus::new(TaskState::Completed),
history: None,
artifacts: None,
metadata: None,
});
let json = serde_json::to_string(&resp).expect("serialize");
let back: SendMessageResponse = serde_json::from_str(&json).expect("deserialize");
match back {
SendMessageResponse::Task(ref t) => {
assert_eq!(t.id, TaskId::new("t1"));
assert_eq!(t.context_id, ContextId::new("ctx"));
assert_eq!(t.status.state, TaskState::Completed);
}
_ => panic!("expected Task variant"),
}
}
#[test]
fn task_list_response_roundtrip() {
use a2a_protocol_types::responses::TaskListResponse;
use a2a_protocol_types::task::*;
let resp = TaskListResponse {
tasks: vec![Task {
id: TaskId::new("t1"),
context_id: ContextId::new("ctx"),
status: TaskStatus::new(TaskState::Completed),
history: None,
artifacts: None,
metadata: None,
}],
next_page_token: "next-page".into(),
page_size: 50,
total_size: 1,
};
let json = serde_json::to_string(&resp).expect("serialize");
assert!(json.contains("\"nextPageToken\""));
let back: TaskListResponse = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.tasks.len(), 1);
assert_eq!(back.tasks[0].id, TaskId::new("t1"));
assert_eq!(back.tasks[0].status.state, TaskState::Completed);
assert_eq!(back.next_page_token, "next-page");
}
#[test]
fn utc_now_iso8601_produces_valid_format() {
let ts = a2a_protocol_types::utc_now_iso8601();
assert_eq!(ts.len(), 20);
assert!(ts.ends_with('Z'));
assert_eq!(&ts[4..5], "-");
assert_eq!(&ts[7..8], "-");
assert_eq!(&ts[10..11], "T");
assert_eq!(&ts[13..14], ":");
assert_eq!(&ts[16..17], ":");
let year: u32 = ts[0..4].parse().unwrap();
let month: u32 = ts[5..7].parse().unwrap();
let day: u32 = ts[8..10].parse().unwrap();
let hour: u32 = ts[11..13].parse().unwrap();
let minute: u32 = ts[14..16].parse().unwrap();
let second: u32 = ts[17..19].parse().unwrap();
assert!(
(2026..=2100).contains(&year),
"year should be in [2026, 2100]: {year}"
);
assert!((1..=12).contains(&month), "invalid month: {month}");
assert!((1..=31).contains(&day), "invalid day: {day}");
assert!(hour < 24, "invalid hour: {hour}");
assert!(minute < 60, "invalid minute: {minute}");
assert!(second < 60, "invalid second: {second}");
}
#[test]
fn agent_extension_roundtrip() {
use a2a_protocol_types::extensions::AgentExtension;
let ext = AgentExtension {
uri: "https://example.com/extensions/custom".into(),
description: Some("A custom extension".into()),
required: Some(true),
params: Some(serde_json::json!({"key": "value"})),
};
let json = serde_json::to_string(&ext).expect("serialize");
let back: AgentExtension = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.uri, "https://example.com/extensions/custom");
assert_eq!(back.required, Some(true));
}