use super::*;
use crate::store::retention::RetentionPolicy;
use a2a_protocol_types::message::MessageId;
use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
use std::time::Duration;
async fn make_store() -> SqliteTaskStore {
SqliteTaskStore::new("sqlite::memory:")
.await
.expect("store")
}
fn task(id: &str, state: TaskState) -> Task {
Task {
id: TaskId::new(id),
context_id: ContextId::new("ctx"),
status: TaskStatus::new(state),
history: None,
artifacts: None,
metadata: None,
}
}
async fn age_key(store: &SqliteTaskStore, key: &str, seconds: i64) {
sqlx::query(
"UPDATE idempotency_keys SET created_at = strftime('%Y-%m-%d %H:%M:%S','now', ?1) \
WHERE key = ?2",
)
.bind(format!("-{seconds} seconds"))
.bind(key)
.execute(&store.pool)
.await
.expect("backdate key");
}
async fn key_count(store: &SqliteTaskStore) -> i64 {
sqlx::query_scalar::<_, i64>("SELECT count(*) FROM idempotency_keys")
.fetch_one(&store.pool)
.await
.expect("count keys")
}
const OLD_KEY: &str = "8f14e45fceea167a5a36dedd4bea2543";
const NEW_KEY: &str = "0123456789abcdef0123456789abcdef";
#[tokio::test]
async fn the_sweep_expires_old_keys_and_keeps_recent_ones() {
let store = make_store().await;
store
.save(&task("t1", TaskState::Completed))
.await
.expect("save");
for key in [OLD_KEY, NEW_KEY] {
store
.claim_idempotency_key(key, &MessageId::new("m1"), &TaskId::new("t1"))
.await
.expect("claim");
}
age_key(&store, OLD_KEY, 7200).await;
assert_eq!(key_count(&store).await, 2, "precondition: two keys");
let report = store
.purge_expired(
&RetentionPolicy::new(Duration::from_secs(1))
.with_idempotency_key_max_age(Some(Duration::from_secs(3600))),
)
.await
.expect("purge");
assert_eq!(
report.idempotency_keys_deleted, 1,
"exactly the expired key, and the report says so"
);
let remaining: Vec<String> =
sqlx::query_scalar::<_, String>("SELECT key FROM idempotency_keys")
.fetch_all(&store.pool)
.await
.expect("remaining");
assert_eq!(
remaining,
vec![NEW_KEY.to_owned()],
"the recent key must survive: expiring it early is a send that runs twice"
);
}
#[tokio::test]
async fn a_policy_without_a_key_age_deletes_no_keys() {
let store = make_store().await;
store
.save(&task("t1", TaskState::Completed))
.await
.expect("save");
store
.claim_idempotency_key(OLD_KEY, &MessageId::new("m1"), &TaskId::new("t1"))
.await
.expect("claim");
age_key(&store, OLD_KEY, 7_000_000).await;
let report = store
.purge_expired(
&RetentionPolicy::new(Duration::from_secs(1)).with_idempotency_key_max_age(None),
)
.await
.expect("purge");
assert_eq!(report.idempotency_keys_deleted, 0);
assert_eq!(
key_count(&store).await,
1,
"a store that opted out must keep its keys however old they are"
);
}
#[tokio::test]
async fn the_sweep_will_not_expire_a_key_younger_than_the_task_age() {
let store = make_store().await;
store
.save(&task("t1", TaskState::Completed))
.await
.expect("save");
store
.claim_idempotency_key(OLD_KEY, &MessageId::new("m1"), &TaskId::new("t1"))
.await
.expect("claim");
age_key(&store, OLD_KEY, 3600).await;
let report = store
.purge_expired(
&RetentionPolicy::new(Duration::from_secs(30 * 24 * 3600))
.with_idempotency_key_max_age(Some(Duration::from_secs(60))),
)
.await
.expect("purge");
assert_eq!(report.idempotency_keys_deleted, 0);
assert_eq!(
key_count(&store).await,
1,
"the clamp must hold in the sweep: this key's task is still retained"
);
}
#[tokio::test]
async fn the_key_sweep_charges_a_batch_for_every_pass_it_makes() {
let store = make_store().await;
store
.save(&task("t1", TaskState::Completed))
.await
.expect("save");
let keys = [OLD_KEY, NEW_KEY, "fedcba9876543210fedcba9876543210"];
for key in keys {
store
.claim_idempotency_key(key, &MessageId::new("m1"), &TaskId::new("t1"))
.await
.expect("claim");
age_key(&store, key, 7200).await;
}
let report = store
.purge_expired(
&RetentionPolicy::new(Duration::from_secs(1))
.with_idempotency_key_max_age(Some(Duration::from_secs(3600)))
.with_batch_size(1),
)
.await
.expect("purge");
assert_eq!(report.idempotency_keys_deleted, 3);
assert_eq!(key_count(&store).await, 0);
assert_eq!(
report.batches, 3,
"three keys at one per batch is three batches, and they are charged for"
);
}