remem-ai 0.4.9

Persistent memory for Claude Code and Codex — single binary, automatic context
Documentation
use rusqlite::{params, Connection};

use super::*;
use crate::memory::types::tests_helper::setup_memory_schema;

#[test]
fn save_lesson_creates_metadata() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    let id = save_lesson(
        &conn,
        &SaveLessonRequest {
            session_id: Some("s1"),
            project: "/repo",
            topic_key: Some("lesson-build-failure"),
            title: "Avoid build failure loops",
            content: "Lesson: after repeated build failures, stop and challenge the hypothesis.",
            confidence: 0.8,
            source_evidence: Some("cargo check failed twice"),
            files: None,
            branch: Some("main"),
            scope: "project",
            created_at_epoch: None,
            stale_after_epoch: None,
        },
    )
    .unwrap();

    let metadata = get_lesson_metadata(&conn, id)
        .unwrap()
        .expect("metadata should exist");
    assert_eq!(metadata.memory_id, id);
    assert_eq!(metadata.reinforcement_count, 1);
    assert_eq!(
        metadata.source_evidence.as_deref(),
        Some("cargo check failed twice")
    );
}

#[test]
fn save_lesson_reinforces_duplicate_topic() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    let req = SaveLessonRequest {
        session_id: Some("s1"),
        project: "/repo",
        topic_key: Some("lesson-build-failure"),
        title: "Avoid build failure loops",
        content: "Lesson: after repeated build failures, stop and challenge the hypothesis.",
        confidence: 0.6,
        source_evidence: Some("first run"),
        files: None,
        branch: None,
        scope: "project",
        created_at_epoch: None,
        stale_after_epoch: None,
    };
    let first_id = save_lesson(&conn, &req).unwrap();

    let second_id = save_lesson(
        &conn,
        &SaveLessonRequest {
            confidence: 0.9,
            source_evidence: Some("second run"),
            ..req
        },
    )
    .unwrap();

    assert_eq!(first_id, second_id);
    let metadata = get_lesson_metadata(&conn, first_id).unwrap().unwrap();
    assert_eq!(metadata.reinforcement_count, 2);
    assert_eq!(metadata.confidence, 0.9);
    assert_eq!(metadata.source_evidence.as_deref(), Some("second run"));
}

#[test]
fn save_lesson_reinforcement_clears_stale_deadline() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);
    let now = chrono::Utc::now().timestamp();

    let req = SaveLessonRequest {
        session_id: Some("s1"),
        project: "/repo",
        topic_key: Some("lesson-stale-refresh"),
        title: "Refresh stale lesson",
        content:
            "Lesson: a reinforced lesson should become usable again when it is no longer stale.",
        confidence: 0.7,
        source_evidence: Some("first run"),
        files: None,
        branch: None,
        scope: "project",
        created_at_epoch: None,
        stale_after_epoch: Some(now - 1),
    };
    let id = save_lesson(&conn, &req).unwrap();
    assert!(list_lessons_for_context(&conn, "/repo", None, 10)
        .unwrap()
        .is_empty());

    let refreshed_id = save_lesson(
        &conn,
        &SaveLessonRequest {
            source_evidence: Some("second run"),
            stale_after_epoch: None,
            ..req
        },
    )
    .unwrap();

    assert_eq!(id, refreshed_id);
    let metadata = get_lesson_metadata(&conn, id).unwrap().unwrap();
    assert_eq!(metadata.stale_after_epoch, None);
    let lessons = list_lessons_for_context(&conn, "/repo", None, 10).unwrap();
    assert_eq!(lessons.len(), 1);
    assert_eq!(lessons[0].memory.title, "Refresh stale lesson");
}

#[test]
fn list_lessons_for_context_filters_low_confidence_and_stale() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);
    let now = chrono::Utc::now().timestamp();

    for (idx, confidence, stale_after_epoch) in
        [(1, 0.9, None), (2, 0.2, None), (3, 0.8, Some(now - 1))]
    {
        save_lesson(
            &conn,
            &SaveLessonRequest {
                session_id: Some("s1"),
                project: "/repo",
                topic_key: Some(&format!("lesson-{idx}")),
                title: &format!("Lesson {idx}"),
                content: "Lesson: use evidence-backed fixes for recurring failures.",
                confidence,
                source_evidence: None,
                files: None,
                branch: None,
                scope: "project",
                created_at_epoch: None,
                stale_after_epoch,
            },
        )
        .unwrap();
    }

    let lessons = list_lessons_for_context(&conn, "/repo", None, 10).unwrap();
    assert_eq!(lessons.len(), 1);
    assert_eq!(lessons[0].memory.title, "Lesson 1");
}

#[test]
fn list_lessons_for_context_filters_other_branches() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    for (title, branch) in [
        ("Main branch lesson", Some("main")),
        ("Feature branch lesson", Some("feature/search")),
        ("Branchless lesson", None),
    ] {
        save_lesson(
            &conn,
            &SaveLessonRequest {
                session_id: Some("s1"),
                project: "/repo",
                topic_key: Some(&title.replace(' ', "-").to_lowercase()),
                title,
                content: "Lesson: branch-specific context should not leak into unrelated branches.",
                confidence: 0.8,
                source_evidence: None,
                files: None,
                branch,
                scope: "project",
                created_at_epoch: None,
                stale_after_epoch: None,
            },
        )
        .unwrap();
    }

    let mut titles: Vec<_> = list_lessons_for_context(&conn, "/repo", Some("main"), 10)
        .unwrap()
        .into_iter()
        .map(|lesson| lesson.memory.title)
        .collect();
    titles.sort();
    assert_eq!(titles, vec!["Branchless lesson", "Main branch lesson"]);

    let all_titles: Vec<_> = list_lessons_for_context(&conn, "/repo", None, 10)
        .unwrap()
        .into_iter()
        .map(|lesson| lesson.memory.title)
        .collect();
    assert!(all_titles.contains(&"Feature branch lesson".to_string()));
}

#[test]
fn list_lessons_for_context_keeps_project_before_global_scope() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    save_lesson(
        &conn,
        &SaveLessonRequest {
            session_id: Some("s1"),
            project: "/global",
            topic_key: Some("lesson-global"),
            title: "Global lesson",
            content: "Lesson: use the verified workflow when every project hits this failure.",
            confidence: 0.99,
            source_evidence: None,
            files: None,
            branch: None,
            scope: "global",
            created_at_epoch: None,
            stale_after_epoch: None,
        },
    )
    .unwrap();
    save_lesson(
        &conn,
        &SaveLessonRequest {
            session_id: Some("s1"),
            project: "/other",
            topic_key: Some("lesson-other-project"),
            title: "Other project lesson",
            content: "Lesson: this project-local item should not leak into unrelated projects.",
            confidence: 0.95,
            source_evidence: None,
            files: None,
            branch: None,
            scope: "project",
            created_at_epoch: None,
            stale_after_epoch: None,
        },
    )
    .unwrap();
    save_lesson(
        &conn,
        &SaveLessonRequest {
            session_id: Some("s1"),
            project: "/repo",
            topic_key: Some("lesson-project"),
            title: "Project lesson",
            content: "Lesson: prefer the project-specific workflow before global advice.",
            confidence: 0.6,
            source_evidence: None,
            files: None,
            branch: None,
            scope: "project",
            created_at_epoch: None,
            stale_after_epoch: None,
        },
    )
    .unwrap();

    let lessons = list_lessons_for_context(&conn, "/repo", None, 10).unwrap();
    let titles: Vec<_> = lessons
        .iter()
        .map(|lesson| lesson.memory.title.as_str())
        .collect();

    assert_eq!(titles, vec!["Project lesson", "Global lesson"]);
}

#[test]
fn list_lessons_for_context_ranks_confidence_reinforcement_and_recency() {
    let conn = Connection::open_in_memory().unwrap();
    setup_memory_schema(&conn);

    let high_confidence = save_ranked_lesson(&conn, "high-confidence", 0.9).unwrap();
    let older_reinforced = save_ranked_lesson(&conn, "older-reinforced", 0.7).unwrap();
    let newer_reinforced = save_ranked_lesson(&conn, "newer-reinforced", 0.7).unwrap();
    let lower_reinforced = save_ranked_lesson(&conn, "lower-reinforced", 0.7).unwrap();
    set_lesson_rank(&conn, high_confidence, 1, 100);
    set_lesson_rank(&conn, older_reinforced, 3, 100);
    set_lesson_rank(&conn, newer_reinforced, 3, 200);
    set_lesson_rank(&conn, lower_reinforced, 2, 300);

    let lessons = list_lessons_for_context(&conn, "/repo", None, 10).unwrap();
    let titles: Vec<_> = lessons
        .iter()
        .map(|lesson| lesson.memory.title.as_str())
        .collect();

    assert_eq!(
        titles,
        vec![
            "high-confidence",
            "newer-reinforced",
            "older-reinforced",
            "lower-reinforced"
        ]
    );
}

#[test]
fn is_lesson_candidate_requires_actionable_signal() {
    assert!(is_lesson_candidate(
        "Root cause: unchecked fallback hid the real error; avoid warning-only degradation."
    ));
    assert!(!is_lesson_candidate(
        "FTS5 trigram tokenizer handles CJK without word boundaries"
    ));
}

fn save_ranked_lesson(conn: &Connection, title: &str, confidence: f64) -> anyhow::Result<i64> {
    save_lesson(
        conn,
        &SaveLessonRequest {
            session_id: Some("s1"),
            project: "/repo",
            topic_key: Some(title),
            title,
            content: "Lesson: keep deterministic ranking when many lessons compete for context.",
            confidence,
            source_evidence: None,
            files: None,
            branch: None,
            scope: "project",
            created_at_epoch: None,
            stale_after_epoch: None,
        },
    )
}

fn set_lesson_rank(
    conn: &Connection,
    memory_id: i64,
    reinforcement_count: i64,
    last_reinforced_at_epoch: i64,
) {
    conn.execute(
        "UPDATE memory_lessons
         SET reinforcement_count = ?2, last_reinforced_at_epoch = ?3
         WHERE memory_id = ?1",
        params![memory_id, reinforcement_count, last_reinforced_at_epoch],
    )
    .unwrap();
}