remem-ai 0.6.88

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::path::PathBuf;

use super::index::ensure_memory_index;
use super::paths::encode_project_path;
use super::runtime::{enforce_native_memory_limit, env_value_enabled};

fn unique_temp_dir(name: &str) -> PathBuf {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("clock should be after unix epoch")
        .as_nanos();
    std::env::temp_dir().join(format!("remem-{}-{}-{}", name, std::process::id(), nanos))
}

#[test]
fn encode_project_path_replaces_slashes_after_canonicalize() {
    let dir = unique_temp_dir("claude-memory-path");
    std::fs::create_dir_all(&dir).expect("temp dir should create");

    let encoded = encode_project_path(dir.to_str().expect("temp path should be utf-8"));
    let canonical =
        crate::db::canonical_project_path(dir.to_str().expect("temp path should be utf-8"));
    let expected = canonical.to_string_lossy().replace('/', "-");

    assert_eq!(encoded, expected);

    std::fs::remove_dir_all(&dir).expect("temp dir should clean up");
}

#[test]
fn ensure_memory_index_is_idempotent() {
    let dir = unique_temp_dir("claude-memory-index-idempotent");
    std::fs::create_dir_all(&dir).expect("temp dir should create");
    let index_path = dir.join("MEMORY.md");
    std::fs::write(&index_path, "# Memory Index\n").expect("index should write");

    ensure_memory_index(&dir).expect("first ensure should succeed");
    ensure_memory_index(&dir).expect("second ensure should succeed");

    let content = std::fs::read_to_string(&index_path).expect("index should read");
    assert_eq!(content.matches("remem_sessions.md").count(), 1);

    std::fs::remove_dir_all(&dir).expect("temp dir should clean up");
}

#[test]
fn ensure_memory_index_creates_new_file_when_missing() {
    let dir = unique_temp_dir("claude-memory-index-create");
    std::fs::create_dir_all(&dir).expect("temp dir should create");

    ensure_memory_index(&dir).expect("ensure should succeed");

    let content = std::fs::read_to_string(dir.join("MEMORY.md")).expect("index should read");
    assert!(content.contains("# Memory Index"));
    assert!(content.contains("remem_sessions.md"));

    std::fs::remove_dir_all(&dir).expect("temp dir should clean up");
}

#[test]
fn native_memory_limit_caps_output_without_breaking_utf8() {
    let content = format!(
        "---\nname: remem_sessions\n---\n\n{}\n",
        "关键决策和最近会话摘要".repeat(80)
    );

    let (capped, truncated) = enforce_native_memory_limit(&content, 512);

    assert!(truncated);
    assert!(capped.len() <= 512, "capped length was {}", capped.len());
    assert!(capped.is_char_boundary(capped.len()));
    assert!(capped.contains("Truncated by remem native memory guard"));
}

#[test]
fn native_memory_disable_env_accepts_explicit_truthy_values() {
    for value in ["1", "true", "TRUE", "yes", "on"] {
        assert!(env_value_enabled(value), "{value}");
    }
    for value in ["", "0", "false", "off", "disabled"] {
        assert!(!env_value_enabled(value), "{value}");
    }
}

#[test]
fn native_memory_sessions_exclude_synthetic_rollup_titles() -> anyhow::Result<()> {
    let conn = rusqlite::Connection::open_in_memory()?;
    crate::migrate::run_migrations(&conn)?;
    conn.execute(
        "INSERT INTO session_summaries
         (memory_session_id, project, request, completed, decisions,
          created_at_epoch, session_row_id, covered_from_event_id,
         covered_to_event_id)
         VALUES
         ('rollup-synthetic', '/repo', 'Captured event range 1..3',
          'synthetic range fallback', '', 10, 1, 1, 3),
         ('rollup-semantic', '/repo', 'Retire legacy Summary writer',
          'semantic rollup summary', 'SessionRollup owns structured fields',
          11, 2, 4, 6),
         ('rollup-structured-fallback', '/repo', 'Captured event range 7..9',
          'structured fallback summary', 'Fallback request keeps decisions',
          12, 3, 7, 9)",
        [],
    )?;

    let sessions = super::runtime::load_recent_sessions(&conn, "/repo")?;
    let content =
        super::render::render_memory_content(&sessions).expect("semantic session should render");

    assert!(content.contains("Retire legacy Summary writer"));
    assert!(content.contains("SessionRollup owns structured fields"));
    assert!(content.contains("Fallback request keeps decisions"));
    assert!(!content.contains("Captured event range 1..3"));
    assert!(!content.contains("Captured event range 7..9"));
    assert!(!content.contains("synthetic range fallback"));
    Ok(())
}