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:
Shunsuke Hayashi 2025-11-23 00:45:42 +09:00
parent c84c9d6c25
commit f740f07c23
2 changed files with 455 additions and 0 deletions

View file

@ -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());
}
}

View file

@ -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"),
}
}
}