moadim 3.2.1

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
#![allow(
    clippy::missing_docs_in_private_items,
    reason = "test helpers and fixtures do not need doc comments"
)]

use super::*;

/// A unique temp directory base for agent registry tests.
fn unique_dir(tag: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!("moadim-agents-{tag}-{}", uuid::Uuid::new_v4()))
}

#[test]
fn available_agents_in_falls_back_when_dir_has_no_toml() {
    // Covers the `names.is_empty()` → built-in defaults branch when the directory
    // is readable but contains no `.toml` stems.
    let dir = unique_dir("empty-readable");
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("notes.txt"), "ignore me").unwrap();

    assert_eq!(
        available_agents_in(&dir),
        vec![
            "claude".to_string(),
            "codex".to_string(),
            "hermes".to_string(),
            "pi".to_string()
        ]
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn codex_default_config_enables_sandbox_network_access() {
    // `codex exec`'s default workspace-write sandbox disables outbound network, which would block
    // an unattended routine from cloning the repo or pushing / opening a PR. The shipped default
    // must re-enable it; parse the actual config string and assert the override is present so the
    // flag can't silently regress to the network-disabled default.
    let cmd: AgentCommand =
        toml::from_str(super::codex::CONFIG).expect("codex default config must be valid TOML");
    assert_eq!(cmd.command, "codex");
    assert!(
        cmd.args
            .iter()
            .any(|arg| arg == "sandbox_workspace_write.network_access=true"),
        "codex default args must enable sandbox network access, got {:?}",
        cmd.args
    );
}

#[test]
fn hermes_default_config_uses_oneshot_prompt_argument() {
    let cmd: AgentCommand =
        toml::from_str(super::hermes::CONFIG).expect("hermes default config must be valid TOML");
    assert_eq!(cmd.command, "hermes");
    assert_eq!(
        cmd.args,
        vec![
            "-z".to_string(),
            "{prompt}".to_string(),
            "--ignore-rules".to_string()
        ]
    );
}

#[test]
fn load_agent_command_parses_a_valid_config() {
    // Happy path: a well-formed config resolves to an `AgentCommand` (Ok), unchanged from before.
    let agent_name = "load-agent-valid-zzz";
    std::fs::create_dir_all(crate::paths::agents_dir()).unwrap();
    let cfg = crate::paths::agent_toml_path(agent_name);
    std::fs::write(&cfg, "command = \"claude\"\nargs = [\"--help\"]\n").unwrap();

    let loaded = load_agent_command(agent_name).unwrap();
    assert_eq!(loaded.command, "claude");
    assert_eq!(loaded.args, vec!["--help".to_string()]);

    std::fs::remove_file(&cfg).unwrap();
}

#[test]
fn load_agent_command_reports_parse_error_for_malformed_config() {
    // A present-but-unparseable config must yield `Parse` (NOT `Missing`), carrying the toml error,
    // so callers can name the real cause instead of "config not found".
    let agent_name = "load-agent-malformed-zzz";
    std::fs::create_dir_all(crate::paths::agents_dir()).unwrap();
    let cfg = crate::paths::agent_toml_path(agent_name);
    std::fs::write(&cfg, "command = [\n").unwrap();

    match load_agent_command(agent_name) {
        Err(AgentLoadError::Parse(err)) => assert!(!err.is_empty()),
        other => panic!("expected Parse error, got {other:?}"),
    }

    std::fs::remove_file(&cfg).unwrap();
}

#[test]
fn load_agent_command_reports_missing_for_absent_config() {
    // No file on disk → `Missing` (and ONLY a genuine not-found), leaving the missing-file behavior
    // identical to before.
    assert!(matches!(
        load_agent_command("load-agent-absent-zzz"),
        Err(AgentLoadError::Missing)
    ));
}

#[test]
fn load_agent_command_reports_unreadable_for_non_not_found_io_error() {
    // A present-but-unreadable config (here: a directory at the `<name>.toml` path, which yields an
    // I/O error whose kind is NOT `NotFound`) must yield `Unreadable`, NOT `Missing` — so an
    // unreadable config is never mislabeled "config not found" and silently dropped.
    let agent_name = "load-agent-unreadable-zzz";
    std::fs::create_dir_all(crate::paths::agents_dir()).unwrap();
    let cfg = crate::paths::agent_toml_path(agent_name);
    std::fs::create_dir_all(&cfg).unwrap();

    match load_agent_command(agent_name) {
        Err(AgentLoadError::Unreadable(err)) => assert!(!err.is_empty()),
        other => panic!("expected Unreadable error, got {other:?}"),
    }

    std::fs::remove_dir_all(&cfg).unwrap();
}

#[test]
fn agent_load_error_display_distinguishes_variants() {
    // Each variant renders distinctly: missing vs. unreadable vs. malformed (the latter two carrying
    // the underlying error).
    assert_eq!(
        AgentLoadError::Missing.to_string(),
        "agent config not found"
    );
    assert_eq!(
        AgentLoadError::Unreadable("permission denied".to_string()).to_string(),
        "unreadable agent config: permission denied"
    );
    assert_eq!(
        AgentLoadError::Parse("boom".to_string()).to_string(),
        "malformed agent TOML: boom"
    );
}

#[test]
fn ensure_default_agents_seeds_into_override_home() {
    // Covers the public `ensure_default_agents` wrapper, which resolves `agents_dir()` through the
    // `MOADIM_HOME_OVERRIDE` seam and seeds the built-in configs there.
    let home = unique_dir("ensure-default");
    let previous = std::env::var_os("MOADIM_HOME_OVERRIDE");
    // SAFETY: tests run single-threaded (RUST_TEST_THREADS=1); the override is restored below.
    unsafe {
        std::env::set_var("MOADIM_HOME_OVERRIDE", &home);
    }

    ensure_default_agents();
    assert!(crate::paths::agents_dir().join("claude.toml").exists());

    // SAFETY: single-threaded harness; restore the saved value.
    unsafe {
        match previous {
            Some(value) => std::env::set_var("MOADIM_HOME_OVERRIDE", value),
            None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
        }
    }
    let _ = std::fs::remove_dir_all(&home);
}
include!("ensure_default_agents_in_returns_early_when_dir_is_uncreatable_tests.rs");