use embellama::{EmbeddingEngine, EngineConfig, extract_gguf_metadata};
use serial_test::serial;
use std::path::PathBuf;
mod common;
fn get_jina_model_path() -> Option<PathBuf> {
let path = PathBuf::from(
std::env::var("HOME").unwrap()
+ "/Library/Caches/roothalia/models/jina-embeddings-v2-base-code-Q4_K_M.gguf",
);
if path.exists() {
Some(path)
} else {
std::env::var("EMBELLAMA_TEST_MODEL")
.ok()
.map(PathBuf::from)
.filter(|p| p.exists() && p.to_string_lossy().contains("jina"))
}
}
#[test]
fn test_extract_gguf_metadata_jina_model() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
eprintln!(
" Expected at: ~/Library/Caches/roothalia/models/jina-embeddings-v2-base-code-Q4_K_M.gguf"
);
return;
};
println!(
"Testing GGUF metadata extraction from: {}",
model_path.display()
);
let result = extract_gguf_metadata(&model_path);
assert!(
result.is_ok(),
"Failed to extract GGUF metadata: {:?}",
result.err()
);
let metadata = result.unwrap();
assert_eq!(
metadata.context_size, 8192,
"Expected Jina model to have 8192 context size from GGUF metadata"
);
println!(
"✓ Successfully extracted context size: {}",
metadata.context_size
);
println!(
" Dimensions from GGUF: {} ({})",
metadata.embedding_dimensions,
if metadata.embedding_dimensions > 0 {
"found"
} else {
"not found - will use model.n_embd()"
}
);
}
#[test]
fn test_extract_gguf_metadata_invalid_file() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"not a gguf file").unwrap();
let result = extract_gguf_metadata(temp_file.path());
assert!(result.is_err(), "Should fail on invalid GGUF file");
println!("✓ Correctly rejects invalid GGUF file");
}
#[test]
#[serial]
fn test_model_autodetect_context_size_from_gguf() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
return;
};
println!("Testing auto-detection of context size from GGUF");
let config = EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("jina-test")
.build()
.unwrap();
let engine = EmbeddingEngine::new(config).expect("Failed to create engine");
println!("✓ Engine created successfully with auto-detected context size");
let embedding = engine.embed(Some("jina-test"), "Test text").unwrap();
assert!(!embedding.is_empty());
println!("✓ Auto-detected context size allows embeddings to work");
engine.cleanup_thread_models();
}
#[test]
#[serial]
fn test_model_explicit_config_overrides_gguf() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
return;
};
println!("Testing that explicit config overrides GGUF metadata");
let config = EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("jina-test")
.with_context_size(2048) .build()
.unwrap();
let engine = EmbeddingEngine::new(config).expect("Failed to create engine");
let embedding = engine.embed(Some("jina-test"), "Test text").unwrap();
assert!(!embedding.is_empty());
println!("✓ Explicit config successfully overrides GGUF metadata");
engine.cleanup_thread_models();
}
#[test]
#[serial]
fn test_embedding_near_context_limit() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
return;
};
println!("Testing embedding generation near effective token limit");
let config = EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("jina-test")
.build()
.unwrap();
let engine = EmbeddingEngine::new(config).expect("Failed to create engine");
let target_tokens = 900;
let chars_needed = target_tokens * 13 / 10;
let large_text = "The quick brown fox jumps over the lazy dog. This is a test of the embedding system with a very long context. "
.repeat(chars_needed / 110);
println!(
"Generated text with approximately {} characters (target: ~{} tokens)",
large_text.len(),
target_tokens
);
let result = engine.embed(Some("jina-test"), &large_text);
match &result {
Ok(embedding) => {
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
norm > 0.1,
"Embedding norm should be non-zero, got: {}",
norm
);
println!(
"✓ Successfully generated embedding for large text (norm: {:.4})",
norm
);
}
Err(e) => {
panic!(
"Failed to generate embedding for text near context limit: {:?}",
e
);
}
}
engine.cleanup_thread_models();
}
#[test]
#[serial]
fn test_embedding_exceeds_context_size() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
return;
};
println!("Testing error handling when text exceeds context limit");
let config = EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("jina-test")
.build()
.unwrap();
let engine = EmbeddingEngine::new(config).expect("Failed to create engine");
let target_tokens = 8500;
let chars_needed = target_tokens * 13 / 10;
let oversized_text = "The quick brown fox jumps over the lazy dog. This is a test of the embedding system with an extremely long context that should exceed the limit. "
.repeat(chars_needed / 150);
println!(
"Generated text with approximately {} characters (target: ~{} tokens)",
oversized_text.len(),
target_tokens
);
let result = engine.embed(Some("jina-test"), &oversized_text);
assert!(result.is_err(), "Expected error for oversized text");
let err = result.unwrap_err();
let err_msg = format!("{:?}", err);
assert!(
err_msg.contains("exceeds")
|| err_msg.contains("token limit")
|| err_msg.contains("InvalidInput"),
"Error should mention token limit. Got: {}",
err_msg
);
println!(
"✓ Correctly rejected oversized text with error: {}",
err_msg
);
engine.cleanup_thread_models();
}
#[test]
#[serial]
fn test_embedding_at_exact_boundary() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
return;
};
println!("Testing boundary conditions with context_size=100");
let test_context_size = 100;
let config = EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("jina-test")
.with_context_size(test_context_size)
.with_n_seq_max(1)
.build()
.unwrap();
let engine = EmbeddingEngine::new(config).expect("Failed to create engine");
let safe_text = "word ".repeat(90); let result = engine.embed(Some("jina-test"), &safe_text);
assert!(result.is_ok(), "Text under limit should succeed");
println!("✓ Text under limit succeeded");
let oversized_text = "word ".repeat(150); let result = engine.embed(Some("jina-test"), &oversized_text);
assert!(result.is_err(), "Text over limit should fail");
println!("✓ Text over limit correctly rejected");
engine.cleanup_thread_models();
}
#[test]
#[serial]
fn test_batch_with_large_contexts() {
common::init_test_logger();
let Some(model_path) = get_jina_model_path() else {
eprintln!("⚠️ Skipping test: Jina model not found");
return;
};
println!("Testing batch processing with large contexts");
let config = EngineConfig::builder()
.with_model_path(model_path)
.with_model_name("jina-test")
.build()
.unwrap();
let engine = EmbeddingEngine::new(config).expect("Failed to create engine");
let target_tokens = 900;
let chars_needed = target_tokens * 13 / 10;
let large_text = "The quick brown fox jumps over the lazy dog in this batch test. "
.repeat(chars_needed / 65);
let texts: Vec<&str> = vec![&large_text, &large_text, &large_text];
println!(
"Testing batch of {} texts, each ~{} tokens",
texts.len(),
target_tokens
);
let result = engine.embed_batch(Some("jina-test"), &texts);
assert!(
result.is_ok(),
"Batch with large contexts should succeed: {:?}",
result.err()
);
let embeddings = result.unwrap();
assert_eq!(embeddings.len(), 3, "Should get 3 embeddings");
for (i, embedding) in embeddings.iter().enumerate() {
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(norm > 0.1, "Embedding {} should have non-zero norm", i);
}
println!("✓ Successfully processed batch with large contexts");
engine.cleanup_thread_models();
}