remem-ai 0.5.145

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::Result;
use rusqlite::Connection;

use super::*;

fn setup_vector_conn() -> Result<Connection> {
    let conn = Connection::open_in_memory()?;
    crate::migrate::run_migrations(&conn)?;
    Ok(conn)
}

fn insert_test_memory(conn: &Connection, id: i64) -> Result<()> {
    conn.execute(
        "INSERT INTO memories
         (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status)
         VALUES (?1, '/repo', 'Credential store', 'SQLCipher encrypts secrets at rest.', 'architecture', 1, 1, 'active')",
        params![id],
    )?;
    Ok(())
}

#[test]
fn vector_search_returns_nearest_memory_embedding() -> Result<()> {
    let conn = setup_vector_conn()?;
    conn.execute(
        "INSERT INTO memories
         (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status)
         VALUES
         (1, '/repo', 'Credential store', 'SQLCipher encrypts secrets at rest.', 'architecture', 1, 1, 'active'),
         (2, '/repo', 'Posting workflow', 'Publish social media drafts after review.', 'procedure', 1, 1, 'active')",
        [],
    )?;
    upsert_memory_embedding(
        &conn,
        1,
        "Credential store",
        "SQLCipher encrypts secrets at rest.",
        "architecture",
        None,
    )?;
    upsert_memory_embedding(
        &conn,
        2,
        "Posting workflow",
        "Publish social media drafts after review.",
        "procedure",
        None,
    )?;

    let query = embed_query_text("How do we protect private persisted data?");
    let outcome = vector_search_filtered(
        &conn,
        &query,
        VectorSearchFilters {
            project: Some("/repo"),
            ..VectorSearchFilters::default()
        },
        5,
    )?;

    assert!(outcome.disabled_reason.is_none());
    assert_eq!(outcome.hits[0].memory_id, 1);
    Ok(())
}

#[test]
fn vector_search_respects_filters() -> Result<()> {
    let conn = setup_vector_conn()?;
    for (id, project, branch, memory_type, status) in [
        (1, "/repo", Some("main"), "architecture", "active"),
        (2, "/other", Some("main"), "architecture", "active"),
        (3, "/repo", Some("feature"), "architecture", "active"),
        (4, "/repo", Some("main"), "decision", "active"),
        (5, "/repo", Some("main"), "architecture", "stale"),
    ] {
        conn.execute(
            "INSERT INTO memories
             (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status, branch)
             VALUES (?1, ?2, 'Credential store', 'SQLCipher encrypts secrets at rest.', ?3, 1, 1, ?4, ?5)",
            params![id, project, memory_type, status, branch],
        )?;
        upsert_memory_embedding(
            &conn,
            id,
            "Credential store",
            "SQLCipher encrypts secrets at rest.",
            memory_type,
            None,
        )?;
    }

    let query = embed_query_text("protect private persisted data");
    let outcome = vector_search_filtered(
        &conn,
        &query,
        VectorSearchFilters {
            project: Some("/repo"),
            branch: Some("main"),
            memory_type: Some("architecture"),
            include_stale: false,
        },
        10,
    )?;
    let ids: Vec<i64> = outcome.hits.iter().map(|hit| hit.memory_id).collect();

    assert_eq!(ids, vec![1]);
    Ok(())
}

#[test]
fn vector_search_uses_profile_memory_id_index_for_embedding_fetch() -> Result<()> {
    let conn = setup_vector_conn()?;
    insert_test_memory(&conn, 1)?;
    upsert_memory_embedding(
        &conn,
        1,
        "Credential store",
        "SQLCipher encrypts secrets at rest.",
        "architecture",
        None,
    )?;

    let plan = conn
        .prepare(
            "EXPLAIN QUERY PLAN
             SELECT memory_id, embedding, dimensions
             FROM memory_embeddings INDEXED BY idx_memory_embeddings_profile_memory_id
             WHERE model = ?1 AND dimensions = ?2 AND memory_id IN (?3)",
        )?
        .query_map(
            params![DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS as i64, 1_i64],
            |row| row.get::<_, String>(3),
        )?
        .collect::<rusqlite::Result<Vec<_>>>()?;

    assert!(
        plan.iter()
            .any(|detail| detail.contains("idx_memory_embeddings_profile_memory_id")),
        "embedding fetch should use profile memory_id index, got {plan:#?}"
    );
    Ok(())
}

#[test]
fn explicit_embedding_backfill_covers_all_statuses_across_batches() -> Result<()> {
    let conn = setup_vector_conn()?;
    for id in 1..=1_002 {
        let status = match id {
            1 => "stale",
            2 => "archived",
            _ => "active",
        };
        conn.execute(
            "INSERT INTO memories
             (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status)
             VALUES (?1, '/repo', 'Backfill memory', 'Backfill should cover all visible statuses.', 'decision', 1, ?1, ?2)",
            params![id, status],
        )?;
    }

    ensure_vec_table(&conn)?;
    assert_eq!(backfill_missing_memory_embeddings(&conn, 1_000)?, 1_000);
    assert_eq!(backfill_missing_memory_embeddings(&conn, 1_000)?, 2);

    let count: i64 = conn.query_row("SELECT COUNT(*) FROM memory_embeddings", [], |row| {
        row.get(0)
    })?;
    assert_eq!(count, 1_002);
    for status in ["stale", "archived"] {
        let status_count: i64 = conn.query_row(
            "SELECT COUNT(*)
             FROM memory_embeddings e
             JOIN memories m ON m.id = e.memory_id
             WHERE m.status = ?1",
            [status],
            |row| row.get(0),
        )?;
        assert_eq!(status_count, 1);
    }
    Ok(())
}

#[test]
fn reindex_report_includes_profile_timings_and_remaining_work() -> Result<()> {
    let conn = setup_vector_conn()?;
    for id in 1..=3 {
        conn.execute(
            "INSERT INTO memories
             (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status)
             VALUES (?1, '/repo', 'Backfill memory', 'Measured backfill report.', 'decision', 1, ?1, 'active')",
            params![id],
        )?;
    }

    ensure_vec_table(&conn)?;
    let report = reindex_memory_embeddings_with_report(&conn, 2)?;

    assert_eq!(report.selected, 2);
    assert_eq!(report.processed, 2);
    assert_eq!(report.model, DEFAULT_EMBEDDING_MODEL);
    assert_eq!(report.dimensions, EMBEDDING_DIMENSIONS);
    assert_eq!(pending_memory_embedding_reindex_count(&conn)?, 1);

    let phases: Vec<&str> = report
        .timings
        .iter()
        .map(|timing| timing.phase.as_str())
        .collect();
    for expected in [
        "profile_probe",
        "select_pending",
        "embed_memory",
        "upsert_embeddings",
        "commit",
        "total",
    ] {
        assert!(
            phases.contains(&expected),
            "missing timing phase {expected}; got {phases:?}"
        );
    }
    Ok(())
}

#[test]
fn reindex_batch_rolls_back_failed_upserts() -> Result<()> {
    let conn = setup_vector_conn()?;
    for (id, updated_at_epoch) in [(1_i64, 2_i64), (2, 1)] {
        conn.execute(
            "INSERT INTO memories
             (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status)
             VALUES (?1, '/repo', 'Backfill memory', 'Batch rollback should be atomic.', 'decision', 1, ?2, 'active')",
            params![id, updated_at_epoch],
        )?;
    }
    conn.execute_batch(
        "CREATE TRIGGER fail_embedding_for_memory_2
         BEFORE INSERT ON memory_embeddings
         WHEN NEW.memory_id = 2
         BEGIN
             SELECT RAISE(FAIL, 'forced embedding failure');
         END;",
    )?;

    let error = reindex_memory_embeddings_with_report(&conn, 2).unwrap_err();
    let message = format!("{error:#}");
    assert!(message.contains("memory id=2"), "{message}");
    assert!(message.contains("forced embedding failure"), "{message}");
    assert_eq!(embedding_count(&conn)?, 0);
    assert_eq!(pending_memory_embedding_reindex_count(&conn)?, 2);
    Ok(())
}

#[test]
fn vector_search_ignores_embeddings_from_other_models() -> Result<()> {
    let conn = setup_vector_conn()?;
    insert_test_memory(&conn, 1)?;
    upsert_memory_embedding(
        &conn,
        1,
        "Credential store",
        "SQLCipher encrypts secrets at rest.",
        "architecture",
        None,
    )?;

    let query = TextEmbedding::new("remote-test-model", vec![0.1, 0.2, 0.3])?;
    let outcome = vector_search_embedding_filtered(
        &conn,
        &query,
        VectorSearchFilters {
            project: Some("/repo"),
            ..VectorSearchFilters::default()
        },
        5,
    )?;

    assert!(outcome.hits.is_empty());
    assert!(outcome
        .disabled_reason
        .as_deref()
        .unwrap_or("")
        .contains("remote-test-model"));
    Ok(())
}

#[test]
fn backfill_rebuilds_embeddings_from_stale_model() -> Result<()> {
    let conn = setup_vector_conn()?;
    insert_test_memory(&conn, 1)?;
    let stale_blob = vec![0u8; 3 * std::mem::size_of::<f32>()];
    conn.execute(
        "INSERT INTO memory_embeddings
         (memory_id, embedding, dimensions, model, content_hash, updated_at_epoch)
         VALUES (1, ?1, 3, 'old-model', 'old-hash', 1)",
        params![stale_blob],
    )?;

    assert_eq!(pending_memory_embedding_count(&conn)?, 1);
    assert_eq!(backfill_missing_memory_embeddings(&conn, 100)?, 1);

    let row: (String, i64) = conn.query_row(
        "SELECT model, dimensions FROM memory_embeddings WHERE memory_id = 1",
        [],
        |row| Ok((row.get(0)?, row.get(1)?)),
    )?;
    assert_eq!(row.0, DEFAULT_EMBEDDING_MODEL);
    assert_eq!(row.1, EMBEDDING_DIMENSIONS as i64);
    assert_eq!(pending_memory_embedding_count(&conn)?, 0);
    Ok(())
}

#[test]
fn reindex_rebuilds_embeddings_when_memory_is_newer_than_embedding() -> Result<()> {
    let conn = setup_vector_conn()?;
    insert_test_memory(&conn, 1)?;
    upsert_memory_embedding(
        &conn,
        1,
        "Credential store",
        "SQLCipher encrypts secrets at rest.",
        "architecture",
        None,
    )?;
    let before_hash: String = conn.query_row(
        "SELECT content_hash FROM memory_embeddings WHERE memory_id = 1",
        [],
        |row| row.get(0),
    )?;
    conn.execute(
        "UPDATE memory_embeddings SET updated_at_epoch = 1 WHERE memory_id = 1",
        [],
    )?;
    conn.execute(
        "UPDATE memories
         SET content = 'SQLCipher protects the local database with encryption at rest.',
             updated_at_epoch = ?1
         WHERE id = 1",
        params![2],
    )?;

    assert_eq!(pending_memory_embedding_reindex_count(&conn)?, 1);
    assert_eq!(reindex_memory_embeddings(&conn, 100)?, 1);

    let after_hash: String = conn.query_row(
        "SELECT content_hash FROM memory_embeddings WHERE memory_id = 1",
        [],
        |row| row.get(0),
    )?;
    assert_ne!(after_hash, before_hash);
    assert_eq!(pending_memory_embedding_reindex_count(&conn)?, 0);
    Ok(())
}

#[test]
fn empty_vector_table_with_memories_is_reported_as_disabled() -> Result<()> {
    let conn = setup_vector_conn()?;
    conn.execute(
        "INSERT INTO memories
         (id, project, title, content, memory_type, created_at_epoch, updated_at_epoch, status)
         VALUES (1, '/repo', 'Needs embedding', 'Backfill should be explicit.', 'decision', 1, 1, 'active')",
        [],
    )?;
    let query = embed_query_text("needs embedding");

    let outcome = vector_search_filtered(&conn, &query, VectorSearchFilters::default(), 10)?;

    assert!(outcome.hits.is_empty());
    assert!(outcome
        .disabled_reason
        .as_deref()
        .unwrap_or("")
        .contains("reindex-embeddings"));
    Ok(())
}

#[test]
fn missing_vector_table_is_reported_as_disabled() -> Result<()> {
    let conn = Connection::open_in_memory()?;
    let query = embed_query_text("anything");
    let outcome = vector_search_filtered(&conn, &query, VectorSearchFilters::default(), 10)?;

    assert!(outcome
        .disabled_reason
        .as_deref()
        .unwrap_or("")
        .contains("memory_embeddings table is missing"));
    assert!(outcome.hits.is_empty());
    Ok(())
}