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:
2025-10-16 22:41:33 +02:00
parent 8525819ab4
commit 200cdbc4bd
3 changed files with 224 additions and 0 deletions

View 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)
}
}

View File

@@ -0,0 +1 @@
pub mod mock_provider;

View 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)
);
}