use super::*;
use std::time::Duration;
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");
}
#[tokio::test]
async fn defaults_a_store_has_not_implemented_refuse_rather_than_pretend() {
use a2a_protocol_types::error::ErrorCode;
let store = MinimalStore;
let id = TaskId::new("t");
let task = Task {
id: id.clone(),
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,
};
let appended = store
.append_event(&id, 1, &StreamResponse::Task(task))
.await;
assert_eq!(
appended.map_err(|e| e.code),
Err(ErrorCode::UnsupportedOperation)
);
let released = store.release_idempotency_key("k").await;
assert_eq!(
released.map_err(|e| e.code),
Err(ErrorCode::UnsupportedOperation)
);
assert_eq!(store.earliest_event_seq(&id).await.unwrap(), None);
}
#[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);
assert_eq!(
config.max_events_per_task,
Some(super::DEFAULT_MAX_EVENTS_PER_TASK)
);
assert_eq!(
super::DEFAULT_MAX_EVENTS_PER_TASK,
512,
"the default is documented as roughly a 500-chunk stream; changing it \
changes how far back a reconnect can resume"
);
assert_eq!(
config.idempotency_key_ttl,
Some(super::DEFAULT_IDEMPOTENCY_KEY_TTL)
);
assert_eq!(
super::DEFAULT_IDEMPOTENCY_KEY_TTL,
Duration::from_secs(86_400),
"one day, matching the SQL stores' DEFAULT_IDEMPOTENCY_KEY_MAX_AGE"
);
assert_eq!(
super::DEFAULT_IDEMPOTENCY_KEY_TTL,
crate::store::retention::DEFAULT_IDEMPOTENCY_KEY_MAX_AGE,
"the two backends must not disagree about how long a retry is honoured"
);
}
#[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,
max_events_per_task: Some(8),
idempotency_key_ttl: None,
};
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");
}
#[test]
fn every_config_setter_sets_its_field() {
let d = TaskStoreConfig::default();
let cfg = TaskStoreConfig::default()
.with_max_capacity(Some(d.max_capacity.unwrap_or(0) + 11))
.with_task_ttl(Some(Duration::from_secs(12)))
.with_eviction_interval(d.eviction_interval + 1)
.with_max_page_size(d.max_page_size + 1)
.with_max_events_per_task(Some(7));
assert_eq!(cfg.max_capacity, Some(d.max_capacity.unwrap_or(0) + 11));
assert_eq!(cfg.task_ttl, Some(Duration::from_secs(12)));
assert_eq!(cfg.eviction_interval, d.eviction_interval + 1);
assert_eq!(cfg.max_page_size, d.max_page_size + 1);
assert_eq!(cfg.max_events_per_task, Some(7));
assert_eq!(
TaskStoreConfig::default()
.with_max_events_per_task(Some(0))
.effective_max_events_per_task(),
Some(1),
);
assert_eq!(
TaskStoreConfig::default()
.with_max_events_per_task(None)
.effective_max_events_per_task(),
None,
);
}