use axum::{
Json, Router,
routing::{get, post},
};
use codetether_agent::a2a::{
A2AClient,
types::{
AgentCapabilities, AgentCard, AgentSkill, JsonRpcRequest, JsonRpcResponse, Message,
MessageRole, MessageSendParams, Part, Task, TaskState, TaskStatus,
},
};
use serde_json::json;
use std::net::SocketAddr;
use tokio::net::TcpListener;
async fn create_mock_server() -> (SocketAddr, tokio::task::JoinHandle<()>) {
async fn agent_card() -> Json<AgentCard> {
Json(AgentCard {
name: "Test Agent".to_string(),
description: "A test agent for integration tests".to_string(),
url: "http://localhost:8080".to_string(),
version: "1.0.0".to_string(),
protocol_version: "0.3.0".to_string(),
preferred_transport: None,
additional_interfaces: vec![],
capabilities: AgentCapabilities {
streaming: false,
push_notifications: false,
state_transition_history: true,
extensions: vec![],
},
skills: vec![AgentSkill {
id: "test-skill".to_string(),
name: "Test Skill".to_string(),
description: "A test skill".to_string(),
tags: vec!["test".to_string()],
examples: vec!["Example 1".to_string()],
input_modes: vec!["text".to_string()],
output_modes: vec!["text".to_string()],
}],
default_input_modes: vec!["text".to_string()],
default_output_modes: vec!["text".to_string()],
provider: None,
icon_url: None,
documentation_url: None,
security_schemes: Default::default(),
security: vec![],
supports_authenticated_extended_card: false,
signatures: vec![],
})
}
async fn handle_rpc(Json(request): Json<JsonRpcRequest>) -> Json<JsonRpcResponse> {
match request.method.as_str() {
"message/send" => {
let task = Task {
id: "task-123".to_string(),
context_id: Some("ctx-456".to_string()),
status: TaskStatus {
state: TaskState::Completed,
message: Some(Message {
message_id: "msg-789".to_string(),
role: MessageRole::Agent,
parts: vec![Part::Text {
text: "Hello from test agent!".to_string(),
}],
context_id: None,
task_id: Some("task-123".to_string()),
metadata: Default::default(),
extensions: vec![],
}),
timestamp: Some("2024-01-01T00:00:00Z".to_string()),
},
artifacts: vec![],
history: vec![],
metadata: Default::default(),
};
Json(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(json!(task)),
error: None,
})
}
"tasks/get" => {
let task = Task {
id: "task-123".to_string(),
context_id: Some("ctx-456".to_string()),
status: TaskStatus {
state: TaskState::Completed,
message: None,
timestamp: Some("2024-01-01T00:00:00Z".to_string()),
},
artifacts: vec![],
history: vec![],
metadata: Default::default(),
};
Json(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(json!(task)),
error: None,
})
}
"tasks/cancel" => {
let task = Task {
id: "task-123".to_string(),
context_id: Some("ctx-456".to_string()),
status: TaskStatus {
state: TaskState::Cancelled,
message: None,
timestamp: Some("2024-01-01T00:00:00Z".to_string()),
},
artifacts: vec![],
history: vec![],
metadata: Default::default(),
};
Json(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(json!(task)),
error: None,
})
}
_ => Json(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(
codetether_agent::a2a::types::JsonRpcError::method_not_found(&request.method),
),
}),
}
}
let app = Router::new()
.route("/.well-known/agent.json", get(agent_card))
.route("/", post(handle_rpc));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let handle = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
(addr, handle)
}
#[tokio::test]
async fn test_a2a_client_get_agent_card() {
let (addr, _server_handle) = create_mock_server().await;
let client = A2AClient::new(format!("http://{}", addr));
let agent_card = client
.get_agent_card()
.await
.expect("Failed to get agent card");
assert_eq!(agent_card.name, "Test Agent");
assert_eq!(agent_card.version, "1.0.0");
assert_eq!(agent_card.protocol_version, "0.3.0");
assert_eq!(agent_card.skills.len(), 1);
assert_eq!(agent_card.skills[0].id, "test-skill");
}
#[tokio::test]
async fn test_a2a_client_send_message() {
let (addr, _server_handle) = create_mock_server().await;
let client = A2AClient::new(format!("http://{}", addr));
let message = Message {
message_id: "msg-001".to_string(),
role: MessageRole::User,
parts: vec![Part::Text {
text: "Hello, test agent!".to_string(),
}],
context_id: None,
task_id: None,
metadata: Default::default(),
extensions: vec![],
};
let params = MessageSendParams {
message,
configuration: None,
};
let _task = client
.send_message(params)
.await
.expect("Failed to send message");
}
#[tokio::test]
async fn test_a2a_client_get_task() {
let (addr, _server_handle) = create_mock_server().await;
let client = A2AClient::new(format!("http://{}", addr));
let task = client
.get_task("task-123", Some(10))
.await
.expect("Failed to get task");
assert_eq!(task.id, "task-123");
assert_eq!(task.status.state, TaskState::Completed);
}
#[tokio::test]
async fn test_a2a_client_cancel_task() {
let (addr, _server_handle) = create_mock_server().await;
let client = A2AClient::new(format!("http://{}", addr));
let task = client
.cancel_task("task-123")
.await
.expect("Failed to cancel task");
assert_eq!(task.id, "task-123");
assert_eq!(task.status.state, TaskState::Cancelled);
}
#[tokio::test]
async fn test_a2a_client_with_token() {
let (addr, _server_handle) = create_mock_server().await;
let client = A2AClient::new(format!("http://{}", addr)).with_token("test-token-123");
let agent_card = client
.get_agent_card()
.await
.expect("Failed to get agent card");
assert_eq!(agent_card.name, "Test Agent");
}
#[tokio::test]
async fn test_a2a_client_raw_rpc_call() {
let (addr, _server_handle) = create_mock_server().await;
let client = A2AClient::new(format!("http://{}", addr));
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
id: json!(42),
method: "message/send".to_string(),
params: json!({
"message": {
"messageId": "raw-msg",
"role": "user",
"parts": [{"kind": "text", "text": "Raw RPC test"}]
}
}),
};
let response = client
.call_rpc(request)
.await
.expect("Failed to make RPC call");
assert_eq!(response.jsonrpc, "2.0");
assert_eq!(response.id, json!(42));
assert!(response.result.is_some());
assert!(response.error.is_none());
}
#[tokio::test]
async fn test_a2a_client_base_url_normalization() {
let client = A2AClient::new("http://example.com/");
let _client_with_token = client.with_token("token");
}