use std::env;
use std::path::PathBuf;
use vipune::errors::Error;
use vipune::{
Config, IngestPolicy, MAX_INPUT_LENGTH, MAX_SEARCH_LIMIT, MemoryStore, detect_project,
};
#[test]
fn test_memory_store_add_then_search_returns_matching_memory() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let memory_id = match store
.add_with_conflict(project_id, "Alice works at Microsoft", None, false)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
assert!(!memory_id.is_empty());
let results = store
.search(project_id, "where does alice work", 10, 0.0)
.expect("Failed to search");
assert_eq!(results.len(), 1);
assert_eq!(results[0].content, "Alice works at Microsoft");
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_memory_store_new_with_path_traversal_returns_error() {
let config = Config::default();
let traversal_path = PathBuf::from("../../../etc/passwd");
let result = MemoryStore::new(&traversal_path, &config.embedding_model, config.clone());
assert!(result.is_err());
}
#[test]
fn test_add_with_empty_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.add_with_conflict("test", "", None, false);
assert!(result.is_err());
if !matches!(result.as_ref().unwrap_err(), Error::EmptyInput) {
panic!("Expected EmptyInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_add_with_oversized_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let long_text = "x".repeat(MAX_INPUT_LENGTH + 1);
let result = store.add_with_conflict("test", &long_text, None, false);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_with_empty_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.search("test", "", 10, 0.0);
assert!(result.is_err());
if !matches!(result.as_ref().unwrap_err(), Error::EmptyInput) {
panic!("Expected EmptyInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_with_oversized_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let long_query = "x".repeat(MAX_INPUT_LENGTH + 1);
let result = store.search("test", &long_query, 10, 0.0);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_config_default_with_no_env_vars_returns_valid_config() {
unsafe {
env::remove_var("VIPUNE_DATABASE_PATH");
env::remove_var("VIPUNE_EMBEDDING_MODEL");
env::remove_var("VIPUNE_MODEL_CACHE");
env::remove_var("VIPUNE_SIMILARITY_THRESHOLD");
env::remove_var("VIPUNE_RECENCY_WEIGHT");
}
let config = Config::default();
assert!(config.database_path.ends_with(".vipune/memories.db"));
assert_eq!(config.embedding_model, "BAAI/bge-small-en-v1.5");
assert!(config.model_cache.ends_with(".vipune/models"));
assert_eq!(config.similarity_threshold, 0.85);
assert_eq!(config.recency_weight, 0.3);
}
#[test]
fn test_detect_project_in_git_repo_returns_project_id() {
let project_id = detect_project(None);
assert!(!project_id.is_empty());
let project_id_override = detect_project(Some("my-custom-project"));
assert_eq!(project_id_override, "my-custom-project");
}
#[test]
fn test_memory_with_stored_content_returns_expected_fields() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let memory_id = match store
.add_with_conflict(
"test-project",
"Test content",
Some(r#"{"key": "value"}"#),
false,
)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let memory = store
.get(&memory_id)
.expect("Failed to get memory")
.expect("Memory not found");
assert_eq!(memory.id, memory_id);
assert_eq!(memory.project_id, "test-project");
assert_eq!(memory.content, "Test content");
assert_eq!(memory.metadata, Some(r#"{"key": "value"}"#.to_string()));
assert!(!memory.created_at.is_empty());
assert!(!memory.updated_at.is_empty());
assert!(memory.similarity.is_none());
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_hybrid_with_test_memories_returns_fused_results() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-hybrid";
match store
.add_with_conflict(project_id, "Authentication uses JWT tokens", None, false)
.expect("Failed to add memory 1")
{
vipune::AddResult::Added { .. } => {}
_ => panic!("Expected AddResult::Added"),
}
match store
.add_with_conflict(project_id, "User management system", None, false)
.expect("Failed to add memory 2")
{
vipune::AddResult::Added { .. } => {}
_ => panic!("Expected AddResult::Added"),
}
let results = store
.search_hybrid(project_id, "auth token", 10, 0.0)
.expect("Failed to search hybrid");
assert!(!results.is_empty());
assert_eq!(results[0].project_id, project_id);
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_update_with_empty_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let memory_id = match store
.add_with_conflict("test", "Original content", None, false)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let result = store.update(&memory_id, "");
assert!(result.is_err());
if !matches!(result.as_ref().unwrap_err(), Error::EmptyInput) {
panic!("Expected EmptyInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_update_with_oversized_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let memory_id = match store
.add_with_conflict("test", "Original content", None, false)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let long_text = "x".repeat(MAX_INPUT_LENGTH + 1);
let result = store.update(&memory_id, &long_text);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_with_zero_limit_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.search("test", "query", 0, 0.0);
assert!(result.is_err());
if let Error::InvalidInput(msg) = &result.as_ref().unwrap_err() {
assert!(msg.contains("Limit must be greater than 0"));
} else {
panic!("Expected InvalidInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_with_limit_over_max_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.search("test", "query", 10_001, 0.0);
assert!(result.is_err());
if let Error::InvalidInput(msg) = &result.as_ref().unwrap_err() {
assert!(msg.contains("exceeds maximum allowed"));
} else {
panic!("Expected InvalidInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_add_with_whitespace_only_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.add_with_conflict("test", " ", None, false);
assert!(result.is_err());
assert!(matches!(result.as_ref().unwrap_err(), Error::EmptyInput));
let result = store.search("test", "\t\n", 10, 0.0);
assert!(result.is_err());
assert!(matches!(result.as_ref().unwrap_err(), Error::EmptyInput));
std::fs::remove_file(db_path).ok();
}
#[cfg(unix)]
#[test]
fn test_memory_store_new_with_symlink_traversal_returns_error() {
use std::os::unix::fs;
let temp_dir = env::temp_dir();
let test_dir = temp_dir.join(format!("vipune_symlink_test_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&test_dir).expect("Failed to create test directory");
let config = Config::default();
let symlink_path = test_dir.join("symlink.db");
let target_path = PathBuf::from("/nonexistent/path/database.db");
fs::symlink(&target_path, &symlink_path).expect("Failed to create symlink");
let result = MemoryStore::new(&symlink_path, &config.embedding_model, config.clone());
std::fs::remove_file(&symlink_path).ok();
std::fs::remove_dir(&test_dir).ok();
assert!(
result.is_err(),
"MemoryStore creation should fail for inaccessible symlink"
);
}
#[test]
fn test_memory_store_new_with_parent_dir_component_returns_error() {
let config = Config::default();
let traversal_path = PathBuf::from("/tmp/../etc/evil.db");
let result = MemoryStore::new(&traversal_path, &config.embedding_model, config.clone());
match result {
Err(Error::Config(msg)) => {
assert!(
msg.contains("..") || msg.contains("escape"),
"Expected parent directory rejection message, got: {}",
msg
);
}
Err(e) => {
panic!(
"Expected Config error with parent dir rejection, got: {}",
e
);
}
Ok(_) => {
panic!("MemoryStore creation should fail for path with parent directory component");
}
}
}
#[test]
fn test_constant_max_search_limit_is_accessible() {
assert_eq!(MAX_SEARCH_LIMIT, 10_000);
}
#[test]
fn test_list_with_zero_limit_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.list("test", 0);
assert!(result.is_err());
if let Error::InvalidInput(msg) = &result.as_ref().unwrap_err() {
assert!(msg.contains("Limit must be greater than 0"));
} else {
panic!("Expected InvalidInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_list_with_limit_over_max_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.list("test", 10_001);
assert!(result.is_err());
if let Error::InvalidInput(msg) = &result.as_ref().unwrap_err() {
assert!(msg.contains("exceeds maximum allowed"));
} else {
panic!("Expected InvalidInput error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_list_regression_coverage() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let _id1 = store
.add_with_conflict(project_id, "first memory", None, true)
.expect("Failed to add memory");
let _id2 = store
.add_with_conflict(project_id, "second memory", None, true)
.expect("Failed to add memory");
let _id3 = store
.add_with_conflict(project_id, "third memory", None, true)
.expect("Failed to add memory");
let results = store.list(project_id, 10).expect("Failed to list");
assert_eq!(results.len(), 3);
assert_eq!(results[0].content, "third memory");
assert_eq!(results[1].content, "second memory");
assert_eq!(results[2].content, "first memory");
let results = store.list(project_id, 2).expect("Failed to list");
assert_eq!(results.len(), 2);
assert_eq!(results[0].content, "third memory");
let _id4 = store
.add_with_conflict("other-project", "other memory", None, true)
.expect("Failed to add memory");
let project1_results = store.list(project_id, 10).expect("Failed to list");
assert_eq!(project1_results.len(), 3);
let empty_results = store
.list("nonexistent_project", 10)
.expect("Failed to list");
assert_eq!(empty_results.len(), 0);
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_list_since_with_timezone_offset() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let _old = store
.add_with_conflict(project_id, "old memory", None, true)
.expect("Failed to add memory");
std::thread::sleep(std::time::Duration::from_millis(10));
let _new = store
.add_with_conflict(project_id, "new memory", None, true)
.expect("Failed to add memory");
let now = chrono::Utc::now();
let one_minute_ago = (now - chrono::Duration::minutes(1)).to_rfc3339();
let results = store
.list_since(project_id, &one_minute_ago, 10)
.expect("Failed to list");
assert!(results.len() >= 1);
assert!(results.iter().any(|m| m.content == "new memory"));
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_list_since_timestamp_precision_equivalence() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let _id = store
.add_with_conflict(project_id, "test memory", None, true)
.expect("Failed to add memory");
std::thread::sleep(std::time::Duration::from_millis(10));
let now = chrono::Utc::now();
let two_seconds_ago = (now - chrono::Duration::seconds(2)).to_rfc3339();
let results1 = store
.list_since(project_id, &two_seconds_ago, 10)
.expect("Failed to list");
let results2 = store
.list_since(project_id, &two_seconds_ago, 10)
.expect("Failed to list");
assert_eq!(results1.len(), results2.len());
if results1.len() > 0 && results2.len() > 0 {
assert_eq!(results1[0].id, results2[0].id);
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_get_many_with_duplicate_ids() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let id1 = match store
.add_with_conflict(project_id, "first", None, true)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let id2 = match store
.add_with_conflict(project_id, "second", None, true)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let results = store
.get_many(&[&id1, &id2, &id1, &id2])
.expect("Failed to get many");
assert_eq!(results.len(), 4);
assert_eq!(results[0].as_ref().unwrap().id, id1);
assert_eq!(results[1].as_ref().unwrap().id, id2);
assert_eq!(results[2].as_ref().unwrap().id, id1);
assert_eq!(results[3].as_ref().unwrap().id, id2);
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_add_at_exactly_max_input_length_returns_success() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let exact_text = "x".repeat(MAX_INPUT_LENGTH);
let result = store.add_with_conflict("test", &exact_text, None, false);
assert!(
result.is_ok(),
"Should accept input at exactly MAX_INPUT_LENGTH"
);
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_add_one_over_max_input_length_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let too_long_text = "x".repeat(MAX_INPUT_LENGTH + 1);
let result = store.add_with_conflict("test", &too_long_text, None, false);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_hybrid_with_empty_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.search_hybrid("test", "", 10, 0.0);
assert!(result.is_err());
assert!(matches!(result.as_ref().unwrap_err(), Error::EmptyInput));
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_search_hybrid_with_oversized_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let long_query = "x".repeat(MAX_INPUT_LENGTH + 1);
let result = store.search_hybrid("test", &long_query, 10, 0.0);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_ingest_conflict_aware_policy_maps_to_existing_behavior() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let result = store
.ingest(
project_id,
"Alice works at Microsoft",
None,
IngestPolicy::ConflictAware,
)
.expect("Failed to add memory");
assert!(
matches!(result, vipune::AddResult::Added { .. }),
"First add should succeed with ConflictAware policy"
);
let result = store
.ingest(
project_id,
"Alice works at Microsoft",
None,
IngestPolicy::ConflictAware,
)
.expect("Failed to check conflicts");
assert!(
matches!(result, vipune::AddResult::Conflicts { .. }),
"Duplicate content should return conflicts with ConflictAware policy"
);
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_ingest_force_policy_bypasses_conflicts() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let project_id = "test-project";
let _id1 = match store
.ingest(
project_id,
"Alice works at Microsoft",
None,
IngestPolicy::ConflictAware,
)
.expect("Failed to add memory")
{
vipune::AddResult::Added { id } => id,
_ => panic!("First add should succeed"),
};
let id2 = match store
.ingest(
project_id,
"Alice works at Microsoft",
None,
IngestPolicy::Force,
)
.expect("Failed to force add")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Force policy should always return Added"),
};
let results = store.list(project_id, 10).expect("Failed to list memories");
assert_eq!(
results.len(),
2,
"Both memories should exist after Force add"
);
assert!(
results.iter().any(|m| m.id == id2),
"Second memory should be stored with Force policy"
);
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_ingest_with_empty_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let result = store.ingest("test", "", None, IngestPolicy::ConflictAware);
assert!(result.is_err());
assert!(matches!(result.as_ref().unwrap_err(), Error::EmptyInput));
let result = store.ingest("test", "", None, IngestPolicy::Force);
assert!(result.is_err());
assert!(matches!(result.as_ref().unwrap_err(), Error::EmptyInput));
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_ingest_with_oversized_input_returns_error() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let long_text = "x".repeat(MAX_INPUT_LENGTH + 1);
let result = store.ingest("test", &long_text, None, IngestPolicy::ConflictAware);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
let result = store.ingest("test", &long_text, None, IngestPolicy::Force);
assert!(result.is_err());
if let Error::InputTooLong {
max_length,
actual_length,
} = &result.as_ref().unwrap_err()
{
assert_eq!(*max_length, MAX_INPUT_LENGTH);
assert_eq!(*actual_length, MAX_INPUT_LENGTH + 1);
} else {
panic!("Expected InputTooLong error");
}
std::fs::remove_file(db_path).ok();
}
#[test]
fn test_ingest_with_metadata_succeeds() {
let temp_dir = env::temp_dir();
let db_path = temp_dir.join(format!("vipune_test_{}.db", uuid::Uuid::new_v4()));
let config = Config::default();
let mut store = MemoryStore::new(db_path.as_path(), &config.embedding_model, config.clone())
.expect("Failed to create store");
let id1 = match store
.ingest(
"test-project",
"Test content",
Some(r#"{"source": "manual"}"#),
IngestPolicy::ConflictAware,
)
.expect("Failed to ingest")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let id2 = match store
.ingest(
"test-project",
"Another test",
Some(r#"{"source": "import"}"#),
IngestPolicy::Force,
)
.expect("Failed to ingest")
{
vipune::AddResult::Added { id } => id,
_ => panic!("Expected AddResult::Added"),
};
let memory1 = store.get(&id1).expect("Failed to get").expect("Not found");
assert_eq!(
memory1.metadata.as_ref().unwrap(),
r#"{"source": "manual"}"#
);
let memory2 = store.get(&id2).expect("Failed to get").expect("Not found");
assert_eq!(
memory2.metadata.as_ref().unwrap(),
r#"{"source": "import"}"#
);
std::fs::remove_file(db_path).ok();
}