pub mod extended_card;
pub mod push_config;
pub mod send_message;
pub mod tasks;
impl crate::client::A2aClient {
pub(crate) fn tenant_or_default(&self, requested: Option<String>) -> Option<String> {
requested.or_else(|| self.config.tenant.clone())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use a2a_protocol_types::params::{ListPushConfigsParams, ListTasksParams, TaskQueryParams};
use a2a_protocol_types::push::TaskPushNotificationConfig;
use crate::error::{ClientError, ClientResult};
use crate::streaming::EventStream;
use crate::transport::Transport;
use crate::{A2aClient, ClientBuilder};
#[derive(Clone, Default)]
struct CapturingTransport {
calls: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
}
impl Transport for CapturingTransport {
fn send_request<'a>(
&'a self,
method: &'a str,
params: serde_json::Value,
_extra_headers: &'a HashMap<String, String>,
) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>> {
self.calls
.lock()
.expect("calls lock")
.push((method.to_owned(), params));
Box::pin(async { Ok(serde_json::json!({})) })
}
fn send_streaming_request<'a>(
&'a self,
method: &'a str,
params: serde_json::Value,
_extra_headers: &'a HashMap<String, String>,
) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
self.calls
.lock()
.expect("calls lock")
.push((method.to_owned(), params));
Box::pin(async { Err(ClientError::Transport("capture only".into())) })
}
}
fn client_with(transport: CapturingTransport, tenant: Option<&str>) -> A2aClient {
let builder = ClientBuilder::new("http://localhost:1").with_custom_transport(transport);
let builder = match tenant {
Some(t) => builder.with_tenant(t),
None => builder,
};
builder.build().expect("build client")
}
fn push_config(tenant: Option<&str>) -> TaskPushNotificationConfig {
TaskPushNotificationConfig {
tenant: tenant.map(str::to_owned),
..TaskPushNotificationConfig::new("task-1", "https://hook.example/cb")
}
}
async fn drive_all(client: &A2aClient) -> HashMap<String, serde_json::Value> {
let _ = client
.get_task(TaskQueryParams {
tenant: None,
id: "task-1".into(),
history_length: None,
})
.await;
let _ = client.list_tasks(ListTasksParams::default()).await;
let _ = client.cancel_task("task-1").await;
let _ = client.subscribe_to_task("task-1").await;
let _ = client.set_push_config(push_config(None)).await;
let _ = client.get_push_config("task-1", "cfg-1").await;
let _ = client
.list_push_configs(ListPushConfigsParams {
tenant: None,
task_id: "task-1".into(),
page_size: None,
page_token: None,
})
.await;
let _ = client.delete_push_config("task-1", "cfg-1").await;
let _ = client.get_extended_agent_card().await;
HashMap::new()
}
const EXPECTED_METHODS: [&str; 9] = [
"GetTask",
"ListTasks",
"CancelTask",
"SubscribeToTask",
"CreateTaskPushNotificationConfig",
"GetTaskPushNotificationConfig",
"ListTaskPushNotificationConfigs",
"DeleteTaskPushNotificationConfig",
"GetExtendedAgentCard",
];
#[tokio::test]
async fn card_tenant_is_sent_on_every_request() {
let transport = CapturingTransport::default();
let client = client_with(transport.clone(), Some("acme"));
drive_all(&client).await;
let calls = transport.calls.lock().expect("calls lock").clone();
let sent: Vec<&str> = calls.iter().map(|(m, _)| m.as_str()).collect();
assert_eq!(
sent, EXPECTED_METHODS,
"every method reached the transport once"
);
for (method, params) in &calls {
assert_eq!(
params.get("tenant").and_then(serde_json::Value::as_str),
Some("acme"),
"{method} must carry the card's tenant; sent params: {params}"
);
}
}
#[tokio::test]
async fn explicit_request_tenant_wins_over_client_default() {
let transport = CapturingTransport::default();
let client = client_with(transport.clone(), Some("acme"));
let _ = client
.get_task(TaskQueryParams {
tenant: Some("globex".into()),
id: "task-1".into(),
history_length: None,
})
.await;
let _ = client
.list_tasks(ListTasksParams {
tenant: Some("globex".into()),
..ListTasksParams::default()
})
.await;
let _ = client.set_push_config(push_config(Some("globex"))).await;
let _ = client
.list_push_configs(ListPushConfigsParams {
tenant: Some("globex".into()),
task_id: "task-1".into(),
page_size: None,
page_token: None,
})
.await;
let calls = transport.calls.lock().expect("calls lock").clone();
assert_eq!(calls.len(), 4);
for (method, params) in &calls {
assert_eq!(
params.get("tenant").and_then(serde_json::Value::as_str),
Some("globex"),
"{method}: the per-request tenant must not be overwritten"
);
}
}
#[tokio::test]
async fn no_tenant_anywhere_leaves_the_field_absent() {
let transport = CapturingTransport::default();
let client = client_with(transport.clone(), None);
drive_all(&client).await;
let calls = transport.calls.lock().expect("calls lock").clone();
assert_eq!(calls.len(), EXPECTED_METHODS.len());
for (method, params) in &calls {
if method == "GetExtendedAgentCard" {
assert!(
params.is_null(),
"parameterless call stays null, got {params}"
);
} else {
assert!(
params.get("tenant").is_none(),
"{method} must omit tenant entirely, sent {params}"
);
}
}
}
}