use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use crate::{
CapabilitySupport, EmbeddingCapability, EmbeddingLimits, EmbeddingProvider, EmbeddingRequest,
EmbeddingResponse, Error, ProviderIdentity, Result,
};
#[derive(Debug, Clone)]
pub struct MockEmbeddingProvider {
state: Arc<Mutex<MockEmbeddingState>>,
embedding_capabilities: HashMap<EmbeddingCapability, CapabilitySupport>,
limits: EmbeddingLimits,
provider_name: Cow<'static, str>,
}
#[derive(Debug)]
struct MockEmbeddingState {
responses: VecDeque<Scripted>,
requests: Vec<EmbeddingRequest>,
}
#[derive(Debug)]
enum Scripted {
Exact(Box<Result<EmbeddingResponse>>),
PerInput(Vec<Vec<f32>>),
}
impl MockEmbeddingProvider {
#[must_use]
pub fn empty() -> Self {
Self::new(std::iter::empty::<Result<EmbeddingResponse>>())
}
#[must_use]
pub fn new<I>(responses: I) -> Self
where
I: IntoIterator<Item = Result<EmbeddingResponse>>,
{
Self {
state: Arc::new(Mutex::new(MockEmbeddingState {
responses: responses
.into_iter()
.map(|response| Scripted::Exact(Box::new(response)))
.collect(),
requests: Vec::new(),
})),
embedding_capabilities: HashMap::new(),
limits: EmbeddingLimits::new(),
provider_name: Cow::Borrowed("mock"),
}
}
#[must_use]
pub fn with_vectors(vectors: Vec<Vec<f32>>) -> Self {
let provider = Self::empty();
provider
.state
.lock()
.unwrap()
.responses
.push_back(Scripted::PerInput(vectors));
provider
}
#[must_use]
pub fn with_error(error: Error) -> Self {
Self::new([Err(error)])
}
pub fn push_response(&self, response: Result<EmbeddingResponse>) {
self.state
.lock()
.unwrap()
.responses
.push_back(Scripted::Exact(Box::new(response)));
}
#[must_use]
pub fn with_provider_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.provider_name = name.into();
self
}
#[must_use]
pub fn with_embedding_capability(
mut self,
capability: EmbeddingCapability,
support: CapabilitySupport,
) -> Self {
self.embedding_capabilities.insert(capability, support);
self
}
#[must_use]
pub fn with_max_inputs_per_call(mut self, max: usize) -> Self {
self.limits.max_inputs_per_call = Some(max);
self
}
#[must_use]
pub fn requests(&self) -> Vec<EmbeddingRequest> {
self.state.lock().unwrap().requests.clone()
}
}
impl ProviderIdentity for MockEmbeddingProvider {
fn provider_name(&self) -> &str {
&self.provider_name
}
fn reads_namespace(&self, namespace: &str) -> bool {
namespace == self.provider_name
}
fn downcast_any(&self, id: std::any::TypeId) -> Option<&(dyn std::any::Any + Send + Sync)> {
crate::expose(self, id)
}
}
impl EmbeddingProvider for MockEmbeddingProvider {
async fn embed(&self, request: &EmbeddingRequest) -> Result<EmbeddingResponse> {
let mut state = self.state.lock().unwrap();
state.requests.push(request.clone());
match state.responses.pop_front() {
Some(Scripted::Exact(response)) => *response,
Some(Scripted::PerInput(vectors)) => Ok(EmbeddingResponse::new(
vectors
.iter()
.cycle()
.take(request.inputs.len())
.cloned()
.collect(),
)),
None => Err(Error::unexpected_response(format!(
"mock embedding provider '{}' has no queued responses",
self.provider_name
))
.authored()),
}
}
fn embedding_capability(
&self,
_model: &str,
capability: EmbeddingCapability,
) -> CapabilitySupport {
self.embedding_capabilities
.get(&capability)
.copied()
.unwrap_or(CapabilitySupport::Unknown)
}
fn embedding_limits(&self, _model: &str) -> EmbeddingLimits {
self.limits
}
}
#[cfg(test)]
mod embedding_mock_tests {
use super::*;
use crate::{
CapabilitySupport, EmbeddingCapability, EmbeddingProvider, EmbeddingRequest,
EmbeddingResponse, Error, ProviderIdentity,
};
#[tokio::test]
async fn mock_embedding_provider_returns_queued_response() {
let provider = MockEmbeddingProvider::with_vectors(vec![vec![0.1, 0.2]]);
let request = EmbeddingRequest::new("mock-embed").input("hello");
let response = provider.embed(&request).await.unwrap();
assert_eq!(response.embeddings, vec![vec![0.1, 0.2]]);
let requests = provider.requests();
assert_eq!(requests.len(), 1);
assert_eq!(
requests[0].inputs,
vec![crate::EmbeddingInput::text("hello")]
);
}
#[tokio::test]
async fn with_vectors_answers_one_vector_per_input() {
let request = EmbeddingRequest::new("m").inputs(["a", "b", "c"]);
let cycled = MockEmbeddingProvider::with_vectors(vec![vec![1.0], vec![2.0]])
.embed(&request)
.await
.unwrap();
assert_eq!(cycled.embeddings, vec![vec![1.0], vec![2.0], vec![1.0]]);
let truncated = MockEmbeddingProvider::with_vectors(vec![vec![1.0]; 5])
.embed(&EmbeddingRequest::new("m").input("a"))
.await
.unwrap();
assert_eq!(truncated.embeddings, vec![vec![1.0]]);
}
#[tokio::test]
async fn queued_responses_can_mismatch_the_input_count() {
let provider = MockEmbeddingProvider::new([Ok(EmbeddingResponse::new(vec![vec![1.0]]))]);
let response = provider
.embed(&EmbeddingRequest::new("m").inputs(["a", "b"]))
.await
.unwrap();
assert_eq!(response.embeddings.len(), 1);
}
#[tokio::test]
async fn mock_embedding_provider_returns_queued_error() {
let provider = MockEmbeddingProvider::with_error(Error::auth("bad"));
let err = provider
.embed(&EmbeddingRequest::new("m").input("x"))
.await
.unwrap_err();
assert!(matches!(err, Error::Auth { .. }));
}
#[tokio::test]
async fn mock_embedding_provider_returns_responses_in_order() {
let provider = MockEmbeddingProvider::new([
Ok(EmbeddingResponse::new(vec![vec![1.0]])),
Ok(EmbeddingResponse::new(vec![vec![2.0]])),
]);
let first = provider
.embed(&EmbeddingRequest::new("m").input("a"))
.await
.unwrap();
let second = provider
.embed(&EmbeddingRequest::new("m").input("b"))
.await
.unwrap();
assert_eq!(first.embeddings, vec![vec![1.0]]);
assert_eq!(second.embeddings, vec![vec![2.0]]);
}
#[tokio::test]
async fn mock_embedding_provider_reports_exhaustion() {
let provider = MockEmbeddingProvider::empty();
let err = provider
.embed(&EmbeddingRequest::new("m").input("x"))
.await
.unwrap_err();
assert!(matches!(err, Error::UnexpectedResponse { .. }));
}
#[test]
fn mock_embedding_provider_exposes_provider_identity_and_capabilities() {
let provider = MockEmbeddingProvider::empty()
.with_provider_name("demo-embed")
.with_embedding_capability(
EmbeddingCapability::BatchInput,
CapabilitySupport::Supported,
);
assert_eq!(provider.provider_name(), "demo-embed");
assert_eq!(
provider.embedding_capability("m", EmbeddingCapability::BatchInput),
CapabilitySupport::Supported
);
assert_eq!(
provider.embedding_capability("m", EmbeddingCapability::OutputDimensions),
CapabilitySupport::Unknown
);
}
}