remem-ai 0.6.90

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

fn with_config_path<T>(path: &std::path::Path, f: impl FnOnce() -> T) -> T {
    let _guard = super::super::TEST_ENV_LOCK
        .lock()
        .expect("env lock should acquire");
    let old = std::env::var("REMEM_CONFIG").ok();
    unsafe { std::env::set_var("REMEM_CONFIG", path) };
    let result = f();
    match old {
        Some(value) => unsafe { std::env::set_var("REMEM_CONFIG", value) },
        None => unsafe { std::env::remove_var("REMEM_CONFIG") },
    }
    result
}

fn temp_config_path(label: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!(
        "remem-{label}-{}-{}.toml",
        std::process::id(),
        chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
    ))
}

#[test]
fn default_config_contains_context_budget_defaults() {
    let text = super::super::default_config_text();
    let defaults = ContextLimits::default();
    assert!(text.contains("[context]"), "{text}");
    assert!(
        text.contains(&format!("total_char_limit = {}", defaults.total_char_limit)),
        "{text}"
    );
    assert!(
        text.contains(&format!(
            "preference_global_limit = {}",
            defaults.preference_global_limit
        )),
        "{text}"
    );
    assert!(
        text.contains(&format!(
            "relevance_k = {}",
            defaults.sessionstart_relevance_k
        )),
        "{text}"
    );
}

#[test]
fn missing_section_uses_compiled_defaults() -> Result<()> {
    let path = temp_config_path("context-budget-missing");
    with_config_path(&path, || -> Result<()> {
        std::fs::write(&path, "version = 1\n")?;
        assert_eq!(context_budget_limits()?, ContextLimits::default());
        Ok(())
    })?;
    std::fs::remove_file(path)?;
    Ok(())
}

#[test]
fn context_section_overrides_selected_budgets() -> Result<()> {
    let path = temp_config_path("context-budget-override");
    with_config_path(&path, || -> Result<()> {
        std::fs::write(
            &path,
            "[context]\ntotal_char_limit = 8000\npreference_global_limit = 0\nrelevance_k = 0\n",
        )?;
        let limits = context_budget_limits()?;
        assert_eq!(limits.total_char_limit, 8_000);
        assert_eq!(limits.preference_global_limit, 0);
        assert_eq!(limits.sessionstart_relevance_k, 0);
        assert_eq!(
            limits.core_char_limit,
            ContextLimits::default().core_char_limit
        );
        Ok(())
    })?;
    std::fs::remove_file(path)?;
    Ok(())
}

#[test]
fn context_section_rejects_non_integer_and_negative() -> Result<()> {
    for (label, body) in [
        ("string", "[context]\ntotal_char_limit = \"8000\"\n"),
        ("negative", "[context]\ncore_item_limit = -1\n"),
    ] {
        let path = temp_config_path(&format!("context-budget-{label}"));
        with_config_path(&path, || -> Result<()> {
            std::fs::write(&path, body)?;
            let err = context_budget_limits().expect_err("invalid context budget must fail");
            assert!(err.to_string().contains("context."), "{err}");
            Ok(())
        })?;
        std::fs::remove_file(path)?;
    }
    Ok(())
}

#[test]
fn context_section_rejects_non_table_values() -> Result<()> {
    for (label, body) in [
        ("scalar", "context = 8000\n"),
        ("inline-table", "context = { total_char_limit = 8000 }\n"),
    ] {
        let path = temp_config_path(&format!("context-budget-{label}"));
        with_config_path(&path, || -> Result<()> {
            std::fs::write(&path, body)?;
            let err = context_budget_limits().expect_err("non-table context must fail");
            assert_eq!(err.to_string(), "context must be a table");
            Ok(())
        })?;
        std::fs::remove_file(path)?;
    }
    Ok(())
}

#[test]
fn env_override_wins_over_context_section() -> Result<()> {
    let path = temp_config_path("context-budget-env-wins");
    with_config_path(&path, || -> Result<()> {
        std::fs::write(&path, "[context]\ntotal_char_limit = 8000\n")?;
        let previous = std::env::var_os("REMEM_CONTEXT_TOTAL_CHAR_LIMIT");
        unsafe { std::env::set_var("REMEM_CONTEXT_TOTAL_CHAR_LIMIT", "7000") };
        let limits = crate::context::ContextLimits::from_runtime();
        match previous {
            Some(value) => unsafe { std::env::set_var("REMEM_CONTEXT_TOTAL_CHAR_LIMIT", value) },
            None => unsafe { std::env::remove_var("REMEM_CONTEXT_TOTAL_CHAR_LIMIT") },
        }
        assert_eq!(limits?.total_char_limit, 7_000);
        Ok(())
    })?;
    std::fs::remove_file(path)?;
    Ok(())
}

#[test]
fn context_budget_can_be_set_through_config_cli() -> Result<()> {
    let path = temp_config_path("context-budget-cli");
    with_config_path(&path, || -> Result<()> {
        super::super::init_config()?;
        super::super::set_config_value("context.total_char_limit", "9000")?;
        let limits = context_budget_limits()?;
        let text = std::fs::read_to_string(&path)?;
        assert_eq!(limits.total_char_limit, 9_000);
        assert!(text.contains("total_char_limit = 9000"), "{text}");
        Ok(())
    })?;
    std::fs::remove_file(path)?;
    Ok(())
}