test: Add comprehensive cache and error_policy tests
Cache tests (18 new): - Test CacheEntry creation and expiration - Test TTLCache insert, remove, clear, stats - Test custom TTL, concurrent access - Test LLMCacheKey and ApiCacheKey creation - Test cache factories and serialization Error policy tests (18 new): - Test FallbackStrategy variants and defaults - Test CircuitState equality - Test CircuitBreaker default, reset, counters - Test circuit transitions and thresholds Core tests: 312 passed (52 new this phase) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c84c9d6c25
commit
f740f07c23
2 changed files with 455 additions and 0 deletions
|
|
@ -237,4 +237,214 @@ mod tests {
|
|||
assert_eq!(key1, key2);
|
||||
assert_ne!(key1, key3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_creation() {
|
||||
let entry = CacheEntry::new("test_value", Duration::from_secs(60));
|
||||
assert_eq!(entry.value, "test_value");
|
||||
assert!(!entry.is_expired());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_expired() {
|
||||
let entry = CacheEntry::new("test_value", Duration::from_millis(0));
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
assert!(entry.is_expired());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_insert_with_custom_ttl() {
|
||||
let cache = TTLCache::new(Duration::from_secs(3600)); // default 1 hour
|
||||
|
||||
// Insert with shorter custom TTL
|
||||
cache.insert_with_ttl("key1", "value1", Duration::from_millis(50)).await;
|
||||
assert_eq!(cache.get(&"key1").await, Some("value1"));
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
assert_eq!(cache.get(&"key1").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_remove() {
|
||||
let cache = TTLCache::new(Duration::from_secs(3600));
|
||||
|
||||
cache.insert("key1", "value1").await;
|
||||
assert_eq!(cache.get(&"key1").await, Some("value1"));
|
||||
|
||||
let removed = cache.remove(&"key1").await;
|
||||
assert_eq!(removed, Some("value1"));
|
||||
assert_eq!(cache.get(&"key1").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_remove_nonexistent() {
|
||||
let cache: TTLCache<String, String> = TTLCache::new(Duration::from_secs(3600));
|
||||
let removed = cache.remove(&"nonexistent".to_string()).await;
|
||||
assert_eq!(removed, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_stats() {
|
||||
let cache = TTLCache::new(Duration::from_millis(100));
|
||||
|
||||
cache.insert("key1", "value1").await;
|
||||
cache.insert("key2", "value2").await;
|
||||
|
||||
let stats = cache.stats().await;
|
||||
assert_eq!(stats.total_entries, 2);
|
||||
assert_eq!(stats.active_entries, 2);
|
||||
assert_eq!(stats.expired_entries, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_stats_with_expired() {
|
||||
let cache = TTLCache::new(Duration::from_millis(50));
|
||||
|
||||
cache.insert("key1", "value1").await;
|
||||
cache.insert_with_ttl("key2", "value2", Duration::from_secs(3600)).await;
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let stats = cache.stats().await;
|
||||
assert_eq!(stats.total_entries, 2);
|
||||
assert_eq!(stats.expired_entries, 1);
|
||||
assert_eq!(stats.active_entries, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_clear() {
|
||||
let cache = TTLCache::new(Duration::from_secs(3600));
|
||||
|
||||
cache.insert("key1", "value1").await;
|
||||
cache.insert("key2", "value2").await;
|
||||
|
||||
let stats = cache.stats().await;
|
||||
assert_eq!(stats.total_entries, 2);
|
||||
|
||||
cache.clear().await;
|
||||
|
||||
let stats = cache.stats().await;
|
||||
assert_eq!(stats.total_entries, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_multiple_types() {
|
||||
let cache: TTLCache<i32, String> = TTLCache::new(Duration::from_secs(3600));
|
||||
|
||||
cache.insert(1, "one".to_string()).await;
|
||||
cache.insert(2, "two".to_string()).await;
|
||||
|
||||
assert_eq!(cache.get(&1).await, Some("one".to_string()));
|
||||
assert_eq!(cache.get(&2).await, Some("two".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_llm_cache_key_without_temperature() {
|
||||
let key1 = LLMCacheKey::new("prompt", "model", None);
|
||||
let key2 = LLMCacheKey::new("prompt", "model", None);
|
||||
let key3 = LLMCacheKey::new("prompt", "model", Some(0.5));
|
||||
|
||||
assert_eq!(key1, key2);
|
||||
assert_ne!(key1, key3);
|
||||
assert!(key1.temperature.is_none());
|
||||
assert!(key3.temperature.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_llm_cache_key_different_models() {
|
||||
let key1 = LLMCacheKey::new("prompt", "claude-3-sonnet", Some(0.7));
|
||||
let key2 = LLMCacheKey::new("prompt", "claude-3-opus", Some(0.7));
|
||||
|
||||
assert_ne!(key1, key2);
|
||||
assert_eq!(key1.model, "claude-3-sonnet");
|
||||
assert_eq!(key2.model, "claude-3-opus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_cache_key_creation() {
|
||||
let key1 = ApiCacheKey::new("/api/v1/messages", "{\"prompt\":\"test\"}");
|
||||
let key2 = ApiCacheKey::new("/api/v1/messages", "{\"prompt\":\"test\"}");
|
||||
let key3 = ApiCacheKey::new("/api/v1/messages", "{\"prompt\":\"different\"}");
|
||||
|
||||
assert_eq!(key1.endpoint, "/api/v1/messages");
|
||||
assert_eq!(key1, key2);
|
||||
assert_ne!(key1, key3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_cache_key_different_endpoints() {
|
||||
let key1 = ApiCacheKey::new("/api/v1/messages", "{}");
|
||||
let key2 = ApiCacheKey::new("/api/v2/messages", "{}");
|
||||
|
||||
assert_ne!(key1, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_llm_cache() {
|
||||
let cache = create_llm_cache();
|
||||
// Default TTL is 1 hour
|
||||
assert_eq!(cache.default_ttl, Duration::from_secs(3600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_api_cache() {
|
||||
let cache = create_api_cache();
|
||||
// Default TTL is 30 minutes
|
||||
assert_eq!(cache.default_ttl, Duration::from_secs(1800));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_stats_serialization() {
|
||||
let stats = CacheStats {
|
||||
total_entries: 100,
|
||||
expired_entries: 10,
|
||||
active_entries: 90,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&stats).unwrap();
|
||||
assert!(json.contains("100"));
|
||||
assert!(json.contains("10"));
|
||||
assert!(json.contains("90"));
|
||||
|
||||
let deserialized: CacheStats = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.total_entries, 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ttl_cache_concurrent_access() {
|
||||
let cache = Arc::new(TTLCache::new(Duration::from_secs(3600)));
|
||||
let cache_clone = cache.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
for i in 0..10 {
|
||||
cache_clone.insert(i, i * 2).await;
|
||||
}
|
||||
});
|
||||
|
||||
handle.await.unwrap();
|
||||
|
||||
for i in 0..10 {
|
||||
assert_eq!(cache.get(&i).await, Some(i * 2));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_llm_cache_full_workflow() {
|
||||
let cache = create_llm_cache();
|
||||
let key = LLMCacheKey::new("Hello, world!", "claude-3-sonnet", Some(0.7));
|
||||
|
||||
// Miss
|
||||
assert!(cache.get(&key).await.is_none());
|
||||
|
||||
// Insert
|
||||
cache.insert(key.clone(), "Response from LLM".to_string()).await;
|
||||
|
||||
// Hit
|
||||
let cached = cache.get(&key).await;
|
||||
assert_eq!(cached, Some("Response from LLM".to_string()));
|
||||
|
||||
// Different key misses
|
||||
let different_key = LLMCacheKey::new("Different prompt", "claude-3-sonnet", Some(0.7));
|
||||
assert!(cache.get(&different_key).await.is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -288,4 +288,249 @@ mod tests {
|
|||
breaker.reset().await;
|
||||
assert_eq!(breaker.state().await, CircuitState::Closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_default() {
|
||||
let strategy = FallbackStrategy::default();
|
||||
match strategy {
|
||||
FallbackStrategy::AcceptPartialSuccess { min_successful } => {
|
||||
assert_eq!(min_successful, 1);
|
||||
}
|
||||
_ => panic!("Expected AcceptPartialSuccess"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_partial_success() {
|
||||
let strategy = FallbackStrategy::partial_success();
|
||||
match strategy {
|
||||
FallbackStrategy::AcceptPartialSuccess { min_successful } => {
|
||||
assert_eq!(min_successful, 1);
|
||||
}
|
||||
_ => panic!("Expected AcceptPartialSuccess"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_lower_temperature() {
|
||||
let strategy = FallbackStrategy::lower_temperature();
|
||||
match strategy {
|
||||
FallbackStrategy::RetryWithLowerTemperature { temperature_reduction } => {
|
||||
assert_eq!(temperature_reduction, 0.2);
|
||||
}
|
||||
_ => panic!("Expected RetryWithLowerTemperature"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_switch_model() {
|
||||
let strategy = FallbackStrategy::switch_to_claude();
|
||||
match strategy {
|
||||
FallbackStrategy::SwitchModel { fallback_model } => {
|
||||
assert_eq!(fallback_model, "claude-sonnet-4-5-20250929");
|
||||
}
|
||||
_ => panic!("Expected SwitchModel"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_wait_for_human() {
|
||||
let strategy = FallbackStrategy::wait_for_human();
|
||||
match strategy {
|
||||
FallbackStrategy::WaitForHumanIntervention { timeout } => {
|
||||
assert_eq!(timeout, Duration::from_secs(24 * 60 * 60));
|
||||
}
|
||||
_ => panic!("Expected WaitForHumanIntervention"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_skip_task() {
|
||||
let strategy = FallbackStrategy::SkipTask;
|
||||
assert!(matches!(strategy, FallbackStrategy::SkipTask));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_state_equality() {
|
||||
assert_eq!(CircuitState::Closed, CircuitState::Closed);
|
||||
assert_eq!(CircuitState::Open, CircuitState::Open);
|
||||
assert_eq!(CircuitState::HalfOpen, CircuitState::HalfOpen);
|
||||
assert_ne!(CircuitState::Closed, CircuitState::Open);
|
||||
assert_ne!(CircuitState::Open, CircuitState::HalfOpen);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_default() {
|
||||
let breaker = CircuitBreaker::default();
|
||||
assert_eq!(breaker.state().await, CircuitState::Closed);
|
||||
assert_eq!(breaker.failure_threshold, 5);
|
||||
assert_eq!(breaker.success_threshold, 2);
|
||||
assert_eq!(breaker.timeout, Duration::from_secs(60));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_default_config() {
|
||||
let breaker = CircuitBreaker::default_config();
|
||||
assert_eq!(breaker.failure_threshold, 5);
|
||||
assert_eq!(breaker.success_threshold, 2);
|
||||
assert_eq!(breaker.timeout, Duration::from_secs(60));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_success_closes_circuit() {
|
||||
let breaker = CircuitBreaker::new(2, 2, Duration::from_millis(10));
|
||||
|
||||
// Open the circuit
|
||||
for _ in 0..2 {
|
||||
let _ = breaker
|
||||
.call(|| {
|
||||
Box::pin(async {
|
||||
Result::<(), std::io::Error>::Err(std::io::Error::other("error"))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
}
|
||||
assert_eq!(breaker.state().await, CircuitState::Open);
|
||||
|
||||
// Wait for timeout
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
|
||||
// First success puts it in half-open
|
||||
let result = breaker
|
||||
.call(|| Box::pin(async { Ok::<i32, std::io::Error>(42) }))
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Second success should close
|
||||
let result = breaker
|
||||
.call(|| Box::pin(async { Ok::<i32, std::io::Error>(42) }))
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
assert_eq!(breaker.state().await, CircuitState::Closed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_consecutive_failures() {
|
||||
let breaker = CircuitBreaker::new(3, 2, Duration::from_secs(60));
|
||||
|
||||
// Record one failure
|
||||
let _ = breaker
|
||||
.call(|| {
|
||||
Box::pin(async {
|
||||
Result::<(), std::io::Error>::Err(std::io::Error::other("error"))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
assert_eq!(breaker.consecutive_failures().await, 1);
|
||||
|
||||
// Record success - should reset failures
|
||||
let _ = breaker
|
||||
.call(|| Box::pin(async { Ok::<(), std::io::Error>(()) }))
|
||||
.await;
|
||||
assert_eq!(breaker.consecutive_failures().await, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_consecutive_successes() {
|
||||
let breaker = CircuitBreaker::new(3, 2, Duration::from_secs(60));
|
||||
|
||||
let _ = breaker
|
||||
.call(|| Box::pin(async { Ok::<(), std::io::Error>(()) }))
|
||||
.await;
|
||||
assert_eq!(breaker.consecutive_successes().await, 1);
|
||||
|
||||
let _ = breaker
|
||||
.call(|| Box::pin(async { Ok::<(), std::io::Error>(()) }))
|
||||
.await;
|
||||
// After reaching success_threshold, successes is reset
|
||||
assert_eq!(breaker.consecutive_successes().await, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_passes_result_through() {
|
||||
let breaker = CircuitBreaker::new(3, 2, Duration::from_secs(60));
|
||||
|
||||
let result = breaker
|
||||
.call(|| Box::pin(async { Ok::<i32, std::io::Error>(42) }))
|
||||
.await;
|
||||
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_custom_thresholds() {
|
||||
let breaker = CircuitBreaker::new(1, 1, Duration::from_millis(10));
|
||||
|
||||
// Single failure opens circuit
|
||||
let _ = breaker
|
||||
.call(|| {
|
||||
Box::pin(async {
|
||||
Result::<(), std::io::Error>::Err(std::io::Error::other("error"))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
assert_eq!(breaker.state().await, CircuitState::Open);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_reset_clears_counters() {
|
||||
let breaker = CircuitBreaker::new(3, 3, Duration::from_secs(60));
|
||||
|
||||
// Record some failures
|
||||
for _ in 0..2 {
|
||||
let _ = breaker
|
||||
.call(|| {
|
||||
Box::pin(async {
|
||||
Result::<(), std::io::Error>::Err(std::io::Error::other("error"))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
assert_eq!(breaker.consecutive_failures().await, 2);
|
||||
|
||||
breaker.reset().await;
|
||||
|
||||
assert_eq!(breaker.consecutive_failures().await, 0);
|
||||
assert_eq!(breaker.consecutive_successes().await, 0);
|
||||
assert_eq!(breaker.state().await, CircuitState::Closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_custom_partial_success() {
|
||||
let strategy = FallbackStrategy::AcceptPartialSuccess { min_successful: 5 };
|
||||
match strategy {
|
||||
FallbackStrategy::AcceptPartialSuccess { min_successful } => {
|
||||
assert_eq!(min_successful, 5);
|
||||
}
|
||||
_ => panic!("Expected AcceptPartialSuccess"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_custom_model() {
|
||||
let strategy = FallbackStrategy::SwitchModel {
|
||||
fallback_model: "gpt-4".to_string(),
|
||||
};
|
||||
match strategy {
|
||||
FallbackStrategy::SwitchModel { fallback_model } => {
|
||||
assert_eq!(fallback_model, "gpt-4");
|
||||
}
|
||||
_ => panic!("Expected SwitchModel"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_strategy_custom_timeout() {
|
||||
let strategy = FallbackStrategy::WaitForHumanIntervention {
|
||||
timeout: Duration::from_secs(3600),
|
||||
};
|
||||
match strategy {
|
||||
FallbackStrategy::WaitForHumanIntervention { timeout } => {
|
||||
assert_eq!(timeout, Duration::from_secs(3600));
|
||||
}
|
||||
_ => panic!("Expected WaitForHumanIntervention"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue