engram-memory 0.6.0

Core binary and shared infrastructure for Engram memory system
Documentation
use std::sync::{Arc, Mutex, RwLock};

use serde_json::json;

use engram_core::config::Config;
use engram_core::dispatch;
use engram_core::error::CoreError;
use engram_core::indexes::IndexSet;
use engram_core::server::ServerState;
use engram_embeddings::Embedder;
use engram_llm_client::{EmbeddingProvider, TextGenerator};
use engram_router::Router;
use engram_storage::Database;

fn build_test_state() -> Arc<ServerState> {
    let database = Database::in_memory().expect("in-memory database");
    let mut config = Config::default();
    config.embedding.provider = "deterministic".into();
    let indexes = IndexSet::new(|| config.build_hnsw_params()).expect("index set");
    let embedder = Embedder::new(0);
    let router = Router::new(0.1, 0.15);
    let embedding_provider: Arc<dyn EmbeddingProvider + Send + Sync> = Arc::from(
        config
            .build_embedding_provider()
            .expect("embedding provider"),
    );
    let text_generator: Option<Arc<dyn TextGenerator + Send + Sync>> =
        config.build_text_generator().ok().map(Arc::from);
    Arc::new(ServerState {
        database: Mutex::new(database),
        indexes: RwLock::new(indexes),
        embedder,
        router: Mutex::new(router),
        config,
        database_path: String::new(),
        embedding_provider,
        text_generator,
    })
}

fn build_test_state_with_deterministic_embeddings() -> Arc<ServerState> {
    let database = Database::in_memory().expect("in-memory database");
    let mut config = Config::default();
    config.embedding.provider = "deterministic".into();
    let indexes = IndexSet::new(|| config.build_hnsw_params()).expect("index set");
    let embedder = Embedder::new(0);
    let router = Router::new(0.1, 0.15);
    let embedding_provider: Arc<dyn EmbeddingProvider + Send + Sync> = Arc::from(
        config
            .build_embedding_provider()
            .expect("embedding provider"),
    );
    let text_generator: Option<Arc<dyn TextGenerator + Send + Sync>> =
        config.build_text_generator().ok().map(Arc::from);
    Arc::new(ServerState {
        database: Mutex::new(database),
        indexes: RwLock::new(indexes),
        embedder,
        router: Mutex::new(router),
        config,
        database_path: String::new(),
        embedding_provider,
        text_generator,
    })
}

#[tokio::test]
async fn dispatch_unknown_method_returns_error() {
    let state = build_test_state();
    let result = dispatch::route("nonexistent_method", &state, json!({})).await;
    let error = result.unwrap_err();
    match error {
        CoreError::DispatchError(message) => {
            assert!(message.contains("nonexistent_method"));
        }
        other => panic!("expected DispatchError, got: {other}"),
    }
}

#[tokio::test]
async fn status_handler_returns_counts() {
    let state = build_test_state();
    let result = dispatch::route("memory_status", &state, json!({})).await;
    let data = result.expect("status should succeed");
    assert_eq!(data["memory_count"], 0);
    assert_eq!(data["indexed_count"], 0);
    assert_eq!(data["pending_judgments"], 0);
    assert_eq!(data["index_size"], 0);
}

#[tokio::test]
async fn status_handler_reflects_inserted_memory() {
    let state = build_test_state();
    insert_test_memory(&state, "test-001");
    let result = dispatch::route("memory_status", &state, json!({})).await;
    let data = result.expect("status should succeed");
    assert_eq!(data["memory_count"], 1);
}

#[tokio::test]
async fn consolidate_preview_returns_empty_on_fresh_database() {
    let state = build_test_state();
    let params = json!({ "stale_days": 90, "min_score": 0.3 });
    let result = dispatch::route("memory_consolidate_preview", &state, params).await;
    let data = result.expect("preview should succeed");
    assert_eq!(data["duplicates"], 0);
    assert_eq!(data["stale"], 0);
    assert_eq!(data["garbage"], 0);
}

#[tokio::test]
async fn judge_handler_explicit_score_updates_memory() {
    let state = build_test_state();
    insert_test_memory(&state, "judge-test-001");
    let params = json!({
        "memory_id": "judge-test-001",
        "score": 0.85,
    });
    let result = dispatch::route("memory_judge", &state, params).await;
    let data = result.expect("judge should succeed");
    let returned_score = data["score"].as_f64().expect("score is a number");
    assert!((returned_score - 0.85).abs() < 0.001);
    assert_eq!(data["degraded"], false);
    let database = state.database.lock().unwrap();
    let memory = database
        .get_memory("judge-test-001")
        .expect("memory exists");
    assert!((memory.score - 0.85).abs() < 0.001);
}

#[tokio::test]
async fn judge_handler_missing_memory_returns_error() {
    let state = build_test_state();
    let params = json!({
        "memory_id": "nonexistent",
        "score": 0.5,
    });
    let result = dispatch::route("memory_judge", &state, params).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn store_handler_rejects_invalid_params() {
    let state = build_test_state();
    let params = json!({ "invalid": "data" });
    let result = dispatch::route("memory_store", &state, params).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn search_handler_rejects_missing_query() {
    let state = build_test_state();
    let params = json!({ "limit": 5 });
    let result = dispatch::route("memory_search", &state, params).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn store_handler_creates_memory() {
    let state = build_test_state_with_deterministic_embeddings();
    let params = json!({
        "memory_type": "decision",
        "context": "user configured the database connection for production deployment",
        "action": "set up connection pooling with maximum fifty connections per instance",
        "result": "database connections are now stable and performant under load",
    });
    let result = dispatch::route("memory_store", &state, params).await;
    let data = result.expect("store should succeed");
    let memory_id = data["id"].as_str().expect("id is a string");
    assert!(!memory_id.is_empty());
    assert_eq!(data["indexed"], true);

    let database = state.database.lock().unwrap();
    let memory = database
        .get_memory(memory_id)
        .expect("memory exists in database");
    assert_eq!(memory.memory_type, "decision");
    assert_eq!(
        memory.context,
        "user configured the database connection for production deployment"
    );
    assert!(memory.indexed);
}

#[tokio::test]
async fn search_handler_finds_stored_memory() {
    let state = build_test_state_with_deterministic_embeddings();
    let store_params = json!({
        "memory_type": "decision",
        "context": "the deployment pipeline runs automated integration tests before merging",
        "action": "configured continuous integration with three parallel test runners",
        "result": "all tests pass within five minutes on every pull request submission",
    });
    let store_result = dispatch::route("memory_store", &state, store_params).await;
    let stored = store_result.expect("store should succeed");
    let stored_id = stored["id"].as_str().expect("stored id");

    let search_params = json!({
        "query": "deployment pipeline integration tests continuous integration runners",
        "limit": 10,
    });
    let search_result = dispatch::route("memory_search", &state, search_params).await;
    let results = search_result.expect("search should succeed");
    let results_array = results["results"].as_array().expect("results is an array");

    let found = results_array.iter().any(|entry| entry["id"] == stored_id);
    assert!(found, "stored memory should appear in search results");
}

fn insert_test_memory(state: &Arc<ServerState>, id: &str) {
    let memory = engram_storage::Memory {
        id: id.to_string(),
        memory_type: "decision".to_string(),
        context: "test context".to_string(),
        action: "test action".to_string(),
        result: "test result".to_string(),
        score: 0.0,
        embedding_context: None,
        embedding_action: None,
        embedding_result: None,
        indexed: false,
        tags: None,
        project: None,
        parent_id: None,
        source_ids: None,
        insight_type: None,
        created_at: "2025-01-01T00:00:00Z".to_string(),
        updated_at: "2025-01-01T00:00:00Z".to_string(),
        used_count: 0,
        last_used_at: None,
        superseded_by: None,
    };
    let database = state.database.lock().unwrap();
    database.insert_memory(&memory).expect("insert memory");
}