#![allow(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
use std::time::Duration;
mod common;
use conproxy::proxy::UpstreamAdapter;
#[test]
fn test_proxy_exports() {
use conproxy::proxy::{
CacheStore, CircuitBreaker, CircuitBreakerConfig, ClientConfig, QdrantConfig,
RequestCoalescer,
};
let _config: ClientConfig = ClientConfig::default();
let _qdrant_config: QdrantConfig = QdrantConfig::default();
let _ = CacheStore::new(Duration::from_secs(60), Duration::from_secs(300), 1000);
let _ = RequestCoalescer::new();
let _ = CircuitBreaker::new(CircuitBreakerConfig::default());
}
#[test]
fn test_cache_store_basic_ops() {
use conproxy::proxy::{CacheStatus, CacheStore, QueryResponse, SearchResult};
let store = CacheStore::new(
Duration::from_secs(60), Duration::from_secs(300), 1000, );
let query = "test query";
assert!(store.get(query).is_none());
let response = QueryResponse {
results: vec![SearchResult {
id: "1".to_string(),
score: 0.9,
content: "Test content".to_string(),
metadata: None,
upstream_id: None,
}],
cache_status: CacheStatus::Miss,
took_ms: 100,
generated_at: None,
miss_reason: None,
};
store.insert(query, response.clone(), "test-upstream".to_string());
let cached = store.get(query);
assert!(cached.is_some());
let entry = cached.unwrap();
assert_eq!(entry.response.results.len(), 1);
assert_eq!(entry.response.results[0].id, "1");
}
#[test]
fn test_query_normalization() {
use conproxy::proxy::CacheStore;
let q1 = " hello world ";
let q2 = "hello world";
let norm1 = CacheStore::normalize_query(q1);
let norm2 = CacheStore::normalize_query(q2);
assert_eq!(norm1, norm2);
let q3 = "HELLO WORLD";
let norm3 = CacheStore::normalize_query(q3);
assert_eq!(norm2, norm3);
}
#[test]
fn test_request_coalescer() {
use conproxy::proxy::{CoalesceAction, RequestCoalescer};
let coalescer = RequestCoalescer::new();
let hash = [0u8; 32]; match coalescer.get_or_insert(hash) {
CoalesceAction::Leader => {} CoalesceAction::Waiter(_) => panic!("First request should be leader"),
}
match coalescer.get_or_insert(hash) {
CoalesceAction::Leader => panic!("Second request should be waiter"),
CoalesceAction::Waiter(_) => {} }
}
#[test]
fn test_circuit_breaker() {
use conproxy::proxy::{CircuitBreaker, CircuitBreakerConfig, CircuitState};
let config = CircuitBreakerConfig {
failure_threshold: 3,
success_threshold: 2,
open_duration: Duration::from_secs(30),
failure_window: Duration::from_secs(60),
};
let breaker = CircuitBreaker::new(config);
assert_eq!(breaker.state(), CircuitState::Closed);
for _ in 0..3 {
breaker.record_failure();
}
assert_eq!(breaker.state(), CircuitState::Open);
}
#[test]
fn test_qdrant_adapter_config() {
use conproxy::proxy::{QdrantAdapter, QdrantConfig};
let config = QdrantConfig {
base_url: "http://qdrant:6333".to_string(),
collection_name: "test_collection".to_string(),
timeout: Duration::from_secs(10),
with_payload: true,
with_vectors: false,
score_threshold: Some(0.5),
api_key: Some("secret".to_string()),
};
let adapter = QdrantAdapter::new(config).unwrap();
assert_eq!(adapter.identifier(), "http://qdrant:6333");
assert_eq!(adapter.timeout(), Duration::from_secs(10));
let metadata = adapter.metadata();
assert_eq!(metadata.adapter_type, "qdrant");
assert_eq!(
metadata.properties.get("collection"),
Some(&"test_collection".to_string())
);
}
#[tokio::test]
async fn test_proxy_client_config() {
use conproxy::proxy::{ClientConfig, ProxyClient};
let config = ClientConfig::new("http://localhost:9999")
.with_timeout(Duration::from_secs(5))
.with_api_key("test-key");
assert_eq!(config.base_url, "http://localhost:9999");
assert_eq!(config.timeout, Duration::from_secs(5));
assert_eq!(config.api_key, Some("test-key".to_string()));
let client = ProxyClient::new(config);
assert!(client.is_ok());
}
#[test]
fn test_query_mode() {
use conproxy::proxy::{QdrantAdapter, QueryMode};
let adapter =
QdrantAdapter::simple("http://localhost:6333", "test", Duration::from_secs(30)).unwrap();
assert_eq!(adapter.query_mode(), QueryMode::Unknown);
adapter.set_query_mode(QueryMode::TextNative);
assert_eq!(adapter.query_mode(), QueryMode::TextNative);
adapter.set_query_mode(QueryMode::VectorOnly);
assert_eq!(adapter.query_mode(), QueryMode::VectorOnly);
}
#[test]
fn test_context_manager() {
use conproxy::proxy::{ContextConfig, ContextManager, QueryMode};
let config = ContextConfig {
default_context: "default".to_string(),
max_active_contexts: 5,
auto_create: true,
inactive_timeout: Duration::from_secs(3600),
};
let manager = ContextManager::new(config);
assert_eq!(manager.current(), "default");
assert_eq!(manager.active_count(), 1);
manager
.create("project-a", "http://localhost:6333", "docs")
.unwrap();
assert_eq!(manager.active_count(), 2);
manager.switch("project-a").unwrap();
assert_eq!(manager.current(), "project-a");
let meta = manager.get("project-a").unwrap();
assert_eq!(meta.upstream_url, "http://localhost:6333");
assert_eq!(meta.collection, "docs");
manager
.set_query_mode("project-a", QueryMode::TextNative)
.unwrap();
assert_eq!(manager.query_mode("project-a"), Some(QueryMode::TextNative));
manager.record_hit();
manager.record_miss();
let stats = manager.stats("project-a").unwrap();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
manager.switch("default").unwrap();
manager.delete("project-a").unwrap();
assert_eq!(manager.active_count(), 1);
}
#[test]
fn test_embedder_config() {
use conproxy::proxy::EmbedderConfig;
let config = EmbedderConfig::new("bge-small-en")
.with_cache_max_entries(5000)
.with_max_batch_size(64)
.with_warmup(false)
.with_cache_ttl(Duration::from_secs(1800));
assert_eq!(config.model_name, "bge-small-en");
assert_eq!(config.cache_max_entries, 5000);
assert_eq!(config.max_batch_size, 64);
assert!(!config.warmup_on_start);
assert_eq!(config.cache_ttl, Duration::from_secs(1800));
let config_no_cache = EmbedderConfig::default().without_cache();
assert_eq!(config_no_cache.cache_max_entries, 0);
}
mod mock_upstream_tests {
use super::common;
use common::mock_upstream::{MockUpstream, ResponseMode};
use conproxy::proxy::upstream::GenericRestAdapter;
use std::time::Duration;
#[tokio::test]
async fn test_mock_upstream_serves_query() {
let (server, url) =
MockUpstream::start_with_mode(ResponseMode::Success { result_count: 3 }).await;
let adapter =
GenericRestAdapter::new(&url, Duration::from_secs(5)).expect("adapter construction");
let req = conproxy::proxy::QueryRequest {
query: "test".to_string(),
top_k: Some(3),
priority: None,
upstream_id: None,
upstream_type: None,
};
let resp = adapter.query(&req).await.expect("query should succeed");
assert_eq!(resp.results.len(), 3, "should get 3 mock results");
assert_eq!(resp.results[0].id, "mock-doc-0");
server.stop().await;
}
#[tokio::test]
async fn test_mock_upstream_empty_mode() {
let (server, url) = MockUpstream::start_with_mode(ResponseMode::Empty).await;
let adapter =
GenericRestAdapter::new(&url, Duration::from_secs(5)).expect("adapter construction");
let req = conproxy::proxy::QueryRequest {
query: "test".to_string(),
top_k: Some(10),
priority: None,
upstream_id: None,
upstream_type: None,
};
let resp = adapter.query(&req).await.expect("query should succeed");
assert_eq!(resp.results.len(), 0, "empty mode returns no results");
server.stop().await;
}
#[tokio::test]
async fn test_mock_upstream_error_mode() {
let (server, url) = MockUpstream::start_with_mode(ResponseMode::Error(503)).await;
let adapter =
GenericRestAdapter::new(&url, Duration::from_secs(5)).expect("adapter construction");
let req = conproxy::proxy::QueryRequest {
query: "test".to_string(),
top_k: Some(10),
priority: None,
upstream_id: None,
upstream_type: None,
};
let result = adapter.query(&req).await;
assert!(result.is_err(), "error mode should propagate error");
server.stop().await;
}
}