use neomemx::core::ChangeType;
use neomemx::prelude::*;
use std::collections::HashMap;
mod error_tests {
use neomemx::error::ErrorCode;
#[test]
fn test_error_code_retryable() {
assert!(ErrorCode::ProviderRateLimit.is_retryable());
assert!(ErrorCode::ProviderTimeout.is_retryable());
assert!(ErrorCode::ProviderConnection.is_retryable());
assert!(!ErrorCode::ConfigInvalid.is_retryable());
assert!(!ErrorCode::ValidationRequired.is_retryable());
assert!(!ErrorCode::ProviderAuth.is_retryable());
}
}
#[cfg(test)]
mod integration {
use super::*;
fn check_env() -> bool {
std::env::var("GROQ_API_KEY").is_ok() && std::env::var("JINA_API_KEY").is_ok()
}
async fn setup_engine() -> Option<NeomemxEngine> {
if !check_env() {
eprintln!("⚠️ Skipping: GROQ_API_KEY or JINA_API_KEY not set");
return None;
}
match NeomemxEngine::new().await {
Ok(engine) => Some(engine),
Err(e) => {
eprintln!("⚠️ Skipping: {}", e);
None
}
}
}
async fn get_fact_by_id(
engine: &NeomemxEngine,
fact_id: &str,
scope: ScopeIdentifiers,
) -> Result<Option<StoredFact>> {
let results = engine
.retrieve_all()
.with_scope(scope)
.limit(1000)
.execute()
.await?;
Ok(results.facts.into_iter().find(|f| f.id == fact_id))
}
#[tokio::test]
async fn test_memory_lifecycle() {
let Some(engine) = setup_engine().await else {
return;
};
let user_id = "test_lifecycle_user";
let scope = ScopeIdentifiers::for_user(user_id);
let outcome = engine
.store("Hi, my name is John. I am a software engineer at Google.")
.with_scope(scope.clone())
.execute()
.await
.unwrap();
assert!(!outcome.operations.is_empty());
let search_results = engine
.search("What does John do?")
.with_scope(scope.clone())
.limit(5)
.execute()
.await
.unwrap();
assert!(!search_results.facts.is_empty());
let all = engine
.retrieve_all()
.with_scope(scope.clone())
.limit(10)
.execute()
.await
.unwrap();
assert!(!all.facts.is_empty());
let count = engine.clear(scope).await.unwrap();
assert!(count > 0);
}
#[tokio::test]
async fn test_memory_add_raw() {
let Some(engine) = setup_engine().await else {
return;
};
let user_id = "test_raw_user";
let scope = ScopeIdentifiers::for_user(user_id);
let outcome = engine
.store("User prefers dark mode in applications")
.with_scope(scope.clone())
.enable_extraction(false)
.execute()
.await
.unwrap();
assert_eq!(outcome.operations.len(), 1);
assert_eq!(
outcome.operations[0].content,
"User prefers dark mode in applications"
);
let _count = engine.clear(scope).await.unwrap();
}
#[tokio::test]
async fn test_memory_update_and_history() {
let Some(engine) = setup_engine().await else {
return;
};
let user_id = "test_update_user";
let scope = ScopeIdentifiers::for_user(user_id);
let outcome = engine
.store("Original content")
.with_scope(scope.clone())
.enable_extraction(false)
.execute()
.await
.unwrap();
let fact_id = &outcome.operations[0].fact_id;
let update_result = engine
.update(fact_id, "Updated content", scope.clone())
.await
.unwrap();
assert_eq!(update_result.previous_content, "Original content");
assert_eq!(update_result.new_content, "Updated content");
let updated = get_fact_by_id(&engine, fact_id, scope.clone())
.await
.unwrap()
.unwrap();
assert_eq!(updated.content, "Updated content");
let history = engine.history(fact_id).await.unwrap();
assert_eq!(history.len(), 2); assert_eq!(history[0].change_type, ChangeType::Created);
assert_eq!(history[1].change_type, ChangeType::Updated);
engine.delete(fact_id, scope.clone()).await.unwrap();
let deleted = get_fact_by_id(&engine, fact_id, scope.clone())
.await
.unwrap();
assert!(deleted.is_none());
let final_history = engine.history(fact_id).await.unwrap();
assert_eq!(final_history.len(), 3);
assert_eq!(final_history[2].change_type, ChangeType::Deleted);
}
#[tokio::test]
async fn test_memory_search_isolation() {
let Some(engine) = setup_engine().await else {
return;
};
let scope_a = ScopeIdentifiers::for_user("user_a");
let scope_b = ScopeIdentifiers::for_user("user_b");
engine
.store("I love playing tennis")
.with_scope(scope_a.clone())
.enable_extraction(false)
.execute()
.await
.unwrap();
engine
.store("I love playing chess")
.with_scope(scope_b.clone())
.enable_extraction(false)
.execute()
.await
.unwrap();
let results = engine
.search("What sports do I play?")
.with_scope(scope_a.clone())
.limit(5)
.execute()
.await
.unwrap();
assert!(!results.facts.is_empty());
assert!(results.facts[0].content.to_lowercase().contains("tennis"));
let _count_a = engine.clear(scope_a).await.unwrap();
let _count_b = engine.clear(scope_b).await.unwrap();
}
#[tokio::test]
async fn test_memory_with_metadata() {
let Some(engine) = setup_engine().await else {
return;
};
let user_id = "test_meta_user";
let scope = ScopeIdentifiers::for_user(user_id);
let mut metadata = HashMap::new();
metadata.insert("category".to_string(), serde_json::json!("work"));
metadata.insert("importance".to_string(), serde_json::json!("high"));
let outcome = engine
.store("I work at Acme Corp")
.with_scope(scope.clone())
.with_metadata(metadata)
.enable_extraction(false)
.execute()
.await
.unwrap();
assert!(!outcome.operations.is_empty());
let fact_id = &outcome.operations[0].fact_id;
let fact = get_fact_by_id(&engine, fact_id, scope.clone())
.await
.unwrap()
.unwrap();
assert_eq!(
fact.metadata.get("category"),
Some(&serde_json::json!("work"))
);
assert_eq!(
fact.metadata.get("importance"),
Some(&serde_json::json!("high"))
);
let _count = engine.clear(scope).await.unwrap();
}
#[tokio::test]
async fn test_memory_with_agent() {
let Some(engine) = setup_engine().await else {
return;
};
let scope = ScopeIdentifiers::for_user("multi_agent_user").with_agent("assistant_1");
let outcome = engine
.store("User prefers formal responses")
.with_scope(scope.clone())
.enable_extraction(false)
.execute()
.await
.unwrap();
assert!(!outcome.operations.is_empty());
let results = engine
.search("response preferences")
.with_scope(scope.clone())
.execute()
.await
.unwrap();
assert!(!results.facts.is_empty());
let _count = engine.clear(scope).await.unwrap();
}
}