#![cfg(feature = "node-cache")]
use std::collections::HashMap;
use std::time::Duration;
use adk_graph::cache::{CacheBackend, NodeCache, NodeCachePolicy, compute_cache_key};
use proptest::prelude::*;
use serde_json::Value;
fn arb_node_name() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_]{0,19}".prop_map(|s| s)
}
fn arb_json_value() -> impl Strategy<Value = Value> {
prop_oneof![
any::<bool>().prop_map(Value::from),
any::<i64>().prop_map(Value::from),
any::<f64>().prop_filter("must be finite", |f| f.is_finite()).prop_map(Value::from),
"[a-zA-Z0-9 ]{0,30}".prop_map(|s| Value::from(s)),
Just(Value::Null),
]
}
fn arb_state() -> impl Strategy<Value = HashMap<String, Value>> {
proptest::collection::hash_map("[a-z]{1,8}".prop_map(|s| s), arb_json_value(), 0..=5)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_cache_key_same_inputs_produce_same_key(
node_name in arb_node_name(),
state in arb_state()
) {
let key1 = compute_cache_key(&node_name, &state);
let key2 = compute_cache_key(&node_name, &state);
prop_assert_eq!(&key1, &key2, "Same inputs must produce identical cache keys");
prop_assert_eq!(key1.len(), 64, "Cache key must be a 64-char hex blake3 digest");
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_cache_get_returns_stored_value(
node_name in arb_node_name(),
state in arb_state(),
result_value in arb_json_value()
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let policy = NodeCachePolicy {
backend: CacheBackend::InMemory { max_entries: 128 },
ttl: Some(Duration::from_secs(300)),
};
let cache = NodeCache::from_policy(&policy);
let key = compute_cache_key(&node_name, &state);
cache.set(&key, result_value.clone(), policy.ttl).await;
let cached = cache.get(&key).await;
prop_assert_eq!(
cached.as_ref(),
Some(&result_value),
"Retrieved value must equal stored value"
);
Ok(())
})?;
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_different_inputs_produce_different_keys(
node_name_a in arb_node_name(),
node_name_b in arb_node_name(),
state_a in arb_state(),
state_b in arb_state()
) {
let inputs_differ = node_name_a != node_name_b || state_a != state_b;
prop_assume!(inputs_differ);
let key_a = compute_cache_key(&node_name_a, &state_a);
let key_b = compute_cache_key(&node_name_b, &state_b);
prop_assert_ne!(
&key_a, &key_b,
"Different inputs must produce different cache keys"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_cache_key_independent_of_insertion_order(
node_name in arb_node_name(),
state in arb_state()
) {
let entries: Vec<(String, Value)> = state.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let mut state_reverse: HashMap<String, Value> = HashMap::new();
for (k, v) in entries.iter().rev() {
state_reverse.insert(k.clone(), v.clone());
}
let key_original = compute_cache_key(&node_name, &state);
let key_reverse = compute_cache_key(&node_name, &state_reverse);
prop_assert_eq!(
&key_original, &key_reverse,
"Cache key must be independent of HashMap insertion order"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_second_call_returns_cached_result(
node_name in arb_node_name(),
state in arb_state(),
execution_result in arb_json_value()
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let policy = NodeCachePolicy {
backend: CacheBackend::InMemory { max_entries: 256 },
ttl: Some(Duration::from_secs(600)),
};
let cache = NodeCache::from_policy(&policy);
let key = compute_cache_key(&node_name, &state);
cache.set(&key, execution_result.clone(), policy.ttl).await;
let key_again = compute_cache_key(&node_name, &state);
let cached = cache.get(&key_again).await;
prop_assert_eq!(
cached.as_ref(),
Some(&execution_result),
"Second call with identical state must return cached result"
);
Ok(())
})?;
}
}