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.
This commit is contained in:
106
crates/owlen-providers/tests/common/mock_provider.rs
Normal file
106
crates/owlen-providers/tests/common/mock_provider.rs
Normal file
@@ -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<ModelInfo>,
|
||||
status: ProviderStatus,
|
||||
#[allow(clippy::type_complexity)]
|
||||
generate_handler: Option<Arc<dyn Fn(GenerateRequest) -> Vec<GenerateChunk> + Send + Sync>>,
|
||||
generate_error: Option<Arc<dyn Fn() -> 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<ModelInfo>) -> Self {
|
||||
self.models = models;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_status(mut self, status: ProviderStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_generate_handler<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(GenerateRequest) -> Vec<GenerateChunk> + Send + Sync + 'static,
|
||||
{
|
||||
self.generate_handler = Some(Arc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_generate_error<F>(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<ProviderStatus> {
|
||||
Ok(self.status)
|
||||
}
|
||||
|
||||
async fn list_models(&self) -> CoreResult<Vec<ModelInfo>> {
|
||||
Ok(self.models.clone())
|
||||
}
|
||||
|
||||
async fn generate_stream(&self, request: GenerateRequest) -> CoreResult<GenerateStream> {
|
||||
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<MockProvider> for Arc<dyn ModelProvider> {
|
||||
fn from(provider: MockProvider) -> Self {
|
||||
Arc::new(provider)
|
||||
}
|
||||
}
|
||||
1
crates/owlen-providers/tests/common/mod.rs
Normal file
1
crates/owlen-providers/tests/common/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod mock_provider;
|
||||
117
crates/owlen-providers/tests/integration_test.rs
Normal file
117
crates/owlen-providers/tests/integration_test.rs
Normal file
@@ -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<dyn owlen_core::provider::ModelProvider> = 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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user