remem-ai 0.4.9

Persistent memory for Claude Code and Codex — single binary, automatic context
Documentation
use anyhow::Result;
use rusqlite::Connection;

use crate::memory::{self, Memory};

use super::{
    add_preference, dedup_with_claude_md, query_global_preferences, query_project_preferences,
    remove_preference, render_preferences, render_preferences_with_limits,
    render_preferences_with_limits_detailed,
};

fn setup_test_db() -> Connection {
    let conn = Connection::open_in_memory()
        .unwrap_or_else(|err| panic!("Failed to open in-memory db: {err}"));
    crate::memory::types::tests_helper::setup_memory_schema(&conn);
    conn
}

#[test]
fn test_render_preferences_empty() -> Result<()> {
    let conn = setup_test_db();
    let mut output = String::new();
    render_preferences(&mut output, &conn, "test/proj", ".")?;
    assert!(
        output.is_empty(),
        "Should not render section when no preferences"
    );
    Ok(())
}

#[test]
fn test_render_preferences_with_data() -> Result<()> {
    let conn = setup_test_db();
    memory::insert_memory(
        &conn,
        None,
        "test/proj",
        Some("pref-1"),
        "Preference: Use Chinese comments",
        "Use Chinese comments in code",
        "preference",
        None,
    )?;

    let mut output = String::new();
    render_preferences(&mut output, &conn, "test/proj", "/nonexistent")?;
    assert!(output.contains("## Your Preferences"));
    assert!(output.contains("Use Chinese comments"));
    Ok(())
}

#[test]
fn test_project_preferences_exclude_global_overlay() -> Result<()> {
    let conn = setup_test_db();
    memory::insert_memory(
        &conn,
        None,
        "test/proj",
        Some("local-pref"),
        "Preference: Local workflow",
        "Use the local project workflow",
        "preference",
        None,
    )?;
    memory::insert_memory_full(
        &conn,
        None,
        "other/proj",
        Some("global-pref"),
        "Preference: AtlasCloud markdown",
        "Verify AtlasCloud markdown rendering",
        "preference",
        None,
        None,
        "global",
        None,
    )?;

    let prefs = query_project_preferences(&conn, "test/proj", 20)?;
    assert_eq!(prefs.len(), 1);
    assert!(prefs[0].text.contains("local project workflow"));
    assert!(!prefs[0].text.contains("AtlasCloud"));
    Ok(())
}

#[test]
fn test_render_preferences_global_limit_zero_does_not_leak_global() -> Result<()> {
    let conn = setup_test_db();
    memory::insert_memory_full(
        &conn,
        None,
        "infra/aip",
        Some("atlas-pref"),
        "Preference: AtlasCloud markdown",
        "Verify AtlasCloud markdown rendering",
        "preference",
        None,
        None,
        "global",
        None,
    )?;

    let mut output = String::new();
    let rendered = render_preferences_with_limits(
        &mut output,
        &conn,
        "work/life/x",
        "/nonexistent",
        20,
        0,
        1500,
    )?;

    assert_eq!(rendered, 0);
    assert!(!output.contains("AtlasCloud"));
    Ok(())
}

#[test]
fn test_render_preferences_global_limit_explicitly_opted_in() -> Result<()> {
    let conn = setup_test_db();
    memory::insert_memory_full(
        &conn,
        None,
        "infra/aip",
        Some("atlas-pref"),
        "Preference: AtlasCloud markdown",
        "Verify AtlasCloud markdown rendering",
        "preference",
        None,
        None,
        "global",
        None,
    )?;

    let mut output = String::new();
    let rendered = render_preferences_with_limits(
        &mut output,
        &conn,
        "work/life/x",
        "/nonexistent",
        20,
        1,
        1500,
    )?;

    assert_eq!(rendered, 1);
    assert!(output.contains("AtlasCloud"));
    Ok(())
}

#[test]
fn test_render_preferences_reports_project_global_split() -> Result<()> {
    let conn = setup_test_db();
    memory::insert_memory(
        &conn,
        None,
        "test/proj",
        Some("local-pref"),
        "Preference: Local workflow",
        "Use the local project workflow",
        "preference",
        None,
    )?;
    memory::insert_memory_full(
        &conn,
        None,
        "other/proj",
        Some("global-pref"),
        "Preference: Global reviews",
        "Review global release notes",
        "preference",
        None,
        None,
        "global",
        None,
    )?;

    let mut output = String::new();
    let summary = render_preferences_with_limits_detailed(
        &mut output,
        &conn,
        "test/proj",
        "/nonexistent",
        20,
        1,
        1500,
    )?;

    assert_eq!(summary.rendered, 2);
    assert_eq!(summary.project_rendered, 1);
    assert_eq!(summary.global_rendered, 1);
    assert!(output.contains("local project workflow"));
    assert!(output.contains("global release notes"));
    Ok(())
}

#[test]
fn test_global_preferences_require_explicit_global_scope() -> Result<()> {
    let conn = setup_test_db();
    for project in &["proj-a", "proj-b", "proj-c"] {
        memory::insert_memory(
            &conn,
            None,
            project,
            Some("global-pref-1"),
            "Preference: Terse responses",
            "Give terse responses without summaries",
            "preference",
            None,
        )?;
    }

    let global = query_global_preferences(&conn, 10)?;
    assert!(
        global.is_empty(),
        "Repeated project-scoped topic_key values must not become global preferences"
    );

    memory::insert_memory_full(
        &conn,
        None,
        "proj-a",
        Some("explicit-global"),
        "Preference: Explicit global",
        "Use explicit global preferences only",
        "preference",
        None,
        None,
        "global",
        None,
    )?;

    let global = query_global_preferences(&conn, 10)?;
    assert_eq!(global.len(), 1);
    assert!(global[0].text.contains("explicit global"));
    Ok(())
}

#[test]
fn test_dedup_with_claude_md() {
    let prefs = vec![
        Memory {
            id: 1,
            session_id: None,
            project: "test".into(),
            topic_key: Some("p1".into()),
            title: "Preference: use chinese comments".into(),
            text: "use chinese comments in code".into(),
            memory_type: "preference".into(),
            files: None,
            created_at_epoch: 0,
            updated_at_epoch: 0,
            status: "active".into(),
            branch: None,
            scope: "global".into(),
        },
        Memory {
            id: 2,
            session_id: None,
            project: "test".into(),
            topic_key: Some("p2".into()),
            title: "Preference: terse output".into(),
            text: "give terse output".into(),
            memory_type: "preference".into(),
            files: None,
            created_at_epoch: 0,
            updated_at_epoch: 0,
            status: "active".into(),
            branch: None,
            scope: "global".into(),
        },
    ];

    let indices = dedup_with_claude_md(&prefs, "/nonexistent");
    assert_eq!(indices.len(), 2, "All prefs should pass when no CLAUDE.md");
}

#[test]
fn test_add_and_remove_preference() -> Result<()> {
    let conn = setup_test_db();
    let id = add_preference(
        &conn,
        "test/proj",
        "Always use descriptive variable names",
        true,
    )?;
    assert!(id > 0);

    let prefs = memory::get_memories_by_type(&conn, "test/proj", "preference", 10)?;
    assert_eq!(prefs.len(), 1);

    let removed = remove_preference(&conn, id)?;
    assert!(removed);

    let prefs = memory::get_memories_by_type(&conn, "test/proj", "preference", 10)?;
    assert!(prefs.is_empty());
    Ok(())
}