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