agentmem 0.1.0

Local-first memory engine for AI coding agents
Documentation
use std::path::Path;
use std::process::Command;

use tempfile::tempdir;

use agent_hashmap::config::Config;
use agent_hashmap::types::{Key, ProjectName, Value};

fn bin() -> &'static str {
    env!("CARGO_BIN_EXE_agentmem")
}

fn init_project(root: &Path) {
    let config = Config::for_project_root(
        ProjectName::new("demo-project").expect("valid project name"),
        root,
    )
    .expect("config");

    let config_path = Config::project_config_path(root);
    config.save(&config_path).expect("save config");
}

fn run_cli(root: &Path, args: &[&str]) -> std::process::Output {
    Command::new(bin())
        .current_dir(root)
        .args(args)
        .output()
        .expect("run cli")
}

#[test]
fn info_fails_cleanly_when_project_is_not_initialized() {
    let dir = tempdir().expect("temp dir");
    let output = run_cli(dir.path(), &["info"]);

    assert!(!output.status.success());
}

#[test]
fn info_shows_project_and_store_metadata() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());

    let output = run_cli(dir.path(), &["info"]);

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Store Info"));
    assert!(stdout.contains("Project"));
    assert!(stdout.contains("Format"));
}

#[test]
fn get_missing_key_returns_success_with_human_message() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());

    let output = run_cli(dir.path(), &["get", "agent/codex/missing"]);

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("key not found"));
}

#[test]
fn set_creates_store_file_on_first_write() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());
    let store_path = Config::project_store_path(dir.path());
    assert!(!store_path.exists());

    let output = run_cli(
        dir.path(),
        &["set", "agent/codex/current_task", "Review PR"],
    );
    assert!(output.status.success());
    assert!(store_path.exists());
}

#[test]
fn set_then_get_roundtrips_through_real_cli() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());

    let set_output = run_cli(
        dir.path(),
        &["set", "agent/codex/current_task", "Review PR"],
    );
    assert!(set_output.status.success());

    let get_output = run_cli(dir.path(), &["get", "agent/codex/current_task"]);
    assert!(get_output.status.success());
    let stdout = String::from_utf8_lossy(&get_output.stdout);
    assert!(stdout.contains("Review PR"));
}

#[test]
fn delete_removes_existing_key() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());

    let _ = run_cli(dir.path(), &["set", "agent/codex/current_task", "Review"]);
    let output = run_cli(dir.path(), &["delete", "agent/codex/current_task"]);
    assert!(output.status.success());

    let get_output = run_cli(dir.path(), &["get", "agent/codex/current_task"]);
    let stdout = String::from_utf8_lossy(&get_output.stdout);
    assert!(stdout.contains("key not found"));
}

#[test]
fn list_with_prefix_filters_results() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());

    let _ = run_cli(dir.path(), &["set", "agent/codex/task", "A"]);
    let _ = run_cli(dir.path(), &["set", "agent/claude/task", "B"]);

    let output = run_cli(dir.path(), &["list", "--prefix", "agent/codex"]);
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("agent/codex/task"));
    assert!(!stdout.contains("agent/claude/task"));
}

#[test]
fn list_prints_all_entries_in_sorted_order() {
    let dir = tempdir().expect("temp dir");
    init_project(dir.path());

    let _ = run_cli(dir.path(), &["set", "agent/codex/task", "C"]);
    let _ = run_cli(dir.path(), &["set", "agent/claude/task", "A"]);
    let _ = run_cli(dir.path(), &["set", "agent/claude/context", "B"]);

    let output = run_cli(dir.path(), &["list"]);
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);

    let first = stdout.find("agent/claude/context").expect("first");
    let second = stdout.find("agent/claude/task").expect("second");
    let third = stdout.find("agent/codex/task").expect("third");
    assert!(first < second && second < third);
}

#[test]
fn typed_key_and_value_validation_examples() {
    let key = Key::new("agent/codex/current_task").expect("valid key");
    let value = Value::new("review docs").expect("valid value");
    assert_eq!(key.as_str(), "agent/codex/current_task");
    assert_eq!(value.as_str(), "review docs");
}