a2a_protocol_server/handler/lifecycle/
list_tasks.rs1use std::collections::HashMap;
9use std::time::Instant;
10
11use a2a_protocol_types::params::ListTasksParams;
12use a2a_protocol_types::responses::TaskListResponse;
13
14use crate::error::ServerResult;
15
16use super::super::helpers::build_call_context;
17use super::super::RequestHandler;
18
19impl RequestHandler {
20 pub async fn on_list_tasks(
26 &self,
27 params: ListTasksParams,
28 headers: Option<&HashMap<String, String>>,
29 ) -> ServerResult<TaskListResponse> {
30 let start = Instant::now();
31 trace_info!(method = "ListTasks", "handling list tasks");
32 self.metrics.on_request("ListTasks");
33
34 let tenant = params.tenant.clone().unwrap_or_default();
35 let result: ServerResult<_> = crate::store::tenant::TenantContext::scope(tenant, async {
36 let call_ctx = build_call_context("ListTasks", headers);
37 self.interceptors.run_before(&call_ctx).await?;
38 let result = self.task_store.list(¶ms).await?;
39 self.interceptors.run_after(&call_ctx).await?;
40 Ok(result)
41 })
42 .await;
43
44 let elapsed = start.elapsed();
45 match &result {
46 Ok(_) => {
47 self.metrics.on_response("ListTasks");
48 self.metrics.on_latency("ListTasks", elapsed);
49 }
50 Err(e) => {
51 self.metrics.on_error("ListTasks", &e.to_string());
52 self.metrics.on_latency("ListTasks", elapsed);
53 }
54 }
55 result
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use a2a_protocol_types::params::ListTasksParams;
62 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
63
64 use crate::agent_executor;
65 use crate::builder::RequestHandlerBuilder;
66
67 struct DummyExecutor;
68 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
69
70 fn make_completed_task(id: &str) -> Task {
71 Task {
72 id: TaskId::new(id),
73 context_id: ContextId::new("ctx-1"),
74 status: TaskStatus::new(TaskState::Completed),
75 history: None,
76 artifacts: None,
77 metadata: None,
78 }
79 }
80
81 #[tokio::test]
82 async fn list_tasks_empty_store_returns_empty() {
83 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
84 let params = ListTasksParams::default();
85 let result = handler
86 .on_list_tasks(params, None)
87 .await
88 .expect("list_tasks should succeed on empty store");
89 assert!(
90 result.tasks.is_empty(),
91 "listing tasks on an empty store should return an empty list"
92 );
93 }
94
95 #[tokio::test]
96 async fn list_tasks_returns_saved_task() {
97 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
98 let task = make_completed_task("t-list-1");
99 handler.task_store.save(task).await.unwrap();
100
101 let params = ListTasksParams::default();
102 let result = handler
103 .on_list_tasks(params, None)
104 .await
105 .expect("list_tasks should succeed");
106 assert_eq!(result.tasks.len(), 1, "should return the one saved task");
107 }
108
109 #[tokio::test]
110 async fn list_tasks_with_tenant() {
111 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
113 let params = ListTasksParams {
114 tenant: Some("test-tenant".to_string()),
115 ..Default::default()
116 };
117 let result = handler
118 .on_list_tasks(params, None)
119 .await
120 .expect("list_tasks with tenant should succeed");
121 assert!(result.tasks.is_empty());
122 }
123
124 #[tokio::test]
125 async fn list_tasks_with_headers() {
126 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
128 let params = ListTasksParams::default();
129 let mut headers = std::collections::HashMap::new();
130 headers.insert("authorization".to_string(), "Bearer tok".to_string());
131 let result = handler
132 .on_list_tasks(params, Some(&headers))
133 .await
134 .expect("list_tasks with headers should succeed");
135 assert!(result.tasks.is_empty());
136 }
137
138 #[tokio::test]
139 async fn list_tasks_error_path_records_metrics() {
140 use crate::call_context::CallContext;
142 use crate::interceptor::ServerInterceptor;
143 use std::future::Future;
144 use std::pin::Pin;
145
146 struct FailInterceptor;
147 impl ServerInterceptor for FailInterceptor {
148 fn before<'a>(
149 &'a self,
150 _ctx: &'a CallContext,
151 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
152 {
153 Box::pin(async {
154 Err(a2a_protocol_types::error::A2aError::internal(
155 "forced failure",
156 ))
157 })
158 }
159 fn after<'a>(
160 &'a self,
161 _ctx: &'a CallContext,
162 ) -> Pin<Box<dyn Future<Output = a2a_protocol_types::error::A2aResult<()>> + Send + 'a>>
163 {
164 Box::pin(async { Ok(()) })
165 }
166 }
167
168 let handler = RequestHandlerBuilder::new(DummyExecutor)
169 .with_interceptor(FailInterceptor)
170 .build()
171 .unwrap();
172
173 let params = ListTasksParams::default();
174 let result = handler.on_list_tasks(params, None).await;
175 assert!(
176 result.is_err(),
177 "list_tasks should fail when interceptor rejects, got: {result:?}"
178 );
179 }
180}