neomemx 0.1.2

A high-performance memory library for AI agents with semantic search
Documentation
//! Integration tests for neomemx

use neomemx::core::ChangeType;
use neomemx::prelude::*;
use std::collections::HashMap;

// TODO: Needs to add more test cases
// =============================================================================
// Error Handling Tests
// =============================================================================

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());
    }
}

// =============================================================================
// Integration Tests (require external services)
// =============================================================================

#[cfg(test)]
mod integration {
    use super::*;

    /// Helper to check if required environment is available
    fn check_env() -> bool {
        std::env::var("GROQ_API_KEY").is_ok() && std::env::var("JINA_API_KEY").is_ok()
    }

    /// Helper to create engine, returns None if env not ready
    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
            }
        }
    }

    /// Helper to get a fact by ID
    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))
    }

    /// Test memory lifecycle with fluent API
    #[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);

        // Store a fact with inference
        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());

        // Search
        let search_results = engine
            .search("What does John do?")
            .with_scope(scope.clone())
            .limit(5)
            .execute()
            .await
            .unwrap();

        assert!(!search_results.facts.is_empty());

        // Get all
        let all = engine
            .retrieve_all()
            .with_scope(scope.clone())
            .limit(10)
            .execute()
            .await
            .unwrap();

        assert!(!all.facts.is_empty());

        // Clean up
        let count = engine.clear(scope).await.unwrap();
        assert!(count > 0);
    }

    /// Test raw memory (no LLM inference)
    #[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);

        // Add raw memory (no inference)
        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"
        );

        // Clean up
        let _count = engine.clear(scope).await.unwrap();
    }

    /// Test memory update and history
    #[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);

        // Add raw memory
        let outcome = engine
            .store("Original content")
            .with_scope(scope.clone())
            .enable_extraction(false)
            .execute()
            .await
            .unwrap();

        let fact_id = &outcome.operations[0].fact_id;

        // Update
        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");

        // Verify update
        let updated = get_fact_by_id(&engine, fact_id, scope.clone())
            .await
            .unwrap()
            .unwrap();
        assert_eq!(updated.content, "Updated content");

        // Check history
        let history = engine.history(fact_id).await.unwrap();
        assert_eq!(history.len(), 2); // CREATED and UPDATED
        assert_eq!(history[0].change_type, ChangeType::Created);
        assert_eq!(history[1].change_type, ChangeType::Updated);

        // Delete
        engine.delete(fact_id, scope.clone()).await.unwrap();

        // Verify deletion
        let deleted = get_fact_by_id(&engine, fact_id, scope.clone())
            .await
            .unwrap();
        assert!(deleted.is_none());

        // History should have DELETE
        let final_history = engine.history(fact_id).await.unwrap();
        assert_eq!(final_history.len(), 3);
        assert_eq!(final_history[2].change_type, ChangeType::Deleted);
    }

    /// Test search isolation between users
    #[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");

        // Add memories for different users
        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();

        // Search for user_a only
        let results = engine
            .search("What sports do I play?")
            .with_scope(scope_a.clone())
            .limit(5)
            .execute()
            .await
            .unwrap();

        // Should only find tennis (user_a's memory)
        assert!(!results.facts.is_empty());
        assert!(results.facts[0].content.to_lowercase().contains("tennis"));

        // Clean up
        let _count_a = engine.clear(scope_a).await.unwrap();
        let _count_b = engine.clear(scope_b).await.unwrap();
    }

    /// Test with metadata
    #[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);

        // Add with metadata
        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());

        // Verify metadata was stored
        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"))
        );

        // Clean up
        let _count = engine.clear(scope).await.unwrap();
    }

    /// Test multi-agent scenario
    #[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");

        // Add memory for specific agent
        let outcome = engine
            .store("User prefers formal responses")
            .with_scope(scope.clone())
            .enable_extraction(false)
            .execute()
            .await
            .unwrap();

        assert!(!outcome.operations.is_empty());

        // Search with agent filter
        let results = engine
            .search("response preferences")
            .with_scope(scope.clone())
            .execute()
            .await
            .unwrap();

        assert!(!results.facts.is_empty());

        // Clean up
        let _count = engine.clear(scope).await.unwrap();
    }
}