use axum::{
extract::{Query, State},
Json,
};
use klieo_a2a::types::Task;
use serde::Deserialize;
use std::sync::Arc;
use crate::error::OpsError;
use crate::state::OpsState;
#[derive(Debug, Deserialize)]
pub(crate) struct ListTasksParams {
pub(crate) context_id: Option<String>,
}
pub(crate) async fn list_tasks(
State(state): State<Arc<OpsState>>,
Query(params): Query<ListTasksParams>,
) -> Result<Json<Vec<Task>>, OpsError> {
let store = state
.task_store
.as_ref()
.ok_or(OpsError::StoreNotConfigured)?;
let context_id = params.context_id.ok_or(OpsError::ContextIdRequired)?;
let tasks = store
.list(Some(&context_id))
.await
.map_err(|e| {
tracing::error!(operation = "list_tasks", context_id = %context_id, detail = %e, "store error");
OpsError::Internal(Box::new(e))
})?;
Ok(Json(tasks))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::Body, http::{Request, StatusCode}, Router, routing::get};
use klieo_auth_common::AllowAnonymous;
use klieo_bus_memory::MemoryBus;
use klieo_a2a::task_store::{A2aTaskStore, DEFAULT_BUCKET};
use std::sync::Arc;
use tower::ServiceExt;
fn no_task_store_state() -> Arc<OpsState> {
Arc::new(OpsState {
authenticator: Arc::new(AllowAnonymous),
run_log_store: None,
task_store: None,
resume_buffer: None,
})
}
fn task_router(state: Arc<OpsState>) -> Router {
Router::new()
.route("/tasks", get(list_tasks))
.with_state(state)
}
#[tokio::test]
async fn list_tasks_no_store_returns_501() {
let app = task_router(no_task_store_state());
let req = Request::builder().uri("/tasks?context_id=ctx-1").body(Body::empty()).unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_IMPLEMENTED);
}
#[tokio::test]
async fn list_tasks_missing_context_id_returns_400() {
let bus = MemoryBus::new();
let store = Arc::new(A2aTaskStore::new(bus.kv.clone(), DEFAULT_BUCKET.into()));
let state = Arc::new(OpsState {
authenticator: Arc::new(AllowAnonymous),
run_log_store: None,
task_store: Some(store),
resume_buffer: None,
});
let app = task_router(state);
let req = Request::builder().uri("/tasks").body(Body::empty()).unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn list_tasks_returns_seeded_task() {
use klieo_a2a::types::{Task, TaskStatus};
let bus = MemoryBus::new();
let store = Arc::new(A2aTaskStore::new(bus.kv.clone(), DEFAULT_BUCKET.into()));
let task = Task {
id: "task-001".to_string(),
contextId: "ctx-test".to_string(),
status: TaskStatus::Completed,
artifacts: vec![],
history: vec![],
metadata: None,
};
store.put(&task).await.unwrap();
let state = Arc::new(OpsState {
authenticator: Arc::new(AllowAnonymous),
run_log_store: None,
task_store: Some(store),
resume_buffer: None,
});
let app = task_router(state);
let req = Request::builder()
.uri("/tasks?context_id=ctx-test")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
let tasks: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0]["id"], "task-001");
}
}