diff --git a/crates/miyabi-core/src/cache.rs b/crates/miyabi-core/src/cache.rs index fea85a7..1d33f67 100644 --- a/crates/miyabi-core/src/cache.rs +++ b/crates/miyabi-core/src/cache.rs @@ -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 = 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 = 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()); + } } diff --git a/crates/miyabi-core/src/error_policy.rs b/crates/miyabi-core/src/error_policy.rs index 10831a5..7f47921 100644 --- a/crates/miyabi-core/src/error_policy.rs +++ b/crates/miyabi-core/src/error_policy.rs @@ -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::(42) })) + .await; + assert!(result.is_ok()); + + // Second success should close + let result = breaker + .call(|| Box::pin(async { Ok::(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::(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"), + } + } }