remem-ai 0.5.155

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::io::{Read, Write};

use anyhow::{Context, Result};
use rusqlite::Connection;

use super::search_with_branch_explain;

const ENV_KEYS: &[&str] = &[
    "REMEM_CONFIG",
    "REMEM_EMBEDDINGS_PROVIDER",
    "REMEM_EMBEDDING_PROVIDER",
    "REMEM_EMBEDDINGS_FALLBACK",
    "REMEM_EMBEDDINGS_API_KEY",
    "REMEM_EMBEDDING_API_KEY",
    "REMEM_EMBEDDINGS_BASE_URL",
    "REMEM_EMBEDDING_BASE_URL",
    "OPENAI_API_KEY",
];

fn with_clean_search_embedding_env<T>(f: impl FnOnce() -> T) -> T {
    let _guard = crate::runtime_config::TEST_ENV_LOCK
        .lock()
        .expect("env lock should acquire");
    let saved = ENV_KEYS
        .iter()
        .map(|key| (*key, std::env::var(key).ok()))
        .collect::<Vec<_>>();
    for key in ENV_KEYS {
        unsafe { std::env::remove_var(key) };
    }
    let result = f();
    for (key, value) in saved {
        match value {
            Some(value) => unsafe { std::env::set_var(key, value) },
            None => unsafe { std::env::remove_var(key) },
        }
    }
    result
}

fn setup_search_conn() -> Result<Connection> {
    let conn = Connection::open_in_memory()?;
    crate::memory::tests_helper::setup_memory_schema(&conn);
    Ok(conn)
}

fn insert_search_memory(conn: &Connection) -> Result<()> {
    conn.execute(
        "INSERT INTO memories
         (id, session_id, project, title, content, memory_type,
          created_at_epoch, updated_at_epoch, status, scope)
         VALUES (1, 'session-1', '/repo', 'Semantic fallback',
                 'FTS result survives provider failure.', 'decision',
                 1, 1, 'active', 'project')",
        [],
    )?;
    Ok(())
}

#[test]
fn search_continues_without_vector_when_api_failure_falls_back_to_off() -> Result<()> {
    with_clean_search_embedding_env(|| -> Result<()> {
        let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
        let addr = listener.local_addr()?;
        let handle = std::thread::spawn(move || -> Result<()> {
            let (mut stream, _) = listener.accept()?;
            let mut buffer = [0_u8; 8192];
            let _ = stream.read(&mut buffer)?;
            let body = "provider unavailable";
            let response = format!(
                "HTTP/1.1 500 Internal Server Error\r\ncontent-length: {}\r\n\r\n{}",
                body.len(),
                body
            );
            stream.write_all(response.as_bytes())?;
            Ok(())
        });
        unsafe {
            std::env::set_var("REMEM_EMBEDDINGS_PROVIDER", "api");
            std::env::set_var("REMEM_EMBEDDINGS_FALLBACK", "off");
            std::env::set_var("REMEM_EMBEDDINGS_API_KEY", "test-key");
            std::env::set_var("REMEM_EMBEDDINGS_BASE_URL", format!("http://{addr}/v1"));
        }
        let conn = setup_search_conn()?;
        insert_search_memory(&conn)?;

        let (memories, explain) = search_with_branch_explain(
            &conn,
            Some("Semantic fallback"),
            Some("/repo"),
            None,
            5,
            0,
            false,
            None,
        )?;
        handle
            .join()
            .map_err(|_| anyhow::anyhow!("embedding test server thread panicked"))??;
        let explain = explain.context("query explain should be present")?;
        let vector = explain
            .channels
            .iter()
            .find(|channel| channel.name == "vector")
            .context("vector channel should be reported")?;

        assert_eq!(memories.first().map(|memory| memory.id), Some(1));
        assert!(!vector.enabled);
        assert_eq!(
            vector.disabled_reason.as_deref(),
            Some("embedding provider is off")
        );
        Ok(())
    })
}