remem-ai 0.6.38

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use rusqlite::Connection;

use super::super::host::HostKind;
use super::super::policy::{ContextLimits, ContextPolicy};
use super::super::query::load_context_data_with_policy;
use super::super::render::{build_context_debug_trace, build_context_stats_footer};
use super::super::render::{ContextRenderStats, SectionRenderStats};
use super::super::types::ContextRequest;
use super::{insert_memory, insert_owned_memory, setup_context_schema};

#[test]
fn startup_context_excludes_unrelated_tool_and_domain_memories() {
    let conn = Connection::open_in_memory().unwrap();
    setup_context_schema(&conn);
    let stash = "/Users/lifcc/Desktop/code/AI/tool/stash";
    let now = chrono::Utc::now().timestamp();

    insert_owned_memory(
        &conn,
        1,
        stash,
        Some("stash-dnd-decision"),
        "decision",
        "Stash DnD uses pointer sensors",
        "The Stash UI uses pointer sensors for drag and drop behavior.",
        now,
        "repo",
        stash,
        Some(stash),
        Some("stash-ui"),
    );
    insert_owned_memory(
        &conn,
        2,
        stash,
        Some("codex-sandbox"),
        "decision",
        "Codex approval mode uses workspace-write",
        "Codex CLI sandbox and approval policy are tool-level facts.",
        now + 1,
        "tool",
        "codex-cli",
        None,
        Some("codex-sandbox"),
    );
    insert_owned_memory(
        &conn,
        3,
        stash,
        Some("grok-api"),
        "discovery",
        "Grok API supports image references",
        "The xAI Grok API wrapper accepts image reference payloads.",
        now + 2,
        "domain",
        "grok-api",
        None,
        Some("grok-api"),
    );
    insert_owned_memory(
        &conn,
        4,
        stash,
        Some("warp-startup"),
        "discovery",
        "Warp launch state is macOS domain context",
        "Warp terminal launch behavior is not Stash repository context.",
        now + 3,
        "domain",
        "macos",
        None,
        Some("macos"),
    );
    insert_memory(
        &conn,
        5,
        stash,
        Some("legacy-stash-fallback"),
        "bugfix",
        "Legacy Stash memory remains compatible",
        "Rows without owner metadata still load through project fallback.",
        now - 1,
    );

    let loaded = load_context_data_with_policy(
        &conn,
        stash,
        None,
        &ContextPolicy::from_limits(ContextLimits {
            candidate_fetch_limit: 2,
            memory_index_limit: 10,
            core_item_limit: 10,
            ..ContextLimits::default()
        }),
        true,
    );
    let titles = loaded
        .memories
        .iter()
        .map(|memory| memory.title.as_str())
        .collect::<Vec<_>>();

    assert!(titles.contains(&"Stash DnD uses pointer sensors"));
    assert!(titles.contains(&"Legacy Stash memory remains compatible"));
    assert!(!titles.contains(&"Codex approval mode uses workspace-write"));
    assert!(!titles.contains(&"Grok API supports image references"));
    assert!(!titles.contains(&"Warp launch state is macOS domain context"));
    assert_eq!(loaded.owner_counts.repo, 1);
    assert_eq!(loaded.owner_counts.legacy, 1);
    assert!(loaded.owner_traces.iter().any(|trace| !trace.included
        && trace.owner_scope.as_deref() == Some("tool")
        && trace.reason == "tool_not_relevant_to_startup"));
    assert!(loaded.owner_traces.iter().any(|trace| !trace.included
        && trace.owner_scope.as_deref() == Some("domain")
        && trace.reason == "domain_not_relevant_to_startup"));
}

#[test]
fn debug_trace_reports_owner_reasons_and_counts() {
    let conn = Connection::open_in_memory().unwrap();
    setup_context_schema(&conn);
    let stash = "/Users/lifcc/Desktop/code/AI/tool/stash";
    let now = chrono::Utc::now().timestamp();

    insert_owned_memory(
        &conn,
        1,
        stash,
        Some("stash-repo"),
        "decision",
        "Stash repo context",
        "Stash repository context should load at startup.",
        now,
        "repo",
        stash,
        Some(stash),
        Some("stash-ui"),
    );
    insert_owned_memory(
        &conn,
        2,
        stash,
        Some("codex-tool"),
        "decision",
        "Codex sandbox context",
        "Codex sandbox context should stay out of Stash startup.",
        now + 1,
        "tool",
        "codex-cli",
        None,
        Some("codex-sandbox"),
    );

    let loaded = load_context_data_with_policy(
        &conn,
        stash,
        None,
        &ContextPolicy::from_limits(ContextLimits::default()),
        true,
    );
    let stats = ContextRenderStats {
        host: "codex-cli".to_string(),
        memories_loaded: loaded.memories.len(),
        core: SectionRenderStats {
            count: loaded.memories.len(),
            chars: 128,
        },
        owner_counts: loaded.owner_counts.clone(),
        ..ContextRenderStats::default()
    };
    let request = ContextRequest {
        cwd: stash.to_string(),
        project: stash.to_string(),
        session_id: Some("sess-owner".to_string()),
        hook_source: None,
        current_branch: None,
        host: HostKind::CodexCli,
        use_colors: false,
    };

    let debug = build_context_debug_trace(&request, &ContextPolicy::from_env(), &loaded, &stats);
    assert!(debug.contains("owner counts repo=1"));
    assert!(debug.contains("owner memory id=1 scope=repo"));
    assert!(debug.contains("included reason=repo_owner_match"));
    assert!(debug.contains("owner memory id=2 scope=tool"));
    assert!(debug.contains("excluded reason=tool_not_relevant_to_startup"));

    let footer = build_context_stats_footer(&stats);
    assert!(!footer.contains("owners repo="));
    assert!(!footer.contains("tool=0 domain=0"));
}

/// GH951: owner traces are debug-only, but `owner_counts` feeds
/// ContextRenderStats on every SessionStart. Gating the counts along with the
/// traces would zero out visible stats, so the two must move independently.
#[test]
fn non_debug_load_keeps_owner_counts_and_drops_traces() {
    let conn = Connection::open_in_memory().unwrap();
    setup_context_schema(&conn);
    let stash = "/Users/lifcc/Desktop/code/AI/tool/stash";
    let now = chrono::Utc::now().timestamp();

    insert_owned_memory(
        &conn,
        1,
        stash,
        Some("stash-dnd-decision"),
        "decision",
        "Stash DnD uses pointer sensors",
        "The Stash UI uses pointer sensors for drag and drop behavior.",
        now,
        "repo",
        stash,
        Some(stash),
        Some("stash-ui"),
    );
    insert_memory(
        &conn,
        2,
        stash,
        Some("legacy-stash-fallback"),
        "bugfix",
        "Legacy Stash memory remains compatible",
        "Rows without owner metadata still load through project fallback.",
        now - 1,
    );
    insert_owned_memory(
        &conn,
        3,
        stash,
        Some("codex-tool-diagnostic"),
        "decision",
        "Codex tool diagnostic stays excluded",
        "Tool-owned context must appear only in the debug exclusion trace.",
        now + 1,
        "tool",
        "codex-cli",
        None,
        Some("codex-sandbox"),
    );

    let policy = ContextPolicy::from_limits(ContextLimits::default());

    let debug_load = load_context_data_with_policy(&conn, stash, None, &policy, true);
    let plain_load = load_context_data_with_policy(&conn, stash, None, &policy, false);

    // The counts are what users see; they must not depend on debug mode.
    assert_eq!(plain_load.owner_counts.repo, debug_load.owner_counts.repo);
    assert_eq!(
        plain_load.owner_counts.legacy,
        debug_load.owner_counts.legacy
    );
    assert!(
        plain_load.owner_counts.repo > 0 || plain_load.owner_counts.legacy > 0,
        "non-debug load must still populate owner counts"
    );

    // The traces are diagnostics; skipping them is what removes the
    // unindexable exclusion scan from the non-debug hot path.
    assert!(
        plain_load.owner_traces.is_empty(),
        "non-debug load must not build owner traces"
    );
    assert!(
        !debug_load.owner_traces.is_empty(),
        "debug load must still report owner traces"
    );
    assert!(
        debug_load.owner_traces.iter().any(|trace| {
            trace.id == 3
                && !trace.included
                && trace.owner_scope.as_deref() == Some("tool")
                && trace.reason == "tool_not_relevant_to_startup"
        }),
        "debug load must execute the exclusion query and report the tool-owned row"
    );

    // Selection itself is unaffected.
    let plain_titles = plain_load
        .memories
        .iter()
        .map(|memory| memory.title.as_str())
        .collect::<Vec<_>>();
    let debug_titles = debug_load
        .memories
        .iter()
        .map(|memory| memory.title.as_str())
        .collect::<Vec<_>>();
    assert_eq!(plain_titles, debug_titles);
}