a2a_protocol_client/methods/
mod.rs1pub mod extended_card;
13pub mod push_config;
14pub mod send_message;
15pub mod tasks;
16
17impl crate::client::A2aClient {
18 pub(crate) fn tenant_or_default(&self, requested: Option<String>) -> Option<String> {
31 requested.or_else(|| self.config.tenant.clone())
32 }
33}
34
35#[cfg(test)]
38mod tests {
39 use std::collections::HashMap;
40 use std::future::Future;
41 use std::pin::Pin;
42 use std::sync::{Arc, Mutex};
43
44 use a2a_protocol_types::params::{ListPushConfigsParams, ListTasksParams, TaskQueryParams};
45 use a2a_protocol_types::push::TaskPushNotificationConfig;
46
47 use crate::error::{ClientError, ClientResult};
48 use crate::streaming::EventStream;
49 use crate::transport::Transport;
50 use crate::{A2aClient, ClientBuilder};
51
52 #[derive(Clone, Default)]
59 struct CapturingTransport {
60 calls: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
61 }
62
63 impl Transport for CapturingTransport {
64 fn send_request<'a>(
65 &'a self,
66 method: &'a str,
67 params: serde_json::Value,
68 _extra_headers: &'a HashMap<String, String>,
69 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>> {
70 self.calls
71 .lock()
72 .expect("calls lock")
73 .push((method.to_owned(), params));
74 Box::pin(async { Ok(serde_json::json!({})) })
75 }
76
77 fn send_streaming_request<'a>(
78 &'a self,
79 method: &'a str,
80 params: serde_json::Value,
81 _extra_headers: &'a HashMap<String, String>,
82 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
83 self.calls
84 .lock()
85 .expect("calls lock")
86 .push((method.to_owned(), params));
87 Box::pin(async { Err(ClientError::Transport("capture only".into())) })
88 }
89 }
90
91 fn client_with(transport: CapturingTransport, tenant: Option<&str>) -> A2aClient {
92 let builder = ClientBuilder::new("http://localhost:1").with_custom_transport(transport);
93 let builder = match tenant {
94 Some(t) => builder.with_tenant(t),
95 None => builder,
96 };
97 builder.build().expect("build client")
98 }
99
100 fn push_config(tenant: Option<&str>) -> TaskPushNotificationConfig {
101 TaskPushNotificationConfig {
102 tenant: tenant.map(str::to_owned),
103 ..TaskPushNotificationConfig::new("task-1", "https://hook.example/cb")
104 }
105 }
106
107 async fn drive_all(client: &A2aClient) -> HashMap<String, serde_json::Value> {
111 let _ = client
112 .get_task(TaskQueryParams {
113 tenant: None,
114 id: "task-1".into(),
115 history_length: None,
116 })
117 .await;
118 let _ = client.list_tasks(ListTasksParams::default()).await;
119 let _ = client.cancel_task("task-1").await;
120 let _ = client.subscribe_to_task("task-1").await;
121 let _ = client.set_push_config(push_config(None)).await;
122 let _ = client.get_push_config("task-1", "cfg-1").await;
123 let _ = client
124 .list_push_configs(ListPushConfigsParams {
125 tenant: None,
126 task_id: "task-1".into(),
127 page_size: None,
128 page_token: None,
129 })
130 .await;
131 let _ = client.delete_push_config("task-1", "cfg-1").await;
132 let _ = client.get_extended_agent_card().await;
133 HashMap::new()
134 }
135
136 const EXPECTED_METHODS: [&str; 9] = [
137 "GetTask",
138 "ListTasks",
139 "CancelTask",
140 "SubscribeToTask",
141 "CreateTaskPushNotificationConfig",
142 "GetTaskPushNotificationConfig",
143 "ListTaskPushNotificationConfigs",
144 "DeleteTaskPushNotificationConfig",
145 "GetExtendedAgentCard",
146 ];
147
148 #[tokio::test]
152 async fn card_tenant_is_sent_on_every_request() {
153 let transport = CapturingTransport::default();
154 let client = client_with(transport.clone(), Some("acme"));
155 drive_all(&client).await;
156
157 let calls = transport.calls.lock().expect("calls lock").clone();
158 let sent: Vec<&str> = calls.iter().map(|(m, _)| m.as_str()).collect();
159 assert_eq!(
160 sent, EXPECTED_METHODS,
161 "every method reached the transport once"
162 );
163 for (method, params) in &calls {
164 assert_eq!(
165 params.get("tenant").and_then(serde_json::Value::as_str),
166 Some("acme"),
167 "{method} must carry the card's tenant; sent params: {params}"
168 );
169 }
170 }
171
172 #[tokio::test]
176 async fn explicit_request_tenant_wins_over_client_default() {
177 let transport = CapturingTransport::default();
178 let client = client_with(transport.clone(), Some("acme"));
179 let _ = client
180 .get_task(TaskQueryParams {
181 tenant: Some("globex".into()),
182 id: "task-1".into(),
183 history_length: None,
184 })
185 .await;
186 let _ = client
187 .list_tasks(ListTasksParams {
188 tenant: Some("globex".into()),
189 ..ListTasksParams::default()
190 })
191 .await;
192 let _ = client.set_push_config(push_config(Some("globex"))).await;
193 let _ = client
194 .list_push_configs(ListPushConfigsParams {
195 tenant: Some("globex".into()),
196 task_id: "task-1".into(),
197 page_size: None,
198 page_token: None,
199 })
200 .await;
201
202 let calls = transport.calls.lock().expect("calls lock").clone();
203 assert_eq!(calls.len(), 4);
204 for (method, params) in &calls {
205 assert_eq!(
206 params.get("tenant").and_then(serde_json::Value::as_str),
207 Some("globex"),
208 "{method}: the per-request tenant must not be overwritten"
209 );
210 }
211 }
212
213 #[tokio::test]
217 async fn no_tenant_anywhere_leaves_the_field_absent() {
218 let transport = CapturingTransport::default();
219 let client = client_with(transport.clone(), None);
220 drive_all(&client).await;
221
222 let calls = transport.calls.lock().expect("calls lock").clone();
223 assert_eq!(calls.len(), EXPECTED_METHODS.len());
224 for (method, params) in &calls {
225 if method == "GetExtendedAgentCard" {
226 assert!(
227 params.is_null(),
228 "parameterless call stays null, got {params}"
229 );
230 } else {
231 assert!(
232 params.get("tenant").is_none(),
233 "{method} must omit tenant entirely, sent {params}"
234 );
235 }
236 }
237 }
238}