use crate::VecboostState;
#[cfg(feature = "cli")]
use crate::api::embedding::{cli_compute_similarity, cli_embed};
use crate::api::embedding::{compute_similarity, embed, embed_batch};
#[cfg(feature = "http")]
use crate::api::embedding::{forge_compute_similarity, forge_embed, forge_embed_batch};
#[cfg(any(feature = "http", feature = "cli"))]
use crate::api::embedding::{to_api_error, uuid_like_id};
use crate::config::model::{DeviceType, EngineType, ModelConfig, Precision};
use crate::domain::{BatchEmbedRequest, EmbedRequest, SimilarityRequest};
use crate::engine::InferenceEngine;
use crate::error::VecboostError;
use crate::module_registry::EmbeddingModule;
use crate::service::embedding::EmbeddingService;
use async_trait::async_trait;
#[cfg(any(feature = "http", feature = "cli"))]
use sdforge::prelude::ApiError;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::tempdir;
use tokio::sync::RwLock;
async fn ensure_state_initialized() {
if crate::api::state().is_ok() {
return;
}
let svc = Arc::new(RwLock::new(make_service(384)));
let mut kit = trait_kit::AsyncKit::new();
kit.set_config(svc);
kit.register::<EmbeddingModule>()
.expect("register EmbeddingModule in test kit");
let kit = kit.build().await.expect("build test kit");
crate::api::init_state(VecboostState { kit: Arc::new(kit) });
}
struct TestEngine {
dimension: usize,
}
impl TestEngine {
fn new(dimension: usize) -> Self {
Self { dimension }
}
fn generate_embedding(&self, text: &str) -> Vec<f32> {
let mut embedding = vec![0.0f32; self.dimension];
let bytes = text.as_bytes();
let mut hash: u64 = 1469598103934665603;
for &byte in bytes {
hash ^= byte as u64;
hash = hash.wrapping_mul(1099511628211);
}
let mut state = hash;
for val in embedding.iter_mut() {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
let float_val = (state as f32 / u32::MAX as f32) * 2.0 - 1.0;
*val = float_val;
}
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for val in embedding.iter_mut() {
*val /= norm;
}
}
embedding
}
}
#[async_trait]
impl InferenceEngine for TestEngine {
fn embed(&self, text: &str) -> Result<Vec<f32>, VecboostError> {
Ok(self.generate_embedding(text))
}
fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, VecboostError> {
Ok(texts.iter().map(|t| self.generate_embedding(t)).collect())
}
fn precision(&self) -> &Precision {
const PRECISION: Precision = Precision::Fp32;
&PRECISION
}
fn supports_mixed_precision(&self) -> bool {
false
}
async fn try_fallback_to_cpu(&mut self, _config: &ModelConfig) -> Result<(), VecboostError> {
Ok(())
}
}
fn make_service(dimension: usize) -> EmbeddingService {
let temp_dir = tempdir().unwrap();
let mock_engine = TestEngine::new(dimension);
let model_config = ModelConfig {
name: "test-model".to_string(),
engine_type: EngineType::Candle,
model_path: PathBuf::from(temp_dir.path()),
tokenizer_path: None,
device: DeviceType::Cpu,
max_batch_size: 32,
pooling_mode: None,
expected_dimension: Some(dimension),
memory_limit_bytes: None,
oom_fallback_enabled: true,
model_sha256: None,
};
let engine: Arc<RwLock<dyn InferenceEngine + Send + Sync>> = Arc::new(RwLock::new(mock_engine));
let _ = temp_dir; EmbeddingService::new(engine, Some(model_config))
}
#[tokio::test]
async fn test_embed_returns_vector() {
let service = make_service(384);
let req = EmbedRequest {
text: "Hello world".to_string(),
normalize: Some(true),
};
let result = embed(&service, req).await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.dimension, 384);
assert_eq!(response.embedding.len(), 384);
let norm: f32 = response.embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-5);
}
#[tokio::test]
async fn test_embed_empty_text_returns_error() {
let service = make_service(384);
let req = EmbedRequest {
text: "".to_string(),
normalize: None,
};
let result = embed(&service, req).await;
assert!(result.is_err());
match result.unwrap_err() {
VecboostError::InvalidInput(_) => {}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[tokio::test]
async fn test_embed_batch_returns_vectors() {
let service = make_service(384);
let texts = vec![
"Hello world".to_string(),
"Rust is great".to_string(),
"Embedding vectors".to_string(),
];
let req = BatchEmbedRequest {
texts: texts.clone(),
mode: None,
normalize: Some(true),
};
let result = embed_batch(&service, req).await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.embeddings.len(), 3);
assert_eq!(response.dimension, 384);
for (i, emb) in response.embeddings.iter().enumerate() {
assert_eq!(emb.embedding.len(), 384);
assert_eq!(emb.text_preview, texts[i]);
}
}
#[tokio::test]
async fn test_embed_batch_empty_returns_error() {
let service = make_service(384);
let req = BatchEmbedRequest {
texts: vec![],
mode: None,
normalize: None,
};
let result = embed_batch(&service, req).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_compute_similarity_returns_score() {
let service = make_service(384);
let req = SimilarityRequest {
source: "Hello world".to_string(),
target: "Hello rust".to_string(),
};
let result = compute_similarity(&service, req).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.score >= -1.0 && response.score <= 1.0);
}
#[tokio::test]
async fn test_compute_similarity_identical_texts_returns_one() {
let service = make_service(384);
let req = SimilarityRequest {
source: "identical text".to_string(),
target: "identical text".to_string(),
};
let result = compute_similarity(&service, req).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!((response.score - 1.0).abs() < 1e-5);
}
#[tokio::test]
async fn test_compute_similarity_empty_source_returns_error() {
let service = make_service(384);
let req = SimilarityRequest {
source: "".to_string(),
target: "valid target".to_string(),
};
let result = compute_similarity(&service, req).await;
assert!(result.is_err());
match result.unwrap_err() {
VecboostError::InvalidInput(_) => {}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[tokio::test]
async fn test_compute_similarity_empty_target_returns_error() {
let service = make_service(384);
let req = SimilarityRequest {
source: "valid source".to_string(),
target: "".to_string(),
};
let result = compute_similarity(&service, req).await;
assert!(result.is_err());
match result.unwrap_err() {
VecboostError::InvalidInput(_) => {}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[cfg(any(feature = "http", feature = "cli"))]
#[test]
fn test_uuid_like_id_format() {
let id = uuid_like_id();
assert!(id.starts_with("err-"));
let suffix = &id[4..];
assert!(
suffix.parse::<u128>().is_ok(),
"suffix should be numeric, got: {}",
suffix
);
}
#[cfg(any(feature = "http", feature = "cli"))]
#[test]
fn test_uuid_like_id_unique() {
let id1 = uuid_like_id();
let id2 = uuid_like_id();
assert_ne!(id1, id2, "consecutive ids should differ");
}
#[cfg(any(feature = "http", feature = "cli"))]
#[test]
fn test_to_api_error_invalid_input() {
let err = VecboostError::InvalidInput("bad input".to_string());
let api_err = to_api_error(err);
match api_err {
ApiError::InvalidInput {
message,
field,
value,
} => {
assert_eq!(message, "bad input");
assert!(field.is_none());
assert!(value.is_none());
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[cfg(any(feature = "http", feature = "cli"))]
#[test]
fn test_to_api_error_model_load_error() {
let err = VecboostError::ModelLoadError("model not found".to_string());
let api_err = to_api_error(err);
match api_err {
ApiError::Internal {
message, error_id, ..
} => {
assert!(message.contains("Model load error"));
assert!(message.contains("model not found"));
assert!(error_id.starts_with("err-"));
}
other => panic!("Expected Internal, got {:?}", other),
}
}
#[cfg(any(feature = "http", feature = "cli"))]
#[test]
fn test_to_api_error_other_variants_become_internal() {
let variants = vec![
VecboostError::ConfigError("cfg err".to_string()),
VecboostError::InferenceError("inf err".to_string()),
VecboostError::AuthenticationError("auth err".to_string()),
VecboostError::DatabaseError("db err".to_string()),
VecboostError::InternalError("internal err".to_string()),
VecboostError::RateLimitExceeded("rl err".to_string()),
VecboostError::ValidationError("val err".to_string()),
VecboostError::IoError("io err".to_string()),
VecboostError::NotFound("nf err".to_string()),
VecboostError::SecurityError("sec err".to_string()),
VecboostError::ModelNotLoaded("not loaded".to_string()),
VecboostError::ModelFileCorrupted("corrupted".to_string()),
VecboostError::ModelIntegrityError("integrity".to_string()),
VecboostError::TokenizationError("tok err".to_string()),
VecboostError::OutOfMemory("oom".to_string()),
];
for err in variants {
let api_err = to_api_error(err.clone());
match api_err {
ApiError::Internal {
message, error_id, ..
} => {
assert!(!message.is_empty(), "message should not be empty");
assert!(error_id.starts_with("err-"));
}
other => panic!("Expected Internal for {:?}, got {:?}", err, other),
}
}
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_embed_success() {
ensure_state_initialized().await;
let req = EmbedRequest {
text: "hello forge".to_string(),
normalize: Some(true),
};
let result = forge_embed(req).await;
assert!(
result.is_ok(),
"forge_embed should succeed: {:?}",
result.err()
);
let response = result.unwrap();
assert_eq!(response.dimension, 384);
assert_eq!(response.embedding.len(), 384);
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_embed_empty_text_returns_error() {
ensure_state_initialized().await;
let req = EmbedRequest {
text: "".to_string(),
normalize: None,
};
let result = forge_embed(req).await;
assert!(result.is_err());
match result.unwrap_err() {
ApiError::InvalidInput { message, .. } => {
assert!(
message.contains("empty") || message.contains("invalid"),
"expected empty/invalid message, got: {}",
message
);
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_embed_batch_success() {
ensure_state_initialized().await;
let req = BatchEmbedRequest {
texts: vec!["text1".to_string(), "text2".to_string()],
mode: None,
normalize: Some(true),
};
let result = forge_embed_batch(req).await;
assert!(result.is_ok(), "forge_embed_batch should succeed");
let response = result.unwrap();
assert_eq!(response.embeddings.len(), 2);
assert_eq!(response.dimension, 384);
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_embed_batch_empty_returns_error() {
ensure_state_initialized().await;
let req = BatchEmbedRequest {
texts: vec![],
mode: None,
normalize: None,
};
let result = forge_embed_batch(req).await;
assert!(result.is_err());
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_compute_similarity_success() {
ensure_state_initialized().await;
let req = SimilarityRequest {
source: "hello".to_string(),
target: "world".to_string(),
};
let result = forge_compute_similarity(req).await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.score >= -1.0 && response.score <= 1.0);
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_compute_similarity_empty_source_error() {
ensure_state_initialized().await;
let req = SimilarityRequest {
source: "".to_string(),
target: "target".to_string(),
};
let result = forge_compute_similarity(req).await;
assert!(result.is_err());
}
#[cfg(feature = "cli")]
#[tokio::test]
async fn test_cli_embed_success() {
ensure_state_initialized().await;
let req = EmbedRequest {
text: "hello cli".to_string(),
normalize: None,
};
let result = cli_embed(req).await;
assert!(
result.is_ok(),
"cli_embed should succeed: {:?}",
result.err()
);
let response = result.unwrap();
assert!(!response.embedding.is_empty());
assert!(response.dimension > 0);
}
#[cfg(feature = "cli")]
#[tokio::test]
async fn test_cli_embed_empty_text_returns_error() {
ensure_state_initialized().await;
let req = EmbedRequest {
text: "".to_string(),
normalize: None,
};
let result = cli_embed(req).await;
assert!(result.is_err());
}
#[cfg(feature = "cli")]
#[tokio::test]
async fn test_cli_compute_similarity_success() {
ensure_state_initialized().await;
let req = SimilarityRequest {
source: "source text".to_string(),
target: "target text".to_string(),
};
let result = cli_compute_similarity(req).await;
assert!(
result.is_ok(),
"cli_compute_similarity should succeed: {:?}",
result.err()
);
let response = result.unwrap();
assert!(response.score >= -1.0 && response.score <= 1.0);
}
#[cfg(feature = "cli")]
#[tokio::test]
async fn test_cli_compute_similarity_empty_source_returns_error() {
ensure_state_initialized().await;
let req = SimilarityRequest {
source: "".to_string(),
target: "target".to_string(),
};
let result = cli_compute_similarity(req).await;
assert!(result.is_err());
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_forge_handler_require_calls_bounded_under_100_requests() {
ensure_state_initialized().await;
for i in 0..100 {
let req = EmbedRequest {
text: format!("benchmark require bound {}", i),
normalize: Some(true),
};
let result = forge_embed(req).await;
assert!(result.is_ok(), "iteration {} failed: {:?}", i, result.err());
}
}
#[cfg(feature = "http")]
#[tokio::test]
async fn test_audit_logger_called_by_forge_handler_pattern() {
use crate::audit::{AuditConfig, AuditLogger};
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path().join("forge_audit.log");
let audit_config = AuditConfig {
enabled: true,
log_file_path: log_path.clone(),
log_level: "info".to_string(),
max_file_size_mb: 100,
max_files: 10,
async_write: true,
};
let logger = AuditLogger::new(audit_config);
let peer_ip = "192.168.1.100".to_string();
let username = "testuser";
logger.log_login_success(username, Some(peer_ip.clone()));
logger.log_logout(username, Some(peer_ip.clone()));
logger.flush().await.unwrap();
let content = tokio::fs::read_to_string(&log_path)
.await
.expect("audit log file should exist after flush");
assert!(
content.contains("login_success"),
"login_success event should be logged"
);
assert!(
content.contains("192.168.1.100"),
"ip should be non-empty in login event"
);
assert!(content.contains("logout"), "logout event should be logged");
assert!(
content.contains(username),
"username should match the login user in logout event"
);
}