mempal 0.6.0

Project memory for coding agents. Single binary, hybrid search, knowledge graph.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use mempal::core::db::Database;
use serde_json::Value;
use tempfile::TempDir;

fn mempal_bin() -> String {
    env!("CARGO_BIN_EXE_mempal").to_string()
}

fn run_mempal(home: &TempDir, args: &[&str]) -> std::process::Output {
    Command::new(mempal_bin())
        .args(args)
        .env("HOME", home.path())
        .output()
        .expect("run mempal")
}

fn run_mempal_with_path(home: &TempDir, args: &[&str], path_value: &str) -> std::process::Output {
    Command::new(mempal_bin())
        .args(args)
        .env("HOME", home.path())
        .env("PATH", path_value)
        .output()
        .expect("run mempal")
}

fn stdout(output: &std::process::Output) -> String {
    String::from_utf8_lossy(&output.stdout).into_owned()
}

fn stderr(output: &std::process::Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}

fn assert_success(output: &std::process::Output) {
    assert!(
        output.status.success(),
        "command failed\nstdout:\n{}\nstderr:\n{}",
        stdout(output),
        stderr(output)
    );
}

fn palace_db_path(home: &TempDir) -> PathBuf {
    home.path().join(".mempal/palace.db")
}

fn install_fake_path_mempal(home: &TempDir) -> String {
    let bin_dir = home.path().join("fake-bin");
    fs::create_dir_all(&bin_dir).expect("create fake bin");
    let fake = bin_dir.join("mempal");
    fs::write(&fake, "#!/bin/sh\nprintf 'mempal 0.0.0\\n'\n").expect("write fake mempal");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&fake).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&fake, perms).unwrap();
    }
    let old_path = std::env::var("PATH").unwrap_or_default();
    format!("{}:{old_path}", bin_dir.display())
}

#[test]
fn test_cli_doctor_json_reports_schema_and_path() {
    let home = TempDir::new().expect("home");
    fs::create_dir_all(home.path().join(".mempal")).expect("create mempal home");
    let db = Database::open(&palace_db_path(&home)).expect("open db");
    assert_eq!(db.schema_version().expect("schema"), 9);
    drop(db);
    let path_value = install_fake_path_mempal(&home);

    let output = run_mempal_with_path(&home, &["doctor", "--format", "json"], &path_value);
    assert_success(&output);
    let value: Value = serde_json::from_str(&stdout(&output)).expect("doctor json");
    assert_eq!(
        value["current_version"].as_str(),
        Some(env!("CARGO_PKG_VERSION"))
    );
    assert_eq!(value["supported_schema_version"], 9);
    assert_eq!(value["db"]["exists"], true);
    assert_eq!(value["db"]["schema_version"], 9);
    assert_eq!(value["install"]["path_matches_current_exe"], false);
    assert!(
        value["warnings"]
            .as_array()
            .expect("warnings")
            .iter()
            .any(|warning| warning.as_str().unwrap_or_default().contains("PATH"))
    );
}

#[test]
fn test_cli_doctor_plain_no_db_is_read_only() {
    let home = TempDir::new().expect("home");
    let output = run_mempal(&home, &["doctor", "--format", "plain"]);
    assert_success(&output);
    let out = stdout(&output);
    assert!(out.contains("db_exists=false"), "{out}");
    assert!(!palace_db_path(&home).exists());
}

#[test]
fn test_cli_doctor_rejects_invalid_format() {
    let home = TempDir::new().expect("home");
    let output = run_mempal(&home, &["doctor", "--format", "yaml"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("unsupported doctor format"));
    assert!(!palace_db_path(&home).exists());
}

#[test]
fn test_cli_maintenance_guided_run_json() {
    let home = TempDir::new().expect("home");
    fs::create_dir_all(home.path().join(".mempal")).expect("create mempal home");
    Database::open(&palace_db_path(&home)).expect("open db");

    let output = run_mempal(&home, &["maintenance", "guided-run", "--format", "json"]);
    assert_success(&output);
    let value: Value = serde_json::from_str(&stdout(&output)).expect("guided run json");
    assert_eq!(value["writes"], false);
    let commands = value["steps"]
        .as_array()
        .expect("steps")
        .iter()
        .filter_map(|step| step["command"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    assert!(commands.contains("research-validate-plan"), "{commands}");
    assert!(commands.contains("adoption review"), "{commands}");
    assert!(commands.contains("cowork-doctor"), "{commands}");
}

#[test]
fn test_cli_maintenance_guided_run_plain() {
    let home = TempDir::new().expect("home");
    fs::create_dir_all(home.path().join(".mempal")).expect("create mempal home");
    Database::open(&palace_db_path(&home)).expect("open db");

    let output = run_mempal(&home, &["maintenance", "guided-run", "--format", "plain"]);
    assert_success(&output);
    let out = stdout(&output);
    assert!(out.contains("Guided Maintenance Run"), "{out}");
    assert!(out.contains("mempal phase3 adoption review"), "{out}");
    assert!(out.contains("mempal cowork-capture"), "{out}");
}

#[test]
fn test_cli_maintenance_guided_run_rejects_invalid_format() {
    let home = TempDir::new().expect("home");
    fs::create_dir_all(home.path().join(".mempal")).expect("create mempal home");
    Database::open(&palace_db_path(&home)).expect("open db");

    let output = run_mempal(&home, &["maintenance", "guided-run", "--format", "yaml"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("unsupported maintenance guided-run format"));
}

#[test]
fn test_cli_release_readiness_json() {
    let home = TempDir::new().expect("home");
    let output = Command::new(mempal_bin())
        .args(["release-readiness", "--format", "json"])
        .env("HOME", home.path())
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run mempal");
    assert_success(&output);
    let value: Value = serde_json::from_str(&stdout(&output)).expect("release readiness json");
    assert_eq!(value["writes"], false);
    let check_names = value["checks"]
        .as_array()
        .expect("checks")
        .iter()
        .filter_map(|check| check["name"].as_str())
        .collect::<Vec<_>>();
    assert!(check_names.contains(&"cargo-metadata"), "{check_names:?}");
    assert!(
        check_names.contains(&"spec-plan-inventory"),
        "{check_names:?}"
    );
    assert!(
        value["recommended_commands"]
            .as_array()
            .expect("commands")
            .iter()
            .any(|command| command
                .as_str()
                .unwrap_or_default()
                .contains("cargo package"))
    );
}

#[test]
fn test_cli_release_readiness_plain() {
    let home = TempDir::new().expect("home");
    let output = Command::new(mempal_bin())
        .args(["release-readiness", "--format", "plain"])
        .env("HOME", home.path())
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run mempal");
    assert_success(&output);
    let out = stdout(&output);
    assert!(out.contains("Release Readiness"), "{out}");
    assert!(out.contains("cargo package"), "{out}");
    assert!(out.contains("mempal doctor"), "{out}");
}

#[test]
fn test_cli_release_readiness_rejects_invalid_format() {
    let home = TempDir::new().expect("home");
    let output = Command::new(mempal_bin())
        .args(["release-readiness", "--format", "yaml"])
        .env("HOME", home.path())
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run mempal");
    assert!(!output.status.success());
    assert!(stderr(&output).contains("unsupported release-readiness format"));
}