remem-ai 0.5.96

Persistent memory for Claude Code and OpenAI Codex coding agents
Documentation
use anyhow::Result;
use rusqlite::{params, Connection};

use super::{check_duplicate, find_hash_duplicates, mark_duplicate_accessed};

fn setup_dedup_schema(conn: &Connection) -> Result<()> {
    conn.execute_batch(
        "CREATE TABLE observations (
            id INTEGER PRIMARY KEY,
            memory_session_id TEXT NOT NULL,
            project TEXT,
            type TEXT NOT NULL,
            title TEXT,
            subtitle TEXT,
            narrative TEXT,
            facts TEXT,
            concepts TEXT,
            files_read TEXT,
            files_modified TEXT,
            prompt_number INTEGER,
            created_at TEXT,
            created_at_epoch INTEGER,
            discovery_tokens INTEGER DEFAULT 0,
            status TEXT DEFAULT 'active',
            last_accessed_epoch INTEGER
        );

        CREATE TABLE sdk_sessions (
            id INTEGER PRIMARY KEY,
            content_session_id TEXT UNIQUE NOT NULL,
            memory_session_id TEXT NOT NULL,
            project TEXT,
            user_prompt TEXT,
            started_at TEXT,
            started_at_epoch INTEGER,
            status TEXT DEFAULT 'active',
            prompt_counter INTEGER DEFAULT 1
        )",
    )?;
    Ok(())
}

fn insert_observation(conn: &Connection, project: &str, narrative: &str) -> Result<i64> {
    let now = chrono::Utc::now();
    conn.execute(
        "INSERT INTO observations \
         (memory_session_id, project, type, title, narrative, created_at, created_at_epoch, discovery_tokens, status) \
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
        params![
            "mem-test",
            project,
            "bugfix",
            "Auth fix",
            narrative,
            now.to_rfc3339(),
            now.timestamp(),
            100,
            "active"
        ],
    )?;
    Ok(conn.last_insert_rowid())
}

#[test]
fn test_hash_dedup_finds_exact_match() -> Result<()> {
    let conn = Connection::open_in_memory()?;
    setup_dedup_schema(&conn)?;

    let narrative = "Fixed authentication bug in login flow";
    insert_observation(&conn, "test-project", narrative)?;

    let content_hash = crate::db::content_identity_hash(narrative.as_bytes());
    let dups = find_hash_duplicates(&conn, "test-project", &content_hash, 900)?;

    assert_eq!(dups.len(), 1);
    Ok(())
}

#[test]
fn test_hash_dedup_accepts_legacy_fnv_hash() -> Result<()> {
    let conn = Connection::open_in_memory()?;
    setup_dedup_schema(&conn)?;

    let narrative = "Fixed authentication bug in login flow";
    insert_observation(&conn, "test-project", narrative)?;

    let legacy_hash = crate::db::legacy_content_identity_hash(narrative.as_bytes());
    let dups = find_hash_duplicates(&conn, "test-project", &legacy_hash, 900)?;

    assert_eq!(dups.len(), 1);
    Ok(())
}

#[test]
fn mark_duplicate_accessed_updates_timestamp() -> Result<()> {
    let conn = Connection::open_in_memory()?;
    setup_dedup_schema(&conn)?;

    let id = insert_observation(&conn, "test-project", "same narrative")?;
    mark_duplicate_accessed(&conn, &[id])?;

    let last_accessed: Option<i64> = conn.query_row(
        "SELECT last_accessed_epoch FROM observations WHERE id = ?1",
        params![id],
        |row| row.get(0),
    )?;
    assert!(last_accessed.is_some());
    Ok(())
}

#[test]
fn check_duplicate_returns_first_hash_duplicate() -> Result<()> {
    let conn = Connection::open_in_memory()?;
    setup_dedup_schema(&conn)?;

    let first = insert_observation(&conn, "test-project", "same narrative")?;
    let second = insert_observation(&conn, "test-project", "same narrative")?;

    let duplicate_id = check_duplicate(&conn, "test-project", "same narrative", None)?;

    assert_eq!(duplicate_id, Some(first));
    let last_accessed: Vec<Option<i64>> = [first, second]
        .iter()
        .map(|id| {
            conn.query_row(
                "SELECT last_accessed_epoch FROM observations WHERE id = ?1",
                params![id],
                |row| row.get(0),
            )
        })
        .collect::<rusqlite::Result<Vec<_>>>()?;
    assert!(last_accessed.iter().all(|value| value.is_some()));
    Ok(())
}