#![cfg(feature = "tower")]
use std::time::Duration;
use liter_llm::tower::{CacheConfig, CacheStore, CachedResponse, InMemoryStore};
use liter_llm::types::{AssistantMessage, ChatCompletionResponse, Choice, FinishReason};
fn dummy_response(id: &str) -> CachedResponse {
CachedResponse::Chat(ChatCompletionResponse {
id: id.into(),
object: "chat.completion".into(),
created: 1_700_000_000,
model: "gpt-4".into(),
choices: vec![Choice {
index: 0,
message: AssistantMessage {
content: Some("Hello!".into()),
name: None,
tool_calls: None,
refusal: None,
function_call: None,
},
finish_reason: Some(FinishReason::Stop),
}],
usage: None,
system_fingerprint: None,
service_tier: None,
})
}
#[tokio::test]
async fn in_memory_lru_eviction_under_load() {
let config = CacheConfig {
max_entries: 10,
ttl: Duration::from_secs(300),
..Default::default()
};
let store = InMemoryStore::new(&config);
for i in 0..10u64 {
let body = format!("request-{i}");
store.put(i, body, dummy_response(&format!("resp-{i}"))).await;
}
for i in 0..10u64 {
let body = format!("request-{i}");
let result = store.get(i, &body).await;
assert!(result.is_some(), "entry {i} should still be in cache before eviction");
}
store.put(10, "request-10".into(), dummy_response("resp-10")).await;
let evicted = store.get(0, "request-0").await;
assert!(evicted.is_none(), "oldest entry (key=0) should have been evicted");
for i in 1..=10u64 {
let body = format!("request-{i}");
let result = store.get(i, &body).await;
assert!(
result.is_some(),
"entry {i} should still be in cache after eviction of key=0"
);
}
}
#[tokio::test]
async fn cache_key_collision_guard() {
let config = CacheConfig {
max_entries: 100,
ttl: Duration::from_secs(300),
..Default::default()
};
let store = InMemoryStore::new(&config);
store.put(1, "request-body-A".into(), dummy_response("resp-A")).await;
let result = store.get(1, "request-body-B").await;
assert!(
result.is_none(),
"get with different request body should return None (collision guard)"
);
let result = store.get(1, "request-body-A").await;
assert!(
result.is_some(),
"get with matching request body should return the cached response"
);
}
#[cfg(feature = "opendal-cache")]
mod opendal_tests {
use super::*;
use liter_llm::tower::OpenDalCacheStore;
use std::collections::HashMap;
#[tokio::test]
async fn opendal_memory_put_get_remove() {
let store = OpenDalCacheStore::from_config("memory", HashMap::new(), "cache/", Duration::from_secs(300))
.expect("memory backend should build");
store
.put(42, "opendal-request-body".into(), dummy_response("opendal-resp"))
.await;
let result = store.get(42, "opendal-request-body").await;
assert!(result.is_some(), "OpenDAL memory cache should return stored entry");
match result.unwrap() {
CachedResponse::Chat(r) => assert_eq!(r.id, "opendal-resp"),
_ => panic!("expected CachedResponse::Chat"),
}
let miss = store.get(42, "different-body").await;
assert!(
miss.is_none(),
"OpenDAL memory cache should return None for mismatched request body"
);
store.remove(42).await;
let after_remove = store.get(42, "opendal-request-body").await;
assert!(after_remove.is_none(), "entry should be gone after remove");
}
#[tokio::test]
async fn opendal_memory_ttl_expiry() {
let store = OpenDalCacheStore::from_config("memory", HashMap::new(), "cache/", Duration::from_secs(0))
.expect("memory backend should build");
store.put(99, "ttl-body".into(), dummy_response("ttl-resp")).await;
tokio::time::sleep(Duration::from_millis(1100)).await;
let result = store.get(99, "ttl-body").await;
assert!(result.is_none(), "expired entry should return None");
}
#[tokio::test]
#[ignore = "requires Redis on localhost:6379"]
async fn redis_cache_put_get_ttl_remove() {
let mut config = HashMap::new();
config.insert("connection_string".into(), "redis://localhost:6379".into());
let store = OpenDalCacheStore::from_config("redis", config, "liter-test/", Duration::from_secs(300))
.expect("redis backend should build");
store.put(1, "redis-body".into(), dummy_response("redis-resp")).await;
let result = store.get(1, "redis-body").await;
assert!(result.is_some(), "redis cache should return stored entry");
match result.unwrap() {
CachedResponse::Chat(r) => assert_eq!(r.id, "redis-resp"),
_ => panic!("expected CachedResponse::Chat"),
}
let miss = store.get(1, "wrong-body").await;
assert!(miss.is_none(), "redis cache should miss on body mismatch");
store.remove(1).await;
let after_remove = store.get(1, "redis-body").await;
assert!(after_remove.is_none(), "entry should be gone after remove");
}
}