remem-ai 0.6.3

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use super::*;
use crate::memory::tests_helper::setup_memory_schema;
use crate::memory::{
    insert_memory, search_memories_fts, search_memories_fts_filtered, search_memories_like,
};
use rusqlite::Connection;

#[test]
fn test_memory_insert_and_query() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    let id = insert_memory(
        &conn,
        Some("session-1"),
        "test/proj",
        None,
        "FTS5 supports CJK",
        "Switched from unicode61 to trigram tokenizer for Chinese text search.",
        "decision",
        Some(r#"["src/db.rs"]"#),
    )
    .unwrap();
    assert!(id > 0);

    let memories = get_recent_memories(&conn, "test/proj", 10).unwrap();
    assert_eq!(memories.len(), 1);
    assert_eq!(memories[0].title, "FTS5 supports CJK");
}

#[test]
fn mark_memories_accessed_updates_usage_columns_once_per_call() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    let first = insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "Usage target",
        "Accessed through full-detail retrieval.",
        "decision",
        None,
    )
    .unwrap();
    let second = insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "Other target",
        "Also accessed through full-detail retrieval.",
        "decision",
        None,
    )
    .unwrap();

    mark_memories_accessed(&conn, &[first, second, first]).unwrap();
    mark_memories_accessed(&conn, &[first]).unwrap();

    let first_usage: (i64, Option<i64>) = conn
        .query_row(
            "SELECT access_count, last_accessed_epoch FROM memories WHERE id = ?1",
            [first],
            |row| Ok((row.get(0)?, row.get(1)?)),
        )
        .unwrap();
    let second_usage: (i64, Option<i64>) = conn
        .query_row(
            "SELECT access_count, last_accessed_epoch FROM memories WHERE id = ?1",
            [second],
            |row| Ok((row.get(0)?, row.get(1)?)),
        )
        .unwrap();

    assert_eq!(first_usage.0, 2);
    assert!(first_usage.1.is_some());
    assert_eq!(second_usage.0, 1);
    assert!(second_usage.1.is_some());
}

#[test]
fn get_memories_by_ids_hides_policy_suppressed_rows_by_default() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    let visible = insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "Visible target",
        "Visible direct-read result.",
        "decision",
        None,
    )
    .unwrap();
    let hidden = insert_memory(
        &conn,
        Some("s2"),
        "proj",
        None,
        "Hidden target",
        "Hidden direct-read result.",
        "decision",
        None,
    )
    .unwrap();
    crate::memory::suppression::create_suppression(
        &conn,
        &crate::memory::suppression::SuppressRequest {
            target: crate::memory::suppression::parse_target(&format!("memory:{hidden}")).unwrap(),
            reason: Some("not useful"),
            actor: Some("test"),
        },
    )
    .unwrap();

    let default = get_memories_by_ids(&conn, &[visible, hidden], Some("proj")).unwrap();
    let default_ids = default.iter().map(|memory| memory.id).collect::<Vec<_>>();
    assert_eq!(default_ids, vec![visible]);

    let audit =
        get_memories_by_ids_with_suppressed_policy(&conn, &[visible, hidden], Some("proj"), true)
            .unwrap();
    let audit_ids = audit.iter().map(|memory| memory.id).collect::<Vec<_>>();
    assert!(audit_ids.contains(&visible));
    assert!(audit_ids.contains(&hidden));
}

#[test]
fn test_memory_fts_search() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "FTS5 trigram tokenizer 支持 CJK",
        "Switched to trigram for Chinese search support.",
        "decision",
        None,
    )
    .unwrap();
    insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "Auth middleware rewrite",
        "Rewrote auth middleware for compliance.",
        "architecture",
        None,
    )
    .unwrap();

    let results = search_memories_fts(&conn, "trigram", Some("proj"), None, 10, 0).unwrap();
    assert_eq!(results.len(), 1);
    assert!(results[0].title.contains("trigram"));
}

fn insert_fts_fixture_memory(conn: &Connection, id: i64, title: &str, status: &str) {
    conn.execute(
        "INSERT INTO memories (
            id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status
         ) VALUES (?1, 'proj', ?2, ?2, 'decision', 100, 100, ?3)",
        rusqlite::params![id, title, status],
    )
    .unwrap();
}

#[test]
fn fixture_fts_indexes_all_statuses_and_filters_visibility_in_queries() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);
    insert_fts_fixture_memory(&conn, 1, "zookeeper active", "active");
    insert_fts_fixture_memory(&conn, 2, "zookeeper stale", "stale");
    insert_fts_fixture_memory(&conn, 3, "zookeeper archived", "archived");

    let active_only =
        search_memories_fts_filtered(&conn, "zookeeper", Some("proj"), None, 10, 0, false, None)
            .unwrap();
    assert_eq!(
        active_only
            .iter()
            .map(|memory| memory.id)
            .collect::<Vec<_>>(),
        vec![1]
    );

    let all_statuses =
        search_memories_fts_filtered(&conn, "zookeeper", Some("proj"), None, 10, 0, true, None)
            .unwrap();
    let mut ids = all_statuses
        .iter()
        .map(|memory| memory.id)
        .collect::<Vec<_>>();
    ids.sort_unstable();
    assert_eq!(ids, vec![1, 2, 3]);
}

#[test]
fn fixture_fts_preserves_rows_across_status_transitions_and_delete() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);
    insert_fts_fixture_memory(&conn, 1, "cassandra transition", "active");

    conn.execute("UPDATE memories SET status = 'stale' WHERE id = 1", [])
        .unwrap();
    let stale =
        search_memories_fts_filtered(&conn, "cassandra", Some("proj"), None, 10, 0, true, None)
            .unwrap();
    assert_eq!(stale.len(), 1);
    assert_eq!(stale[0].status, "stale");
    assert!(search_memories_fts_filtered(
        &conn,
        "cassandra",
        Some("proj"),
        None,
        10,
        0,
        false,
        None,
    )
    .unwrap()
    .is_empty());

    conn.execute("UPDATE memories SET status = 'active' WHERE id = 1", [])
        .unwrap();
    assert_eq!(
        search_memories_fts_filtered(&conn, "cassandra", Some("proj"), None, 10, 0, false, None,)
            .unwrap()
            .len(),
        1
    );

    conn.execute("DELETE FROM memories WHERE id = 1", [])
        .unwrap();
    assert!(search_memories_fts_filtered(
        &conn,
        "cassandra",
        Some("proj"),
        None,
        10,
        0,
        true,
        None
    )
    .unwrap()
    .is_empty());
}

fn memory_fts_trigger_sql(conn: &Connection) -> Vec<(String, String)> {
    let mut stmt = conn
        .prepare(
            "SELECT name, sql FROM sqlite_master
             WHERE type = 'trigger' AND name IN ('memories_ai', 'memories_au', 'memories_ad')
             ORDER BY name",
        )
        .unwrap();
    stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
        .unwrap()
        .collect::<rusqlite::Result<Vec<_>>>()
        .unwrap()
}

#[test]
fn fixture_fts_triggers_match_canonical_migrations() {
    let fixture = Connection::open_in_memory().unwrap();
    setup_memory_schema(&fixture);

    let migrated = Connection::open_in_memory().unwrap();
    crate::migrate::run_migrations(&migrated).unwrap();

    assert_eq!(
        memory_fts_trigger_sql(&fixture),
        memory_fts_trigger_sql(&migrated)
    );
}

#[test]
fn test_memory_like_fallback() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "DB schema migration",
        "Updated schema from v7 to v8.",
        "decision",
        None,
    )
    .unwrap();

    let results = search_memories_like(&conn, &["DB"], Some("proj"), None, 10, 0).unwrap();
    assert_eq!(results.len(), 1);
    assert!(results[0].title.contains("DB"));
}

#[test]
fn test_memory_type_filter() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "Bug: unicode61 fails CJK",
        "Root cause: unicode61 tokenizer doesn't segment Chinese.",
        "bugfix",
        None,
    )
    .unwrap();
    insert_memory(
        &conn,
        Some("s1"),
        "proj",
        None,
        "Use trigram tokenizer",
        "Decided to use trigram for CJK support.",
        "decision",
        None,
    )
    .unwrap();

    assert_eq!(
        get_memories_by_type(&conn, "proj", "bugfix", 10)
            .unwrap()
            .len(),
        1
    );
    assert_eq!(
        get_memories_by_type(&conn, "proj", "decision", 10)
            .unwrap()
            .len(),
        1
    );
}