From 200cdbc4bdf8a2a83675d33eb65a1e56770b45d2 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Thu, 16 Oct 2025 22:41:33 +0200 Subject: [PATCH] test(provider): add integration tests for ProviderManager using MockProvider - Introduce `MockProvider` with configurable models, health status, generation handlers, and error simulation. - Add common test utilities and integration tests covering provider registration, model aggregation, request routing, error handling, and health refresh. --- .../tests/common/mock_provider.rs | 106 ++++++++++++++++ crates/owlen-providers/tests/common/mod.rs | 1 + .../owlen-providers/tests/integration_test.rs | 117 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 crates/owlen-providers/tests/common/mock_provider.rs create mode 100644 crates/owlen-providers/tests/common/mod.rs create mode 100644 crates/owlen-providers/tests/integration_test.rs diff --git a/crates/owlen-providers/tests/common/mock_provider.rs b/crates/owlen-providers/tests/common/mock_provider.rs new file mode 100644 index 0000000..867015f --- /dev/null +++ b/crates/owlen-providers/tests/common/mock_provider.rs @@ -0,0 +1,106 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use futures::stream::{self, StreamExt}; +use owlen_core::Result as CoreResult; +use owlen_core::provider::{ + GenerateChunk, GenerateRequest, GenerateStream, ModelInfo, ModelProvider, ProviderMetadata, + ProviderStatus, ProviderType, +}; + +pub struct MockProvider { + metadata: ProviderMetadata, + models: Vec, + status: ProviderStatus, + #[allow(clippy::type_complexity)] + generate_handler: Option Vec + Send + Sync>>, + generate_error: Option owlen_core::Error + Send + Sync>>, +} + +impl MockProvider { + pub fn new(id: &str) -> Self { + let metadata = ProviderMetadata::new( + id, + format!("Mock Provider ({})", id), + ProviderType::Local, + false, + ); + + Self { + metadata, + models: vec![ModelInfo { + name: format!("{}-primary", id), + size_bytes: None, + capabilities: vec!["chat".into()], + description: Some("Mock model".into()), + provider: ProviderMetadata::new(id, "Mock", ProviderType::Local, false), + metadata: Default::default(), + }], + status: ProviderStatus::Available, + generate_handler: None, + generate_error: None, + } + } + + pub fn with_models(mut self, models: Vec) -> Self { + self.models = models; + self + } + + pub fn with_status(mut self, status: ProviderStatus) -> Self { + self.status = status; + self + } + + pub fn with_generate_handler(mut self, handler: F) -> Self + where + F: Fn(GenerateRequest) -> Vec + Send + Sync + 'static, + { + self.generate_handler = Some(Arc::new(handler)); + self + } + + pub fn with_generate_error(mut self, factory: F) -> Self + where + F: Fn() -> owlen_core::Error + Send + Sync + 'static, + { + self.generate_error = Some(Arc::new(factory)); + self + } +} + +#[async_trait] +impl ModelProvider for MockProvider { + fn metadata(&self) -> &ProviderMetadata { + &self.metadata + } + + async fn health_check(&self) -> CoreResult { + Ok(self.status) + } + + async fn list_models(&self) -> CoreResult> { + Ok(self.models.clone()) + } + + async fn generate_stream(&self, request: GenerateRequest) -> CoreResult { + if let Some(factory) = &self.generate_error { + return Err(factory()); + } + + let chunks = if let Some(handler) = &self.generate_handler { + (handler)(request) + } else { + vec![GenerateChunk::final_chunk()] + }; + + let stream = stream::iter(chunks.into_iter().map(Ok)).boxed(); + Ok(Box::pin(stream)) + } +} + +impl From for Arc { + fn from(provider: MockProvider) -> Self { + Arc::new(provider) + } +} diff --git a/crates/owlen-providers/tests/common/mod.rs b/crates/owlen-providers/tests/common/mod.rs new file mode 100644 index 0000000..d7c1429 --- /dev/null +++ b/crates/owlen-providers/tests/common/mod.rs @@ -0,0 +1 @@ +pub mod mock_provider; diff --git a/crates/owlen-providers/tests/integration_test.rs b/crates/owlen-providers/tests/integration_test.rs new file mode 100644 index 0000000..5114369 --- /dev/null +++ b/crates/owlen-providers/tests/integration_test.rs @@ -0,0 +1,117 @@ +mod common; + +use std::sync::Arc; + +use futures::StreamExt; + +use common::mock_provider::MockProvider; +use owlen_core::config::Config; +use owlen_core::provider::{ + GenerateChunk, GenerateRequest, ModelInfo, ProviderManager, ProviderType, +}; + +#[allow(dead_code)] +fn base_config() -> Config { + Config { + providers: Default::default(), + ..Default::default() + } +} + +fn make_model(name: &str, provider: &str) -> ModelInfo { + ModelInfo { + name: name.into(), + size_bytes: None, + capabilities: vec!["chat".into()], + description: Some("mock".into()), + provider: owlen_core::provider::ProviderMetadata::new( + provider, + provider, + ProviderType::Local, + false, + ), + metadata: Default::default(), + } +} + +#[tokio::test] +async fn registers_providers_and_lists_ids() { + let manager = ProviderManager::default(); + let provider: Arc = MockProvider::new("mock-a").into(); + + manager.register_provider(provider).await; + let ids = manager.provider_ids().await; + + assert_eq!(ids, vec!["mock-a".to_string()]); +} + +#[tokio::test] +async fn aggregates_models_across_providers() { + let manager = ProviderManager::default(); + let provider_a = MockProvider::new("mock-a").with_models(vec![make_model("alpha", "mock-a")]); + let provider_b = MockProvider::new("mock-b").with_models(vec![make_model("beta", "mock-b")]); + + manager.register_provider(provider_a.into()).await; + manager.register_provider(provider_b.into()).await; + + let models = manager.list_all_models().await.unwrap(); + assert_eq!(models.len(), 2); + assert!(models.iter().any(|m| m.model.name == "alpha")); + assert!(models.iter().any(|m| m.model.name == "beta")); +} + +#[tokio::test] +async fn routes_generation_to_specific_provider() { + let manager = ProviderManager::default(); + let provider = MockProvider::new("mock-gen").with_generate_handler(|_req| { + vec![ + GenerateChunk::from_text("hello"), + GenerateChunk::final_chunk(), + ] + }); + + manager.register_provider(provider.into()).await; + + let request = GenerateRequest::new("mock-gen::primary"); + let mut stream = manager.generate("mock-gen", request).await.unwrap(); + let mut collected = Vec::new(); + while let Some(chunk) = stream.next().await { + collected.push(chunk.unwrap()); + } + + assert_eq!(collected.len(), 2); + assert_eq!(collected[0].text.as_deref(), Some("hello")); + assert!(collected[1].is_final); +} + +#[tokio::test] +async fn marks_provider_unavailable_on_error() { + let manager = ProviderManager::default(); + let provider = MockProvider::new("flaky") + .with_generate_error(|| owlen_core::Error::Network("boom".into())); + + manager.register_provider(provider.into()).await; + let request = GenerateRequest::new("flaky::model"); + let result = manager.generate("flaky", request).await; + assert!(result.is_err()); + + let status = manager.provider_status("flaky").await.unwrap(); + assert!(matches!( + status, + owlen_core::provider::ProviderStatus::Unavailable + )); +} + +#[tokio::test] +async fn health_refresh_updates_status_cache() { + let manager = ProviderManager::default(); + let provider = + MockProvider::new("healthy").with_status(owlen_core::provider::ProviderStatus::Available); + + manager.register_provider(provider.into()).await; + let statuses = manager.refresh_health().await; + assert_eq!( + statuses.get("healthy"), + Some(&owlen_core::provider::ProviderStatus::Available) + ); +}