mod in_memory;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::params::ListTasksParams;
use a2a_protocol_types::responses::TaskListResponse;
use a2a_protocol_types::task::{Task, TaskId};
pub use in_memory::InMemoryTaskStore;
pub trait TaskStore: Send + Sync + 'static {
fn save<'a>(
&'a self,
task: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>;
fn get<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>>;
fn list<'a>(
&'a self,
params: &'a ListTasksParams,
) -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>>;
fn insert_if_absent<'a>(
&'a self,
task: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>>;
fn delete<'a>(
&'a self,
id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>>;
fn count<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> {
Box::pin(async { Ok(0) })
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MinimalStore;
impl TaskStore for MinimalStore {
fn save<'a>(
&'a self,
_task: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn get<'a>(
&'a self,
_id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>> {
Box::pin(async { Ok(None) })
}
fn list<'a>(
&'a self,
_params: &'a ListTasksParams,
) -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>> {
Box::pin(async { Ok(TaskListResponse::new(vec![])) })
}
fn insert_if_absent<'a>(
&'a self,
_task: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>> {
Box::pin(async { Ok(true) })
}
fn delete<'a>(
&'a self,
_id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test]
async fn default_count_returns_zero() {
let store = MinimalStore;
let count = store.count().await.unwrap();
assert_eq!(count, 0, "default count() should return 0");
}
#[test]
fn task_store_config_default_values() {
let config = super::TaskStoreConfig::default();
assert_eq!(config.max_capacity, Some(10_000));
assert_eq!(config.task_ttl, Some(Duration::from_secs(3600)));
assert_eq!(config.eviction_interval, 64);
assert_eq!(config.max_page_size, 1000);
}
#[test]
fn task_store_config_clone_and_debug() {
let config = super::TaskStoreConfig {
max_capacity: Some(500),
task_ttl: None,
eviction_interval: 32,
max_page_size: 100,
};
let cloned = config;
assert_eq!(cloned.max_capacity, Some(500));
assert_eq!(cloned.task_ttl, None);
assert_eq!(cloned.eviction_interval, 32);
assert_eq!(cloned.max_page_size, 100);
let debug_str = format!("{cloned:?}");
assert!(
debug_str.contains("TaskStoreConfig"),
"Debug output should contain struct name: {debug_str}"
);
}
#[tokio::test]
async fn minimal_store_save_get_list_delete() {
let store = MinimalStore;
let task = Task {
id: TaskId::new("test"),
context_id: a2a_protocol_types::task::ContextId::new("ctx"),
status: a2a_protocol_types::task::TaskStatus::new(
a2a_protocol_types::task::TaskState::Submitted,
),
history: None,
artifacts: None,
metadata: None,
};
store.save(&task).await.expect("save should succeed");
assert!(
store.get(&TaskId::new("test")).await.unwrap().is_none(),
"MinimalStore get should return None"
);
let list_result = store.list(&ListTasksParams::default()).await.unwrap();
assert!(
list_result.tasks.is_empty(),
"MinimalStore list should return empty"
);
assert!(
store.insert_if_absent(&task).await.unwrap(),
"insert_if_absent should return true"
);
store
.delete(&TaskId::new("test"))
.await
.expect("delete should succeed");
}
}
#[derive(Debug, Clone)]
pub struct TaskStoreConfig {
pub max_capacity: Option<usize>,
pub task_ttl: Option<Duration>,
pub eviction_interval: u64,
pub max_page_size: u32,
}
impl Default for TaskStoreConfig {
fn default() -> Self {
Self {
max_capacity: Some(10_000),
task_ttl: Some(Duration::from_secs(3600)), eviction_interval: 64,
max_page_size: 1000,
}
}
}