remem-ai 0.4.4

Persistent memory for Claude Code — single binary, zero subprocesses
Documentation
use axum::{body::to_bytes, extract::State, http::StatusCode, response::IntoResponse};
use rusqlite::params;
use serde_json::Value;

use crate::db::test_support::ScopedTestDataDir;
use crate::{db, memory};

use super::handlers::{handle_status, search_request_from_params};
use super::types::SearchParams;
use super::DbState;

#[test]
fn db_state_is_stateless() {
    assert_eq!(std::mem::size_of::<DbState>(), 0);
}

#[test]
fn search_request_from_params_clamps_limit_and_offset() {
    let request = search_request_from_params(SearchParams {
        query: Some("hello".to_string()),
        project: None,
        memory_type: None,
        limit: Some(999),
        offset: Some(-5),
        include_stale: None,
        branch: None,
        multi_hop: None,
    });

    assert_eq!(request.limit, 100);
    assert_eq!(request.offset, 0);
    // Canonical default for `include_stale` is `true` so MCP and REST agree.
    assert!(request.include_stale);
    assert!(!request.multi_hop);
}

#[test]
fn search_request_from_params_preserves_filters() {
    let request = search_request_from_params(SearchParams {
        query: Some("hello".to_string()),
        project: Some("proj".to_string()),
        memory_type: Some("decision".to_string()),
        limit: Some(8),
        offset: Some(3),
        include_stale: Some(true),
        branch: Some("main".to_string()),
        multi_hop: Some(true),
    });

    assert_eq!(request.query.as_deref(), Some("hello"));
    assert_eq!(request.project.as_deref(), Some("proj"));
    assert_eq!(request.memory_type.as_deref(), Some("decision"));
    assert_eq!(request.limit, 8);
    assert_eq!(request.offset, 3);
    assert!(request.include_stale);
    assert_eq!(request.branch.as_deref(), Some("main"));
    assert!(request.multi_hop);
}

#[tokio::test]
async fn status_handler_reopens_database_after_file_removal() {
    let test_dir = ScopedTestDataDir::new("api-status");

    let first = handle_status(State(DbState)).await.into_response();
    assert_eq!(first.status(), StatusCode::OK);
    assert!(test_dir.db_path().exists());

    test_dir.remove_db_files();
    assert!(!test_dir.db_path().exists());

    let second = handle_status(State(DbState)).await.into_response();
    assert_eq!(second.status(), StatusCode::OK);
    assert!(test_dir.db_path().exists());
}

#[tokio::test]
async fn status_handler_matches_shared_system_stats() {
    let _test_dir = ScopedTestDataDir::new("api-status-stats");
    let conn = db::open_db().expect("db should open");

    memory::insert_memory(
        &conn,
        Some("session-1"),
        "proj-a",
        None,
        "active memory",
        "kept",
        "decision",
        None,
    )
    .expect("active memory insert should succeed");
    let archived_memory_id = memory::insert_memory(
        &conn,
        Some("session-2"),
        "proj-a",
        None,
        "archived memory",
        "hidden",
        "decision",
        None,
    )
    .expect("archived memory insert should succeed");
    conn.execute(
        "UPDATE memories SET status = 'archived' WHERE id = ?1",
        params![archived_memory_id],
    )
    .expect("memory archive update should succeed");

    db::insert_observation(
        &conn,
        "session-1",
        "proj-a",
        "feature",
        Some("active observation"),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        1,
    )
    .expect("active observation insert should succeed");
    let stale_observation_id = db::insert_observation(
        &conn,
        "session-2",
        "proj-a",
        "feature",
        Some("stale observation"),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        1,
    )
    .expect("stale observation insert should succeed");
    conn.execute(
        "UPDATE observations SET status = 'stale' WHERE id = ?1",
        params![stale_observation_id],
    )
    .expect("observation stale update should succeed");

    let stats = db::query_system_stats(&conn).expect("system stats should load");
    drop(conn);

    let response = handle_status(State(DbState)).await.into_response();
    assert_eq!(response.status(), StatusCode::OK);

    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("response body should read");
    let payload: Value =
        serde_json::from_slice(&body).expect("status response should be valid json");
    assert_eq!(payload["memories"], stats.active_memories);
    assert_eq!(payload["observations"], stats.active_observations);
    assert_eq!(payload["captured_events"], stats.captured_events);
    assert_eq!(
        payload["pending_extraction_tasks"],
        stats.pending_extraction_tasks
    );
}