remem-ai 0.5.11

Persistent memory for Claude Code and Codex — single binary, automatic context
Documentation
use super::{save_memory, SaveMemoryRequest};
use crate::db::{self, test_support::ScopedTestDataDir};

#[test]
fn repeated_lesson_save_reinforces_metadata_and_logs_update() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("lesson-save-reinforces");
    let conn = db::open_db()?;
    let req = SaveMemoryRequest {
        text: "Lesson: keep operation audit without losing lesson reinforcement.".to_string(),
        title: Some("Lesson reinforcement".to_string()),
        project: Some("proj".to_string()),
        topic_key: Some("lesson-reinforcement".to_string()),
        memory_type: Some("lesson".to_string()),
        local_copy_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };

    let first = save_memory(&conn, &req)?;
    let second = save_memory(&conn, &req)?;

    assert_eq!(first.id, second.id);
    assert_eq!(second.operation, "update");
    let reinforcement_count: i64 = conn.query_row(
        "SELECT reinforcement_count FROM memory_lessons WHERE memory_id = ?1",
        [first.id],
        |row| row.get(0),
    )?;
    let operations = conn
        .prepare("SELECT operation FROM memory_operation_log ORDER BY id ASC")?
        .query_map([], |row| row.get::<_, String>(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;
    assert_eq!(reinforcement_count, 2);
    assert_eq!(operations, vec!["add".to_string(), "update".to_string()]);
    Ok(())
}

#[test]
fn direct_save_refreshes_expired_same_text_current_fact() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("save-expired-current-fact-refresh");
    let conn = db::open_db()?;
    let req = SaveMemoryRequest {
        text: "Local dev server is currently running at localhost:5173.".to_string(),
        title: Some("Dev server status".to_string()),
        project: Some("proj".to_string()),
        topic_key: Some("repo:proj:dev-server".to_string()),
        memory_type: Some("discovery".to_string()),
        scope: Some("project".to_string()),
        local_copy_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };
    let first = save_memory(&conn, &req)?;
    conn.execute(
        "UPDATE memories
         SET expires_at_epoch = ?1, valid_from_epoch = ?2
         WHERE id = ?3",
        rusqlite::params![1_i64, 0_i64, first.id],
    )?;

    let second = save_memory(&conn, &req)?;

    assert_eq!(second.id, first.id);
    assert_eq!(second.operation, "update");
    let (status, expires_at): (String, i64) = conn.query_row(
        "SELECT status, expires_at_epoch FROM memories WHERE id = ?1",
        [first.id],
        |row| Ok((row.get(0)?, row.get(1)?)),
    )?;
    assert_eq!(status, "active");
    assert!(
        expires_at > chrono::Utc::now().timestamp(),
        "same-text save should refresh expired currentness metadata"
    );
    let operation: String = conn.query_row(
        "SELECT operation FROM memory_operation_log ORDER BY id DESC LIMIT 1",
        [],
        |row| row.get(0),
    )?;
    assert_eq!(operation, "update");
    Ok(())
}

#[test]
fn direct_save_same_text_metadata_change_updates_row() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("save-same-text-metadata-update");
    let conn = db::open_db()?;
    let first_req = SaveMemoryRequest {
        text: "This fact keeps the same content while durable metadata changes.".to_string(),
        title: Some("Old title".to_string()),
        project: Some("proj".to_string()),
        topic_key: Some("metadata-refresh".to_string()),
        memory_type: Some("discovery".to_string()),
        files: Some(vec!["src/old.rs".to_string()]),
        branch: Some("main".to_string()),
        scope: Some("project".to_string()),
        local_copy_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };
    let first = save_memory(&conn, &first_req)?;
    let second_req = SaveMemoryRequest {
        title: Some("New title".to_string()),
        files: Some(vec!["src/new.rs".to_string()]),
        branch: Some("feature".to_string()),
        ..first_req
    };

    let second = save_memory(&conn, &second_req)?;

    assert_eq!(second.id, first.id);
    assert_eq!(second.operation, "update");
    let (title, files, branch): (String, Option<String>, Option<String>) = conn.query_row(
        "SELECT title, files, branch FROM memories WHERE id = ?1",
        [first.id],
        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
    )?;
    assert_eq!(title, "New title");
    assert_eq!(files.as_deref(), Some("[\"src/new.rs\"]"));
    assert_eq!(branch.as_deref(), Some("feature"));
    Ok(())
}

#[test]
fn direct_save_updates_active_duplicate_topic_row() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("save-active-topic-duplicate-target");
    let conn = db::open_db()?;
    let req = SaveMemoryRequest {
        text: "Use the current active duplicate row for planner-aligned writes.".to_string(),
        title: Some("Planner target".to_string()),
        project: Some("proj".to_string()),
        topic_key: Some("planner-target".to_string()),
        memory_type: Some("discovery".to_string()),
        scope: Some("project".to_string()),
        local_copy_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };
    let stale = save_memory(&conn, &req)?;
    conn.execute(
        "UPDATE memories
         SET status = 'stale', updated_at_epoch = ?1
         WHERE id = ?2",
        rusqlite::params![10_i64, stale.id],
    )?;
    conn.execute(
        "INSERT INTO memories
         (session_id, project, topic_key, title, content, memory_type, files, search_context,
          created_at_epoch, updated_at_epoch, status, branch, scope,
          source_project, target_project, owner_scope, owner_key, context_class)
         VALUES (?1, 'proj', 'planner-target', 'Current active row',
                 'Existing active duplicate content.', 'discovery', NULL,
                 'Existing active duplicate content.', ?2, ?3, 'active', NULL, 'project',
                 'proj', 'proj', 'repo', 'proj', 'startup_core')",
        rusqlite::params!["legacy-active-session", 20_i64, 20_i64],
    )?;
    let active = conn.last_insert_rowid();
    let update_req = SaveMemoryRequest {
        text: "Updated planner-aligned content.".to_string(),
        title: Some("Planner target updated".to_string()),
        ..req
    };

    let saved = save_memory(&conn, &update_req)?;

    assert_eq!(saved.id, active);
    assert_eq!(saved.operation, "update");
    let stale_status: String = conn.query_row(
        "SELECT status FROM memories WHERE id = ?1",
        [stale.id],
        |row| row.get(0),
    )?;
    assert_eq!(stale_status, "stale");
    let active_content: String = conn.query_row(
        "SELECT content FROM memories WHERE id = ?1",
        [active],
        |row| row.get(0),
    )?;
    assert_eq!(active_content, "Updated planner-aligned content.");
    let active_topic_rows: i64 = conn.query_row(
        "SELECT COUNT(*) FROM memories
         WHERE project = 'proj'
           AND topic_key = 'planner-target'
           AND scope = 'project'
           AND status = 'active'",
        [],
        |row| row.get(0),
    )?;
    assert_eq!(active_topic_rows, 1);
    let logged_result: i64 = conn.query_row(
        "SELECT result_memory_id FROM memory_operation_log ORDER BY id DESC LIMIT 1",
        [],
        |row| row.get(0),
    )?;
    assert_eq!(logged_result, active);
    Ok(())
}

#[test]
fn save_memory_creates_manual_claim_after_successful_memory_write() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("save-memory-claim-success");
    let conn = db::open_db()?;
    let req = SaveMemoryRequest {
        text: "Use exact session memory claims to suppress duplicate summary candidates."
            .to_string(),
        title: Some("Session claims".to_string()),
        project: Some("proj".to_string()),
        session_id: Some("session-claim-success".to_string()),
        host: Some("codex-cli".to_string()),
        memory_type: Some("decision".to_string()),
        local_copy_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };

    let saved = save_memory(&conn, &req)?;

    assert_eq!(saved.claim_status, "saved");
    let claim_id = saved
        .claim_id
        .ok_or_else(|| anyhow::anyhow!("claim id should be returned"))?;
    assert_eq!(saved.claim_error, None);
    let claim: (i64, String, String, String, String, String) = conn.query_row(
        "SELECT memory_id, project, session_id, host, claim_source, memory_type
         FROM memory_claims
         WHERE id = ?1",
        [claim_id],
        |row| {
            Ok((
                row.get(0)?,
                row.get(1)?,
                row.get(2)?,
                row.get(3)?,
                row.get(4)?,
                row.get(5)?,
            ))
        },
    )?;
    assert_eq!(claim.0, saved.id);
    assert_eq!(claim.1, "proj");
    assert_eq!(claim.2, "session-claim-success");
    assert_eq!(claim.3, "codex-cli");
    assert_eq!(claim.4, "manual_save");
    assert_eq!(claim.5, "decision");
    Ok(())
}

#[test]
fn save_memory_claim_disabled_preserves_existing_behavior() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("save-memory-claim-disabled");
    let conn = db::open_db()?;
    let req = SaveMemoryRequest {
        text: "Claim disabled should still save durable memory.".to_string(),
        title: Some("Claim disabled".to_string()),
        project: Some("proj".to_string()),
        session_id: Some("session-claim-disabled".to_string()),
        memory_type: Some("discovery".to_string()),
        local_copy_enabled: Some(false),
        claim_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };

    let saved = save_memory(&conn, &req)?;

    assert_eq!(saved.status, "saved");
    assert_eq!(saved.claim_status, "disabled");
    assert_eq!(saved.claim_id, None);
    assert_eq!(saved.claim_error, None);
    let claim_count: i64 =
        conn.query_row("SELECT COUNT(*) FROM memory_claims", [], |row| row.get(0))?;
    assert_eq!(claim_count, 0);
    Ok(())
}

#[test]
fn save_memory_claim_write_failure_is_reported_not_silent() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("save-memory-claim-failure");
    let conn = db::open_db()?;
    conn.execute("DROP TABLE memory_claims", [])?;
    let req = SaveMemoryRequest {
        text: "Durable memory should survive a claim write failure.".to_string(),
        title: Some("Claim failure".to_string()),
        project: Some("proj".to_string()),
        session_id: Some("session-claim-failure".to_string()),
        memory_type: Some("discovery".to_string()),
        local_copy_enabled: Some(false),
        ..SaveMemoryRequest::default()
    };

    let saved = save_memory(&conn, &req)?;

    assert_eq!(saved.status, "saved");
    assert_eq!(saved.claim_status, "failed");
    assert_eq!(saved.claim_id, None);
    assert!(saved
        .claim_error
        .as_deref()
        .is_some_and(|error| error.contains("memory_claims")));
    let memory_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM memories WHERE id = ?1",
        [saved.id],
        |row| row.get(0),
    )?;
    assert_eq!(memory_count, 1);
    Ok(())
}