1use a2a::*;
4use async_trait::async_trait;
5use futures::{StreamExt, stream::BoxStream};
6use std::{collections::HashMap, sync::Arc};
7use tokio::sync::{RwLock, broadcast};
8
9use crate::middleware::ServiceParams;
10
11const EXECUTION_BUFFER_CAPACITY: usize = 32;
12
13#[derive(Clone)]
14struct ExecutionEvent {
15 sequence: u64,
16 result: Result<StreamResponse, A2AError>,
17}
18
19#[derive(Default)]
20struct ActiveExecutionState {
21 sequence: u64,
22 snapshot_task: Option<Task>,
23}
24
25struct ActiveExecution {
26 sender: broadcast::Sender<ExecutionEvent>,
27 state: RwLock<ActiveExecutionState>,
28}
29
30impl ActiveExecution {
31 fn new(task: Task) -> Arc<Self> {
32 let (sender, _) = broadcast::channel(EXECUTION_BUFFER_CAPACITY);
33 Arc::new(Self {
34 sender,
35 state: RwLock::new(ActiveExecutionState {
36 sequence: 0,
37 snapshot_task: Some(task),
38 }),
39 })
40 }
41
42 fn subscribe(&self) -> broadcast::Receiver<ExecutionEvent> {
43 self.sender.subscribe()
44 }
45
46 async fn resubscribe(&self) -> (broadcast::Receiver<ExecutionEvent>, Option<Task>, u64) {
47 let receiver = self.sender.subscribe();
48 let state = self.state.read().await;
49 (receiver, state.snapshot_task.clone(), state.sequence)
50 }
51
52 async fn publish(&self, result: Result<StreamResponse, A2AError>, snapshot_task: Option<Task>) {
53 let event = {
54 let mut state = self.state.write().await;
55 if let Some(task) = snapshot_task {
56 state.snapshot_task = Some(task);
57 }
58 state.sequence += 1;
59 ExecutionEvent {
60 sequence: state.sequence,
61 result,
62 }
63 };
64 let _ = self.sender.send(event);
65 }
66
67 async fn snapshot_is_terminal(&self) -> bool {
68 let state = self.state.read().await;
69 state
70 .snapshot_task
71 .as_ref()
72 .is_some_and(|task| task.status.state.is_terminal())
73 }
74}
75
76#[derive(Default)]
77struct ExecutionManager {
78 executions: RwLock<HashMap<TaskId, Arc<ActiveExecution>>>,
79}
80
81impl ExecutionManager {
82 async fn start(&self, task: Task) -> Result<Arc<ActiveExecution>, A2AError> {
83 let mut executions = self.executions.write().await;
84 if executions.contains_key(&task.id) {
85 return Err(A2AError::invalid_request(format!(
86 "task execution is already in progress: {}",
87 task.id
88 )));
89 }
90
91 let active = ActiveExecution::new(task.clone());
92 executions.insert(task.id.clone(), active.clone());
93 Ok(active)
94 }
95
96 async fn get(&self, task_id: &str) -> Option<Arc<ActiveExecution>> {
97 let executions = self.executions.read().await;
98 executions.get(task_id).cloned()
99 }
100
101 async fn resubscribe(
102 &self,
103 task_id: &str,
104 ) -> Option<(broadcast::Receiver<ExecutionEvent>, Option<Task>, u64)> {
105 let active = self.get(task_id).await?;
106 let (receiver, snapshot_task, sequence) = active.resubscribe().await;
107 Some((receiver, snapshot_task, sequence))
108 }
109
110 async fn finish(&self, task_id: &str, active: &Arc<ActiveExecution>) {
111 let mut executions = self.executions.write().await;
112 let should_remove = executions
113 .get(task_id)
114 .is_some_and(|current| Arc::ptr_eq(current, active));
115 if should_remove {
116 executions.remove(task_id);
117 }
118 }
119}
120
121struct ExecutionRuntime {
122 executor: Arc<dyn crate::AgentExecutor>,
123 task_store: Arc<dyn crate::TaskStore>,
124 push_config_store: Option<Arc<dyn crate::PushConfigStore>>,
125 push_sender: Option<Arc<crate::HttpPushSender>>,
126 execution_manager: Arc<ExecutionManager>,
127}
128
129struct SubscriptionState {
130 receiver: broadcast::Receiver<ExecutionEvent>,
131 snapshot_task: Option<Task>,
132 min_sequence: u64,
133 done: bool,
134}
135
136fn is_terminal_event(event: &StreamResponse) -> bool {
137 match event {
138 StreamResponse::Task(task) => task.status.state.is_terminal(),
139 StreamResponse::StatusUpdate(update) => update.status.state.is_terminal(),
140 _ => false,
141 }
142}
143
144fn task_id_for_event(event: &StreamResponse) -> Option<&str> {
145 match event {
146 StreamResponse::Task(task) => Some(&task.id),
147 StreamResponse::StatusUpdate(update) => Some(&update.task_id),
148 StreamResponse::ArtifactUpdate(update) => Some(&update.task_id),
149 StreamResponse::Message(_) => None,
150 }
151}
152
153fn should_interrupt_non_streaming(
154 req: &SendMessageRequest,
155 event: &StreamResponse,
156) -> Option<TaskId> {
157 if req
158 .configuration
159 .as_ref()
160 .and_then(|config| config.return_immediately)
161 .unwrap_or(false)
162 {
163 return match event {
164 StreamResponse::Message(_) => None,
165 _ => task_id_for_event(event).map(ToOwned::to_owned),
166 };
167 }
168
169 match event {
170 StreamResponse::Task(task) if task.status.state == TaskState::AuthRequired => {
171 Some(task.id.clone())
172 }
173 StreamResponse::StatusUpdate(update) if update.status.state == TaskState::AuthRequired => {
174 Some(update.task_id.clone())
175 }
176 _ => None,
177 }
178}
179
180async fn send_push_notifications(
181 task_id: &str,
182 push_config_store: Option<&dyn crate::PushConfigStore>,
183 push_sender: Option<&crate::HttpPushSender>,
184 event: &StreamResponse,
185) -> Result<(), A2AError> {
186 let Some(push_config_store) = push_config_store else {
187 return Ok(());
188 };
189 let Some(push_sender) = push_sender else {
190 return Ok(());
191 };
192
193 let configs = push_config_store.list(task_id).await?;
194 for config in configs {
195 push_sender.send_push(&config, event.clone()).await?;
196 }
197
198 Ok(())
199}
200
201async fn save_task(task_store: &dyn crate::TaskStore, task: Task) -> Result<Task, A2AError> {
202 match task_store.update(task.clone()).await {
203 Ok(_) => Ok(task),
204 Err(error) if error.code == error_code::TASK_NOT_FOUND => {
205 task_store.create(task.clone()).await?;
206 Ok(task)
207 }
208 Err(error) => Err(error),
209 }
210}
211
212async fn apply_event_to_task(
213 task_store: &dyn crate::TaskStore,
214 current_task: Option<Task>,
215 event: &StreamResponse,
216) -> Result<Option<Task>, A2AError> {
217 match event {
218 StreamResponse::Task(task) => save_task(task_store, task.clone()).await.map(Some),
219 StreamResponse::StatusUpdate(update) => {
220 let mut task = current_task
221 .or(task_store.get(&update.task_id).await?)
222 .ok_or_else(|| A2AError::task_not_found(&update.task_id))?;
223 task.status = update.status.clone();
224 save_task(task_store, task).await.map(Some)
225 }
226 StreamResponse::ArtifactUpdate(update) => {
227 let mut task = current_task
228 .or(task_store.get(&update.task_id).await?)
229 .ok_or_else(|| A2AError::task_not_found(&update.task_id))?;
230 let artifacts = task.artifacts.get_or_insert_with(Vec::new);
231 artifacts.push(update.artifact.clone());
232 save_task(task_store, task).await.map(Some)
233 }
234 StreamResponse::Message(_) => Ok(current_task),
235 }
236}
237
238fn subscription_stream(
239 receiver: broadcast::Receiver<ExecutionEvent>,
240 snapshot_task: Option<Task>,
241 min_sequence: u64,
242) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
243 Box::pin(futures::stream::unfold(
244 SubscriptionState {
245 receiver,
246 snapshot_task,
247 min_sequence,
248 done: false,
249 },
250 |mut state| async move {
251 if state.done {
252 return None;
253 }
254
255 if let Some(task) = state.snapshot_task.take() {
256 if task.status.state.is_terminal() {
257 state.done = true;
258 }
259 return Some((Ok(StreamResponse::Task(task)), state));
260 }
261
262 loop {
263 match state.receiver.recv().await {
264 Ok(event) if event.sequence <= state.min_sequence => continue,
265 Ok(event) => {
266 state.min_sequence = event.sequence;
267 match &event.result {
268 Ok(item) if is_terminal_event(item) => state.done = true,
269 Err(_) => state.done = true,
270 _ => {}
271 }
272 return Some((event.result.clone(), state));
273 }
274 Err(broadcast::error::RecvError::Lagged(_)) => {
275 state.done = true;
276 return Some((
277 Err(A2AError::internal(
278 "subscription fell behind active execution",
279 )),
280 state,
281 ));
282 }
283 Err(broadcast::error::RecvError::Closed) => return None,
284 }
285 }
286 },
287 ))
288}
289
290async fn drive_execution(
291 runtime: ExecutionRuntime,
292 task_id: TaskId,
293 active: Arc<ActiveExecution>,
294 exec_ctx: crate::ExecutorContext,
295) {
296 let mut current_task = {
297 let state = active.state.read().await;
298 state.snapshot_task.clone()
299 };
300 let mut stream = runtime.executor.execute(exec_ctx);
301
302 while let Some(result) = stream.next().await {
303 if active.snapshot_is_terminal().await {
304 break;
305 }
306
307 match result {
308 Ok(event) => {
309 match apply_event_to_task(runtime.task_store.as_ref(), current_task.clone(), &event)
310 .await
311 {
312 Ok(updated_task) => {
313 current_task = updated_task.clone();
314 if let Err(error) = send_push_notifications(
315 &task_id,
316 runtime.push_config_store.as_deref(),
317 runtime.push_sender.as_deref(),
318 &event,
319 )
320 .await
321 {
322 active.publish(Err(error), None).await;
323 break;
324 }
325 active.publish(Ok(event.clone()), updated_task).await;
326 if is_terminal_event(&event) {
327 break;
328 }
329 }
330 Err(error) => {
331 active.publish(Err(error), None).await;
332 break;
333 }
334 }
335 }
336 Err(error) => {
337 active.publish(Err(error), None).await;
338 break;
339 }
340 }
341 }
342
343 runtime.execution_manager.finish(&task_id, &active).await;
344}
345
346#[async_trait]
353pub trait RequestHandler: Send + Sync + 'static {
354 async fn send_message(
355 &self,
356 params: &ServiceParams,
357 req: SendMessageRequest,
358 ) -> Result<SendMessageResponse, A2AError>;
359
360 async fn send_streaming_message(
361 &self,
362 params: &ServiceParams,
363 req: SendMessageRequest,
364 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError>;
365
366 async fn get_task(&self, params: &ServiceParams, req: GetTaskRequest)
367 -> Result<Task, A2AError>;
368
369 async fn list_tasks(
370 &self,
371 params: &ServiceParams,
372 req: ListTasksRequest,
373 ) -> Result<ListTasksResponse, A2AError>;
374
375 async fn cancel_task(
376 &self,
377 params: &ServiceParams,
378 req: CancelTaskRequest,
379 ) -> Result<Task, A2AError>;
380
381 async fn subscribe_to_task(
382 &self,
383 params: &ServiceParams,
384 req: SubscribeToTaskRequest,
385 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError>;
386
387 async fn create_push_config(
388 &self,
389 params: &ServiceParams,
390 req: TaskPushNotificationConfig,
391 ) -> Result<TaskPushNotificationConfig, A2AError>;
392
393 async fn get_push_config(
394 &self,
395 params: &ServiceParams,
396 req: GetTaskPushNotificationConfigRequest,
397 ) -> Result<TaskPushNotificationConfig, A2AError>;
398
399 async fn list_push_configs(
400 &self,
401 params: &ServiceParams,
402 req: ListTaskPushNotificationConfigsRequest,
403 ) -> Result<ListTaskPushNotificationConfigsResponse, A2AError>;
404
405 async fn delete_push_config(
406 &self,
407 params: &ServiceParams,
408 req: DeleteTaskPushNotificationConfigRequest,
409 ) -> Result<(), A2AError>;
410
411 async fn get_extended_agent_card(
412 &self,
413 params: &ServiceParams,
414 req: GetExtendedAgentCardRequest,
415 ) -> Result<AgentCard, A2AError>;
416}
417
418pub struct DefaultRequestHandler {
421 executor: Arc<dyn crate::AgentExecutor>,
422 task_store: Arc<dyn crate::TaskStore>,
423 execution_manager: Arc<ExecutionManager>,
424 push_config_store: Option<Arc<dyn crate::PushConfigStore>>,
425 push_sender: Option<Arc<crate::HttpPushSender>>,
426 capabilities: AgentCapabilities,
427}
428
429impl DefaultRequestHandler {
430 pub fn new(executor: impl crate::AgentExecutor, task_store: impl crate::TaskStore) -> Self {
431 DefaultRequestHandler {
432 executor: Arc::new(executor),
433 task_store: Arc::new(task_store),
434 execution_manager: Arc::new(ExecutionManager::default()),
435 push_config_store: None,
436 push_sender: None,
437 capabilities: AgentCapabilities::default(),
438 }
439 }
440
441 pub fn with_push_config_store(
442 mut self,
443 push_config_store: impl crate::PushConfigStore,
444 ) -> Self {
445 self.push_sender = Some(Arc::new(crate::HttpPushSender::new(None)));
446 self.push_config_store = Some(Arc::new(push_config_store));
447 self.capabilities.push_notifications = Some(true);
448 self
449 }
450
451 pub fn with_push_notifications(
452 mut self,
453 push_config_store: impl crate::PushConfigStore,
454 push_sender: crate::HttpPushSender,
455 ) -> Self {
456 self.push_config_store = Some(Arc::new(push_config_store));
457 self.push_sender = Some(Arc::new(push_sender));
458 self.capabilities.push_notifications = Some(true);
459 self
460 }
461
462 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
463 self.capabilities = capabilities;
464 self
465 }
466
467 fn push_config_store(&self) -> Result<&dyn crate::PushConfigStore, A2AError> {
468 self.push_config_store
469 .as_deref()
470 .ok_or_else(A2AError::push_notification_not_supported)
471 }
472
473 async fn load_task(&self, task_id: &str) -> Result<Task, A2AError> {
474 self.task_store
475 .get(task_id)
476 .await?
477 .ok_or_else(|| A2AError::task_not_found(task_id))
478 }
479
480 async fn prepare_task_for_execution(
481 &self,
482 req: &SendMessageRequest,
483 ) -> Result<(Task, Option<Task>, String), A2AError> {
484 let task_id = req.message.task_id.clone().unwrap_or_else(new_task_id);
485 let stored = self.task_store.get(&task_id).await?;
486 let context_id = stored
487 .as_ref()
488 .map(|task| task.context_id.clone())
489 .or_else(|| req.message.context_id.clone())
490 .unwrap_or_else(new_context_id);
491
492 let task = if let Some(existing) = stored.clone() {
493 existing
494 } else {
495 let task = Task {
496 id: task_id,
497 context_id: context_id.clone(),
498 status: TaskStatus {
499 state: TaskState::Submitted,
500 message: None,
501 timestamp: Some(chrono::Utc::now()),
502 },
503 artifacts: None,
504 history: Some(vec![req.message.clone()]),
505 metadata: None,
506 };
507 self.task_store.create(task.clone()).await?;
508 task
509 };
510
511 Ok((task, stored, context_id))
512 }
513
514 async fn save_request_push_config(
515 &self,
516 task_id: &str,
517 req: &SendMessageRequest,
518 ) -> Result<(), A2AError> {
519 let Some(config) = req
520 .configuration
521 .as_ref()
522 .and_then(|configuration| configuration.task_push_notification_config.clone())
523 else {
524 return Ok(());
525 };
526
527 let mut config = config;
528 config.task_id = task_id.to_string();
529 if config.tenant.is_none() {
530 config.tenant = req.tenant.clone();
531 }
532 self.push_config_store()?.save(config).await?;
533 Ok(())
534 }
535
536 async fn start_execution(
537 &self,
538 params: &ServiceParams,
539 req: SendMessageRequest,
540 include_task_snapshot_in_context: bool,
541 ) -> Result<(TaskId, BoxStream<'static, Result<StreamResponse, A2AError>>), A2AError> {
542 let (task, stored_task, context_id) = self.prepare_task_for_execution(&req).await?;
543 let task_id = task.id.clone();
544 self.save_request_push_config(&task_id, &req).await?;
545 let active = self.execution_manager.start(task.clone()).await?;
546 let receiver = active.subscribe();
547
548 let exec_ctx = crate::ExecutorContext {
549 message: Some(req.message),
550 task_id: task_id.clone(),
551 stored_task: if include_task_snapshot_in_context {
552 Some(task.clone())
553 } else {
554 stored_task
555 },
556 context_id,
557 metadata: req.metadata,
558 user: None,
559 service_params: params.clone(),
560 tenant: req.tenant,
561 };
562
563 let runtime = ExecutionRuntime {
564 executor: Arc::clone(&self.executor),
565 task_store: Arc::clone(&self.task_store),
566 push_config_store: self.push_config_store.clone(),
567 push_sender: self.push_sender.clone(),
568 execution_manager: Arc::clone(&self.execution_manager),
569 };
570 let active_execution = active.clone();
571 let execution_task_id = task_id.clone();
572
573 tokio::spawn(async move {
574 drive_execution(runtime, execution_task_id, active_execution, exec_ctx).await;
575 });
576
577 Ok((task_id, subscription_stream(receiver, None, 0)))
578 }
579}
580
581#[async_trait]
582impl RequestHandler for DefaultRequestHandler {
583 async fn send_message(
584 &self,
585 params: &ServiceParams,
586 req: SendMessageRequest,
587 ) -> Result<SendMessageResponse, A2AError> {
588 let (task_id, mut stream) = self.start_execution(params, req.clone(), true).await?;
589 let mut last_event = None;
590
591 while let Some(item) = stream.next().await {
592 let event = item?;
593
594 if let Some(interrupt_task_id) = should_interrupt_non_streaming(&req, &event) {
595 return Ok(SendMessageResponse::Task(
596 self.load_task(&interrupt_task_id).await?,
597 ));
598 }
599
600 match event {
601 StreamResponse::Message(message) => {
602 return Ok(SendMessageResponse::Message(message));
603 }
604 other => last_event = Some(other),
605 }
606 }
607
608 match last_event {
609 Some(StreamResponse::Task(task)) => Ok(SendMessageResponse::Task(task)),
610 Some(StreamResponse::StatusUpdate(update)) => Ok(SendMessageResponse::Task(
611 self.load_task(&update.task_id).await?,
612 )),
613 Some(StreamResponse::ArtifactUpdate(update)) => Ok(SendMessageResponse::Task(
614 self.load_task(&update.task_id).await?,
615 )),
616 Some(StreamResponse::Message(message)) => Ok(SendMessageResponse::Message(message)),
617 None => Ok(SendMessageResponse::Task(self.load_task(&task_id).await?)),
618 }
619 }
620
621 async fn send_streaming_message(
622 &self,
623 params: &ServiceParams,
624 req: SendMessageRequest,
625 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
626 let (_, stream) = self.start_execution(params, req, false).await?;
627 Ok(stream)
628 }
629
630 async fn get_task(
631 &self,
632 _params: &ServiceParams,
633 req: GetTaskRequest,
634 ) -> Result<Task, A2AError> {
635 self.task_store
636 .get(&req.id)
637 .await?
638 .ok_or_else(|| A2AError::task_not_found(&req.id))
639 }
640
641 async fn list_tasks(
642 &self,
643 _params: &ServiceParams,
644 req: ListTasksRequest,
645 ) -> Result<ListTasksResponse, A2AError> {
646 self.task_store.list(&req).await
647 }
648
649 async fn cancel_task(
650 &self,
651 params: &ServiceParams,
652 req: CancelTaskRequest,
653 ) -> Result<Task, A2AError> {
654 let task = self.load_task(&req.id).await?;
655
656 if task.status.state.is_terminal() {
657 return Err(A2AError::task_not_cancelable(&req.id));
658 }
659
660 let active_execution = self.execution_manager.get(&req.id).await;
661
662 let exec_ctx = crate::ExecutorContext {
663 message: None,
664 task_id: req.id.clone(),
665 stored_task: Some(task.clone()),
666 context_id: task.context_id.clone(),
667 metadata: req.metadata,
668 user: None,
669 service_params: params.clone(),
670 tenant: req.tenant,
671 };
672
673 let mut stream = self.executor.cancel(exec_ctx);
674 let mut current_task = Some(task);
675
676 while let Some(event) = stream.next().await {
677 match event {
678 Ok(event) => {
679 let updated_task =
680 apply_event_to_task(self.task_store.as_ref(), current_task.clone(), &event)
681 .await?;
682 current_task = updated_task.clone();
683
684 send_push_notifications(
685 &req.id,
686 self.push_config_store.as_deref(),
687 self.push_sender.as_deref(),
688 &event,
689 )
690 .await?;
691
692 if let Some(active) = &active_execution {
693 active.publish(Ok(event.clone()), updated_task).await;
694 }
695
696 if is_terminal_event(&event) {
697 break;
698 }
699 }
700 Err(error) => {
701 if let Some(active) = &active_execution {
702 active.publish(Err(error.clone()), None).await;
703 }
704 return Err(error);
705 }
706 }
707 }
708
709 if let Some(active) = active_execution {
710 self.execution_manager.finish(&req.id, &active).await;
711 }
712
713 self.load_task(&req.id).await
714 }
715
716 async fn subscribe_to_task(
717 &self,
718 _params: &ServiceParams,
719 req: SubscribeToTaskRequest,
720 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
721 let (receiver, snapshot_task, sequence) = self
722 .execution_manager
723 .resubscribe(&req.id)
724 .await
725 .ok_or_else(|| A2AError::task_not_found(&req.id))?;
726 Ok(subscription_stream(receiver, snapshot_task, sequence))
727 }
728
729 async fn create_push_config(
730 &self,
731 _params: &ServiceParams,
732 req: TaskPushNotificationConfig,
733 ) -> Result<TaskPushNotificationConfig, A2AError> {
734 self.push_config_store()?.save(req).await
735 }
736
737 async fn get_push_config(
738 &self,
739 _params: &ServiceParams,
740 req: GetTaskPushNotificationConfigRequest,
741 ) -> Result<TaskPushNotificationConfig, A2AError> {
742 self.push_config_store()?.get(&req.task_id, &req.id).await
743 }
744
745 async fn list_push_configs(
746 &self,
747 _params: &ServiceParams,
748 req: ListTaskPushNotificationConfigsRequest,
749 ) -> Result<ListTaskPushNotificationConfigsResponse, A2AError> {
750 let mut configs = self.push_config_store()?.list(&req.task_id).await?;
751 configs.sort_by(|left, right| left.id.cmp(&right.id));
752
753 let page_size = match req.page_size {
754 Some(size) if size > 0 => size as usize,
755 _ => 50,
756 };
757 let start = req
758 .page_token
759 .as_deref()
760 .and_then(|token| token.parse::<usize>().ok())
761 .unwrap_or(0)
762 .min(configs.len());
763 let end = (start + page_size).min(configs.len());
764 let next_page_token = (end < configs.len()).then(|| end.to_string());
765
766 Ok(ListTaskPushNotificationConfigsResponse {
767 configs: configs[start..end].to_vec(),
768 next_page_token,
769 })
770 }
771
772 async fn delete_push_config(
773 &self,
774 _params: &ServiceParams,
775 req: DeleteTaskPushNotificationConfigRequest,
776 ) -> Result<(), A2AError> {
777 self.push_config_store()?
778 .delete(&req.task_id, &req.id)
779 .await
780 }
781
782 async fn get_extended_agent_card(
783 &self,
784 _params: &ServiceParams,
785 _req: GetExtendedAgentCardRequest,
786 ) -> Result<AgentCard, A2AError> {
787 Err(A2AError::unsupported_operation(
788 "extended agent card not configured",
789 ))
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::executor::ExecutorContext;
797 use crate::push::InMemoryPushConfigStore;
798 use crate::task_store::InMemoryTaskStore;
799
800 use crate::test_util::install_crypto_provider;
801 use axum::{
802 Router,
803 body::Bytes,
804 extract::State,
805 http::{HeaderMap, StatusCode, header},
806 routing::post,
807 };
808 use futures::stream;
809 use std::sync::Arc;
810 use tokio::{
811 net::TcpListener,
812 sync::{Notify, mpsc, oneshot},
813 time::{Duration, timeout},
814 };
815
816 struct EchoExecutor;
817
818 struct PushEventExecutor;
819
820 struct ResumableExecutor {
821 release: Arc<Notify>,
822 }
823
824 #[derive(Debug)]
825 struct CapturedPush {
826 authorization: Option<String>,
827 notification_token: Option<String>,
828 event: StreamResponse,
829 }
830
831 impl crate::AgentExecutor for EchoExecutor {
832 fn execute(
833 &self,
834 ctx: ExecutorContext,
835 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
836 let task = Task {
837 id: ctx.task_id.clone(),
838 context_id: ctx.context_id.clone(),
839 status: TaskStatus {
840 state: TaskState::Completed,
841 message: ctx.message,
842 timestamp: None,
843 },
844 artifacts: None,
845 history: None,
846 metadata: None,
847 };
848 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
849 }
850
851 fn cancel(
852 &self,
853 ctx: ExecutorContext,
854 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
855 let task = Task {
856 id: ctx.task_id.clone(),
857 context_id: ctx.context_id.clone(),
858 status: TaskStatus {
859 state: TaskState::Canceled,
860 message: None,
861 timestamp: None,
862 },
863 artifacts: None,
864 history: None,
865 metadata: None,
866 };
867 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
868 }
869 }
870
871 impl crate::AgentExecutor for PushEventExecutor {
872 fn execute(
873 &self,
874 ctx: ExecutorContext,
875 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
876 let working = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
877 task_id: ctx.task_id.clone(),
878 context_id: ctx.context_id.clone(),
879 status: TaskStatus {
880 state: TaskState::Working,
881 message: None,
882 timestamp: None,
883 },
884 metadata: None,
885 });
886 let completed = StreamResponse::Task(Task {
887 id: ctx.task_id.clone(),
888 context_id: ctx.context_id.clone(),
889 status: TaskStatus {
890 state: TaskState::Completed,
891 message: ctx.message,
892 timestamp: None,
893 },
894 artifacts: None,
895 history: ctx.stored_task.and_then(|task| task.history),
896 metadata: None,
897 });
898
899 Box::pin(stream::iter(vec![Ok(working), Ok(completed)]))
900 }
901
902 fn cancel(
903 &self,
904 ctx: ExecutorContext,
905 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
906 let task = Task {
907 id: ctx.task_id.clone(),
908 context_id: ctx.context_id.clone(),
909 status: TaskStatus {
910 state: TaskState::Canceled,
911 message: None,
912 timestamp: None,
913 },
914 artifacts: None,
915 history: None,
916 metadata: None,
917 };
918 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
919 }
920 }
921
922 impl crate::AgentExecutor for ResumableExecutor {
923 fn execute(
924 &self,
925 ctx: ExecutorContext,
926 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
927 let release = self.release.clone();
928 let working_task = Task {
929 id: ctx.task_id.clone(),
930 context_id: ctx.context_id.clone(),
931 status: TaskStatus {
932 state: TaskState::Working,
933 message: None,
934 timestamp: None,
935 },
936 artifacts: None,
937 history: ctx.stored_task.and_then(|task| task.history),
938 metadata: None,
939 };
940 let completed_task = Task {
941 id: working_task.id.clone(),
942 context_id: working_task.context_id.clone(),
943 status: TaskStatus {
944 state: TaskState::Completed,
945 message: None,
946 timestamp: None,
947 },
948 artifacts: None,
949 history: working_task.history.clone(),
950 metadata: None,
951 };
952
953 Box::pin(stream::unfold(
954 (Some(working_task), Some((release, completed_task))),
955 |(working_task, completion)| async move {
956 if let Some(task) = working_task {
957 return Some((Ok(StreamResponse::Task(task)), (None, completion)));
958 }
959
960 if let Some((release, task)) = completion {
961 release.notified().await;
962 return Some((Ok(StreamResponse::Task(task)), (None, None)));
963 }
964
965 None
966 },
967 ))
968 }
969
970 fn cancel(
971 &self,
972 ctx: ExecutorContext,
973 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
974 let task = Task {
975 id: ctx.task_id.clone(),
976 context_id: ctx.context_id.clone(),
977 status: TaskStatus {
978 state: TaskState::Canceled,
979 message: None,
980 timestamp: None,
981 },
982 artifacts: None,
983 history: None,
984 metadata: None,
985 };
986 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
987 }
988 }
989
990 fn make_handler() -> DefaultRequestHandler {
991 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
992 }
993
994 fn make_handler_with_push_configs() -> DefaultRequestHandler {
995 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
996 .with_push_config_store(InMemoryPushConfigStore::new())
997 }
998
999 fn make_push_delivery_handler() -> DefaultRequestHandler {
1000 DefaultRequestHandler::new(PushEventExecutor, InMemoryTaskStore::new())
1001 .with_push_config_store(InMemoryPushConfigStore::new())
1002 }
1003
1004 fn make_resumable_handler() -> (DefaultRequestHandler, Arc<Notify>) {
1005 let release = Arc::new(Notify::new());
1006 (
1007 DefaultRequestHandler::new(
1008 ResumableExecutor {
1009 release: release.clone(),
1010 },
1011 InMemoryTaskStore::new(),
1012 ),
1013 release,
1014 )
1015 }
1016
1017 fn make_message() -> Message {
1018 Message::new(Role::User, vec![Part::text("hello")])
1019 }
1020
1021 async fn capture_push(
1022 State(sender): State<mpsc::UnboundedSender<CapturedPush>>,
1023 headers: HeaderMap,
1024 body: Bytes,
1025 ) -> StatusCode {
1026 let captured = CapturedPush {
1027 authorization: headers
1028 .get(header::AUTHORIZATION)
1029 .and_then(|value| value.to_str().ok())
1030 .map(ToOwned::to_owned),
1031 notification_token: headers
1032 .get("A2A-Notification-Token")
1033 .and_then(|value| value.to_str().ok())
1034 .map(ToOwned::to_owned),
1035 event: serde_json::from_slice(&body).unwrap(),
1036 };
1037 sender.send(captured).unwrap();
1038 StatusCode::ACCEPTED
1039 }
1040
1041 async fn start_push_webhook() -> (
1042 String,
1043 mpsc::UnboundedReceiver<CapturedPush>,
1044 oneshot::Sender<()>,
1045 tokio::task::JoinHandle<()>,
1046 ) {
1047 let (sender, receiver) = mpsc::unbounded_channel();
1048 let app = Router::new()
1049 .route("/", post(capture_push))
1050 .with_state(sender);
1051 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1052 let address = listener.local_addr().unwrap();
1053 let (shutdown_tx, shutdown_rx) = oneshot::channel();
1054 let server = tokio::spawn(async move {
1055 axum::serve(listener, app)
1056 .with_graceful_shutdown(async {
1057 let _ = shutdown_rx.await;
1058 })
1059 .await
1060 .unwrap();
1061 });
1062
1063 (format!("http://{address}/"), receiver, shutdown_tx, server)
1064 }
1065
1066 async fn next_push(receiver: &mut mpsc::UnboundedReceiver<CapturedPush>) -> CapturedPush {
1067 timeout(Duration::from_secs(5), receiver.recv())
1068 .await
1069 .unwrap()
1070 .unwrap()
1071 }
1072
1073 #[tokio::test]
1074 async fn test_send_message_creates_task() {
1075 let handler = make_handler();
1076 let params = ServiceParams::new();
1077 let req = SendMessageRequest {
1078 message: make_message(),
1079 configuration: None,
1080 metadata: None,
1081 tenant: None,
1082 };
1083 let result = handler.send_message(¶ms, req).await.unwrap();
1084 match result {
1085 SendMessageResponse::Task(task) => {
1086 assert_eq!(task.status.state, TaskState::Completed);
1087 }
1088 _ => panic!("expected Task response"),
1089 }
1090 }
1091
1092 #[tokio::test]
1093 async fn test_send_streaming_message() {
1094 use futures::StreamExt;
1095 let handler = make_handler();
1096 let params = ServiceParams::new();
1097 let req = SendMessageRequest {
1098 message: make_message(),
1099 configuration: None,
1100 metadata: None,
1101 tenant: None,
1102 };
1103 let mut stream = handler.send_streaming_message(¶ms, req).await.unwrap();
1104 let event = stream.next().await.unwrap().unwrap();
1105 assert!(matches!(event, StreamResponse::Task(_)));
1106 }
1107
1108 #[tokio::test]
1109 async fn test_get_task_not_found() {
1110 let handler = make_handler();
1111 let params = ServiceParams::new();
1112 let req = GetTaskRequest {
1113 id: "nonexistent".into(),
1114 history_length: None,
1115 tenant: None,
1116 };
1117 let result = handler.get_task(¶ms, req).await;
1118 assert!(result.is_err());
1119 }
1120
1121 #[tokio::test]
1122 async fn test_get_task_after_send() {
1123 let handler = make_handler();
1124 let params = ServiceParams::new();
1125 let mut msg = make_message();
1126 msg.task_id = Some("t1".into());
1127 msg.context_id = Some("c1".into());
1128 let req = SendMessageRequest {
1129 message: msg,
1130 configuration: None,
1131 metadata: None,
1132 tenant: None,
1133 };
1134 handler.send_message(¶ms, req).await.unwrap();
1135 let task = handler
1136 .get_task(
1137 ¶ms,
1138 GetTaskRequest {
1139 id: "t1".into(),
1140 history_length: None,
1141 tenant: None,
1142 },
1143 )
1144 .await
1145 .unwrap();
1146 assert_eq!(task.id, "t1");
1147 }
1148
1149 #[tokio::test]
1150 async fn test_list_tasks() {
1151 let handler = make_handler();
1152 let params = ServiceParams::new();
1153 let req = ListTasksRequest {
1154 context_id: None,
1155 status: None,
1156 page_size: None,
1157 page_token: None,
1158 history_length: None,
1159 status_timestamp_after: None,
1160 include_artifacts: None,
1161 tenant: None,
1162 };
1163 let resp = handler.list_tasks(¶ms, req).await.unwrap();
1164 assert!(resp.tasks.is_empty());
1165 }
1166
1167 #[tokio::test]
1168 async fn test_cancel_task_not_found() {
1169 let handler = make_handler();
1170 let params = ServiceParams::new();
1171 let req = CancelTaskRequest {
1172 id: "nonexistent".into(),
1173 metadata: None,
1174 tenant: None,
1175 };
1176 let result = handler.cancel_task(¶ms, req).await;
1177 assert!(result.is_err());
1178 }
1179
1180 #[tokio::test]
1181 async fn test_cancel_task_working() {
1182 let handler = make_handler();
1183 let params = ServiceParams::new();
1184 let task = Task {
1186 id: "t2".into(),
1187 context_id: "c2".into(),
1188 status: TaskStatus {
1189 state: TaskState::Working,
1190 message: None,
1191 timestamp: None,
1192 },
1193 artifacts: None,
1194 history: None,
1195 metadata: None,
1196 };
1197 handler.task_store.create(task).await.unwrap();
1198 let req = CancelTaskRequest {
1199 id: "t2".into(),
1200 metadata: None,
1201 tenant: None,
1202 };
1203 let result = handler.cancel_task(¶ms, req).await.unwrap();
1204 assert_eq!(result.status.state, TaskState::Canceled);
1205 }
1206
1207 #[tokio::test]
1208 async fn test_cancel_task_already_completed() {
1209 let handler = make_handler();
1210 let params = ServiceParams::new();
1211 let task = Task {
1212 id: "t3".into(),
1213 context_id: "c3".into(),
1214 status: TaskStatus {
1215 state: TaskState::Completed,
1216 message: None,
1217 timestamp: None,
1218 },
1219 artifacts: None,
1220 history: None,
1221 metadata: None,
1222 };
1223 handler.task_store.create(task).await.unwrap();
1224 let req = CancelTaskRequest {
1225 id: "t3".into(),
1226 metadata: None,
1227 tenant: None,
1228 };
1229 let result = handler.cancel_task(¶ms, req).await;
1230 assert!(result.is_err());
1231 }
1232
1233 #[tokio::test]
1234 async fn test_subscribe_requires_active_execution() {
1235 let handler = make_handler();
1236 let params = ServiceParams::new();
1237 let req = SubscribeToTaskRequest {
1238 id: "t1".into(),
1239 tenant: None,
1240 };
1241 let result = handler.subscribe_to_task(¶ms, req).await;
1242 match result {
1243 Ok(_) => panic!("expected subscribe_to_task to fail"),
1244 Err(error) => assert_eq!(error.code, error_code::TASK_NOT_FOUND),
1245 }
1246 }
1247
1248 #[tokio::test]
1249 async fn test_send_message_return_immediately_allows_resubscribe() {
1250 use futures::StreamExt;
1251
1252 let (handler, release) = make_resumable_handler();
1253 let params = ServiceParams::new();
1254 let mut message = make_message();
1255 message.task_id = Some("t-resume".into());
1256 message.context_id = Some("c-resume".into());
1257
1258 let response = handler
1259 .send_message(
1260 ¶ms,
1261 SendMessageRequest {
1262 message,
1263 configuration: Some(SendMessageConfiguration {
1264 accepted_output_modes: None,
1265 task_push_notification_config: None,
1266 history_length: None,
1267 return_immediately: Some(true),
1268 }),
1269 metadata: None,
1270 tenant: None,
1271 },
1272 )
1273 .await
1274 .unwrap();
1275
1276 match response {
1277 SendMessageResponse::Task(task) => {
1278 assert_eq!(task.status.state, TaskState::Working);
1279 }
1280 _ => panic!("expected Task response"),
1281 }
1282
1283 let mut subscription = handler
1284 .subscribe_to_task(
1285 ¶ms,
1286 SubscribeToTaskRequest {
1287 id: "t-resume".into(),
1288 tenant: None,
1289 },
1290 )
1291 .await
1292 .unwrap();
1293
1294 let snapshot = subscription.next().await.unwrap().unwrap();
1295 match snapshot {
1296 StreamResponse::Task(task) => assert_eq!(task.status.state, TaskState::Working),
1297 _ => panic!("expected snapshot task"),
1298 }
1299
1300 release.notify_waiters();
1301
1302 let terminal = subscription.next().await.unwrap().unwrap();
1303 match terminal {
1304 StreamResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1305 _ => panic!("expected terminal task"),
1306 }
1307
1308 assert!(subscription.next().await.is_none());
1309
1310 let result = handler
1311 .subscribe_to_task(
1312 ¶ms,
1313 SubscribeToTaskRequest {
1314 id: "t-resume".into(),
1315 tenant: None,
1316 },
1317 )
1318 .await;
1319 match result {
1320 Ok(_) => panic!("expected subscribe_to_task to fail after completion"),
1321 Err(error) => assert_eq!(error.code, error_code::TASK_NOT_FOUND),
1322 }
1323 }
1324
1325 #[tokio::test]
1326 async fn test_push_config_not_supported_without_store() {
1327 let handler = make_handler();
1328 let params = ServiceParams::new();
1329 let result = handler
1330 .create_push_config(
1331 ¶ms,
1332 TaskPushNotificationConfig {
1333 task_id: "t1".into(),
1334 url: "http://example.com".into(),
1335 id: None,
1336 token: None,
1337 authentication: None,
1338 tenant: None,
1339 },
1340 )
1341 .await;
1342 assert!(result.is_err());
1343 }
1344
1345 #[tokio::test]
1346 async fn test_push_config_crud_with_store() {
1347 install_crypto_provider();
1348 let handler = make_handler_with_push_configs();
1349 let params = ServiceParams::new();
1350
1351 let created = handler
1352 .create_push_config(
1353 ¶ms,
1354 TaskPushNotificationConfig {
1355 task_id: "t1".into(),
1356 url: "https://example.com/first".into(),
1357 id: Some("cfg-1".into()),
1358 token: None,
1359 authentication: None,
1360 tenant: Some("tenant-a".into()),
1361 },
1362 )
1363 .await
1364 .unwrap();
1365 assert_eq!(created.id.as_deref(), Some("cfg-1"));
1366
1367 handler
1368 .create_push_config(
1369 ¶ms,
1370 TaskPushNotificationConfig {
1371 task_id: "t1".into(),
1372 url: "https://example.com/second".into(),
1373 id: Some("cfg-2".into()),
1374 token: None,
1375 authentication: None,
1376 tenant: Some("tenant-a".into()),
1377 },
1378 )
1379 .await
1380 .unwrap();
1381
1382 let fetched = handler
1383 .get_push_config(
1384 ¶ms,
1385 GetTaskPushNotificationConfigRequest {
1386 task_id: "t1".into(),
1387 id: "cfg-1".into(),
1388 tenant: Some("tenant-a".into()),
1389 },
1390 )
1391 .await
1392 .unwrap();
1393 assert_eq!(fetched.url, "https://example.com/first");
1394
1395 let first_page = handler
1396 .list_push_configs(
1397 ¶ms,
1398 ListTaskPushNotificationConfigsRequest {
1399 task_id: "t1".into(),
1400 page_size: Some(1),
1401 page_token: Some("0".into()),
1402 tenant: Some("tenant-a".into()),
1403 },
1404 )
1405 .await
1406 .unwrap();
1407 assert_eq!(first_page.configs.len(), 1);
1408 assert_eq!(first_page.configs[0].id.as_deref(), Some("cfg-1"));
1409 assert_eq!(first_page.next_page_token.as_deref(), Some("1"));
1410
1411 let default_page = handler
1412 .list_push_configs(
1413 ¶ms,
1414 ListTaskPushNotificationConfigsRequest {
1415 task_id: "t1".into(),
1416 page_size: Some(0),
1417 page_token: None,
1418 tenant: Some("tenant-a".into()),
1419 },
1420 )
1421 .await
1422 .unwrap();
1423 assert_eq!(default_page.configs.len(), 2);
1424 assert!(default_page.next_page_token.is_none());
1425
1426 handler
1427 .delete_push_config(
1428 ¶ms,
1429 DeleteTaskPushNotificationConfigRequest {
1430 task_id: "t1".into(),
1431 id: "cfg-1".into(),
1432 tenant: Some("tenant-a".into()),
1433 },
1434 )
1435 .await
1436 .unwrap();
1437
1438 let remaining = handler
1439 .list_push_configs(
1440 ¶ms,
1441 ListTaskPushNotificationConfigsRequest {
1442 task_id: "t1".into(),
1443 page_size: None,
1444 page_token: None,
1445 tenant: Some("tenant-a".into()),
1446 },
1447 )
1448 .await
1449 .unwrap();
1450 assert_eq!(remaining.configs.len(), 1);
1451 assert_eq!(remaining.configs[0].id.as_deref(), Some("cfg-2"));
1452 }
1453
1454 #[tokio::test]
1455 async fn test_send_message_request_push_config_delivers_webhooks() {
1456 install_crypto_provider();
1457 let (url, mut receiver, shutdown_tx, server) = start_push_webhook().await;
1458 let handler = make_push_delivery_handler();
1459 let params = ServiceParams::new();
1460 let mut message = make_message();
1461 message.task_id = Some("t-push-request".into());
1462 message.context_id = Some("c-push-request".into());
1463
1464 let response = handler
1465 .send_message(
1466 ¶ms,
1467 SendMessageRequest {
1468 message,
1469 configuration: Some(SendMessageConfiguration {
1470 accepted_output_modes: None,
1471 task_push_notification_config: Some(TaskPushNotificationConfig {
1472 task_id: String::new(),
1473 url: url.clone(),
1474 id: Some("cfg-request".into()),
1475 token: Some("notify-token".into()),
1476 authentication: Some(AuthenticationInfo {
1477 scheme: "bearer".into(),
1478 credentials: Some("secret-token".into()),
1479 }),
1480 tenant: None,
1481 }),
1482 history_length: None,
1483 return_immediately: None,
1484 }),
1485 metadata: None,
1486 tenant: None,
1487 },
1488 )
1489 .await
1490 .unwrap();
1491
1492 match response {
1493 SendMessageResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1494 _ => panic!("expected Task response"),
1495 }
1496
1497 let saved = handler
1498 .get_push_config(
1499 ¶ms,
1500 GetTaskPushNotificationConfigRequest {
1501 task_id: "t-push-request".into(),
1502 id: "cfg-request".into(),
1503 tenant: None,
1504 },
1505 )
1506 .await
1507 .unwrap();
1508 assert_eq!(saved.url, url);
1509
1510 let first = next_push(&mut receiver).await;
1511 assert_eq!(first.authorization.as_deref(), Some("Bearer secret-token"));
1512 assert_eq!(first.notification_token.as_deref(), Some("notify-token"));
1513 match first.event {
1514 StreamResponse::StatusUpdate(update) => {
1515 assert_eq!(update.task_id, "t-push-request");
1516 assert_eq!(update.status.state, TaskState::Working);
1517 }
1518 _ => panic!("expected status update push"),
1519 }
1520
1521 let second = next_push(&mut receiver).await;
1522 assert_eq!(second.authorization.as_deref(), Some("Bearer secret-token"));
1523 assert_eq!(second.notification_token.as_deref(), Some("notify-token"));
1524 match second.event {
1525 StreamResponse::Task(task) => {
1526 assert_eq!(task.id, "t-push-request");
1527 assert_eq!(task.status.state, TaskState::Completed);
1528 }
1529 _ => panic!("expected final task push"),
1530 }
1531
1532 shutdown_tx.send(()).unwrap();
1533 server.await.unwrap();
1534 }
1535
1536 #[tokio::test]
1537 async fn test_send_message_stored_push_config_delivers_webhooks() {
1538 install_crypto_provider();
1539 let (url, mut receiver, shutdown_tx, server) = start_push_webhook().await;
1540 let handler = make_push_delivery_handler();
1541 let params = ServiceParams::new();
1542
1543 handler
1544 .create_push_config(
1545 ¶ms,
1546 TaskPushNotificationConfig {
1547 task_id: "t-push-stored".into(),
1548 url: url.clone(),
1549 id: Some("cfg-stored".into()),
1550 token: Some("stored-token".into()),
1551 authentication: None,
1552 tenant: None,
1553 },
1554 )
1555 .await
1556 .unwrap();
1557
1558 let mut message = make_message();
1559 message.task_id = Some("t-push-stored".into());
1560 message.context_id = Some("c-push-stored".into());
1561 let response = handler
1562 .send_message(
1563 ¶ms,
1564 SendMessageRequest {
1565 message,
1566 configuration: None,
1567 metadata: None,
1568 tenant: None,
1569 },
1570 )
1571 .await
1572 .unwrap();
1573
1574 match response {
1575 SendMessageResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1576 _ => panic!("expected Task response"),
1577 }
1578
1579 let first = next_push(&mut receiver).await;
1580 assert_eq!(first.notification_token.as_deref(), Some("stored-token"));
1581 match first.event {
1582 StreamResponse::StatusUpdate(update) => {
1583 assert_eq!(update.task_id, "t-push-stored");
1584 assert_eq!(update.status.state, TaskState::Working);
1585 }
1586 _ => panic!("expected status update push"),
1587 }
1588
1589 let second = next_push(&mut receiver).await;
1590 assert_eq!(second.notification_token.as_deref(), Some("stored-token"));
1591 match second.event {
1592 StreamResponse::Task(task) => {
1593 assert_eq!(task.id, "t-push-stored");
1594 assert_eq!(task.status.state, TaskState::Completed);
1595 }
1596 _ => panic!("expected final task push"),
1597 }
1598
1599 shutdown_tx.send(()).unwrap();
1600 server.await.unwrap();
1601 }
1602
1603 #[tokio::test]
1604 async fn test_extended_agent_card_not_configured() {
1605 let handler = make_handler();
1606 let params = ServiceParams::new();
1607 let result = handler
1608 .get_extended_agent_card(¶ms, GetExtendedAgentCardRequest { tenant: None })
1609 .await;
1610 assert!(result.is_err());
1611 }
1612
1613 #[test]
1614 fn test_with_capabilities() {
1615 let handler = make_handler().with_capabilities(AgentCapabilities {
1616 streaming: Some(true),
1617 push_notifications: None,
1618 extensions: None,
1619 extended_agent_card: None,
1620 });
1621 assert_eq!(handler.capabilities.streaming, Some(true));
1622 }
1623
1624 #[test]
1625 fn test_with_push_config_store_enables_push_capability() {
1626 install_crypto_provider();
1627 let handler = make_handler_with_push_configs();
1628 assert_eq!(handler.capabilities.push_notifications, Some(true));
1629 }
1630}