agentmem 0.1.1

Local-first memory engine for AI coding agents
Documentation
use tempfile::tempdir;

use agentmem::config::Config;
use agentmem::store::locking;
use agentmem::store::Store;
use agentmem::types::{Key, Namespace, ProjectName, Value};

fn open_test_store() -> (tempfile::TempDir, Config, Store) {
    let dir = tempdir().expect("failed to create temporary directory");

    let config = Config::for_project_root(
        ProjectName::new("demo-project").expect("valid project name"),
        dir.path(),
    )
    .expect("failed to build config");

    let store = Store::open(config.clone()).expect("failed to open store");

    (dir, config, store)
}

#[test]
fn store_starts_empty() {
    let (_dir, _config, store) = open_test_store();

    assert!(store.is_empty());
    assert_eq!(store.len(), 0);
    assert!(store.entries().is_empty());
}

#[test]
fn set_then_get_returns_value() {
    let (_dir, _config, mut store) = open_test_store();

    let key = Key::new("agent/claude/current_task").expect("valid key");
    let value = Value::new("Review auth middleware").expect("valid value");

    let previous = store
        .set(key.clone(), value.clone())
        .expect("set should succeed");

    assert!(previous.is_none());

    let retrieved = store.get(&key).expect("value should exist");
    assert_eq!(retrieved, &value);
}

#[test]
fn set_overwrites_existing_value_and_returns_previous() {
    let (_dir, _config, mut store) = open_test_store();

    let key = Key::new("agent/claude/current_task").expect("valid key");
    let first = Value::new("Review PR #41").expect("valid value");
    let second = Value::new("Review PR #42").expect("valid value");

    let previous = store
        .set(key.clone(), first.clone())
        .expect("first set should succeed");
    assert!(previous.is_none());

    let previous = store
        .set(key.clone(), second.clone())
        .expect("second set should succeed");

    assert_eq!(previous.as_ref(), Some(&first));
    assert_eq!(store.get(&key), Some(&second));
}

#[test]
fn delete_existing_key_removes_value() {
    let (_dir, _config, mut store) = open_test_store();

    let key = Key::new("agent/codex/current_task").expect("valid key");
    let value = Value::new("Refactor persistence layer").expect("valid value");

    store
        .set(key.clone(), value.clone())
        .expect("set should succeed");

    let removed = store.delete(&key).expect("value should be removed");
    assert_eq!(removed, value);
    assert!(store.get(&key).is_none());
    assert!(!store.contains(&key));
}

#[test]
fn delete_missing_key_returns_none() {
    let (_dir, _config, mut store) = open_test_store();

    let key = Key::new("agent/gemini/current_task").expect("valid key");

    assert!(store.delete(&key).is_none());
}

#[test]
fn namespace_listing_returns_only_matching_entries() {
    let (_dir, _config, mut store) = open_test_store();

    store
        .set(
            Key::new("agent/claude/current_task").expect("valid key"),
            Value::new("Audit auth flow").expect("valid value"),
        )
        .expect("set should succeed");

    store
        .set(
            Key::new("agent/claude/context/summary").expect("valid key"),
            Value::new("Need safer token rotation").expect("valid value"),
        )
        .expect("set should succeed");

    store
        .set(
            Key::new("agent/codex/current_task").expect("valid key"),
            Value::new("Write tests").expect("valid value"),
        )
        .expect("set should succeed");

    let namespace = Namespace::new("agent/claude").expect("valid namespace");
    let entries = store.list_namespace(&namespace);

    assert_eq!(entries.len(), 2);
    assert!(entries
        .iter()
        .all(|entry| entry.key.as_str().starts_with("agent/claude/")));
}

#[test]
fn delete_namespace_removes_only_matching_entries() {
    let (_dir, _config, mut store) = open_test_store();

    store
        .set(
            Key::new("agent/claude/current_task").expect("valid key"),
            Value::new("A").expect("valid value"),
        )
        .expect("set should succeed");

    store
        .set(
            Key::new("agent/claude/context/summary").expect("valid key"),
            Value::new("B").expect("valid value"),
        )
        .expect("set should succeed");

    store
        .set(
            Key::new("agent/codex/current_task").expect("valid key"),
            Value::new("C").expect("valid value"),
        )
        .expect("set should succeed");

    let removed = store.delete_namespace(&Namespace::new("agent/claude").expect("valid namespace"));

    assert_eq!(removed, 2);
    assert_eq!(store.len(), 1);
    assert!(store
        .get(&Key::new("agent/codex/current_task").expect("valid key"))
        .is_some());
}

#[test]
fn entries_are_returned_in_stable_sorted_order() {
    let (_dir, _config, mut store) = open_test_store();

    for (key, value) in [
        ("agent/codex/current_task", "c"),
        ("agent/claude/current_task", "a"),
        ("agent/claude/context", "b"),
    ] {
        store
            .set(
                Key::new(key).expect("valid key"),
                Value::new(value).expect("valid value"),
            )
            .expect("set should succeed");
    }

    let entries = store.entries();
    let keys: Vec<String> = entries
        .iter()
        .map(|entry| entry.key.as_str().to_owned())
        .collect();

    assert_eq!(
        keys,
        vec![
            "agent/claude/context",
            "agent/claude/current_task",
            "agent/codex/current_task",
        ]
    );
}

#[test]
fn flush_then_reopen_roundtrips_data() {
    let (_dir, config, mut store) = open_test_store();

    let key = Key::new("project/demo/root").expect("valid key");
    let value = Value::new("/workspace/demo").expect("valid value");

    store
        .set(key.clone(), value.clone())
        .expect("set should succeed");

    store.flush().expect("flush should succeed");

    let reopened = Store::open(config).expect("reopen should succeed");

    assert_eq!(reopened.get(&key), Some(&value));
}

#[test]
fn reload_discards_unsaved_in_memory_changes() {
    let (_dir, config, mut store) = open_test_store();

    let key = Key::new("agent/claude/current_task").expect("valid key");

    store
        .set(
            key.clone(),
            Value::new("Initial value").expect("valid value"),
        )
        .expect("set should succeed");

    store.flush().expect("flush should succeed");

    store
        .set(
            key.clone(),
            Value::new("Unsaved change").expect("valid value"),
        )
        .expect("set should succeed");

    store.reload().expect("reload should succeed");

    assert_eq!(
        store.get(&key).expect("key should exist").as_str(),
        "Initial value"
    );

    let reopened = Store::open(config).expect("reopen should succeed");
    assert_eq!(
        reopened.get(&key).expect("key should exist").as_str(),
        "Initial value"
    );
}

#[test]
fn open_locked_acquires_lock_and_release_lock_clears_it() {
    let (dir, config, _store) = open_test_store();

    let mut locked = Store::open_locked(config).expect("open locked store");

    assert!(locked.is_locked());
    assert!(locking::is_locked(locked.path()));

    locked.release_lock().expect("release lock");
    assert!(!locking::is_locked(
        &dir.path().join(".agentmem/store.json")
    ));
}

#[test]
fn clear_then_flush_persists_empty_store() {
    let (_dir, config, mut store) = open_test_store();

    store
        .set(
            Key::new("agent/claude/current_task").expect("valid key"),
            Value::new("task").expect("valid value"),
        )
        .expect("set should succeed");
    store.flush().expect("flush should succeed");

    store.clear();
    store.flush().expect("flush after clear");

    let reopened = Store::open(config).expect("reopen");
    assert!(reopened.is_empty());
}