unified-agent-api 0.3.5

Agent-agnostic facade and registry for wrapper backends
Documentation
use std::{env, ffi::OsString, path::PathBuf, time::Duration};

use crate::mcp::AgentWrapperMcpCommandContext;

#[cfg(unix)]
use std::fs;

use super::super::{
    resolve::{resolve_codex_binary_path, resolve_codex_mcp_command},
    CODEX_BINARY_ENV, CODEX_HOME_ENV, PATH_ENV,
};
use super::support::{
    assert_backend_spawn_failure, sample_config, sample_context, test_env_lock, CurrentDirGuard,
    EnvGuard,
};

#[test]
fn resolve_codex_mcp_command_applies_precedence_and_materializes_injected_home() {
    let resolved = resolve_codex_mcp_command(&sample_config(), &sample_context()).expect("resolve");

    assert_eq!(resolved.binary_path, PathBuf::from("/tmp/fake-codex"));
    assert_eq!(resolved.working_dir, Some(PathBuf::from("request/workdir")));
    assert_eq!(resolved.timeout, Some(Duration::from_secs(5)));
    assert_eq!(
        resolved.env.get("CONFIG_ONLY").map(String::as_str),
        Some("config-only")
    );
    assert_eq!(
        resolved.env.get("OVERRIDE_ME").map(String::as_str),
        Some("request")
    );
    assert_eq!(
        resolved.env.get("REQUEST_ONLY").map(String::as_str),
        Some("request-only")
    );
    assert_eq!(
        resolved
            .env
            .get(super::super::CODEX_BINARY_ENV)
            .map(String::as_str),
        Some("/tmp/fake-codex")
    );
    assert_eq!(
        resolved.env.get(CODEX_HOME_ENV).map(String::as_str),
        Some("/tmp/codex-home")
    );
    assert_eq!(
        resolved.materialize_codex_home,
        Some(PathBuf::from("/tmp/codex-home"))
    );
}

#[test]
fn resolve_codex_mcp_command_skips_materialize_when_request_overrides_codex_home() {
    let mut context = sample_context();
    context
        .env
        .insert(CODEX_HOME_ENV.to_string(), "/tmp/request-home".to_string());

    let resolved = resolve_codex_mcp_command(&sample_config(), &context).expect("resolve");

    assert_eq!(
        resolved.env.get(CODEX_HOME_ENV).map(String::as_str),
        Some("/tmp/request-home")
    );
    assert_eq!(resolved.materialize_codex_home, None);
}

#[test]
fn resolve_codex_mcp_command_uses_backend_defaults_when_request_values_absent() {
    let resolved =
        resolve_codex_mcp_command(&sample_config(), &AgentWrapperMcpCommandContext::default())
            .expect("resolve");

    assert_eq!(resolved.working_dir, Some(PathBuf::from("default/workdir")));
    assert_eq!(resolved.timeout, Some(Duration::from_secs(30)));
}

#[cfg(unix)]
#[test]
fn resolve_codex_mcp_command_canonicalizes_relative_binary_before_working_dir() {
    let _env_lock = test_env_lock();
    let wrapper_dir = super::support::temp_test_dir("relative-wrapper");
    let binary_dir = wrapper_dir.join("bin");
    let working_dir = super::support::temp_test_dir("relative-working-dir");
    let binary_path =
        super::support::write_fake_codex(&binary_dir, "#!/usr/bin/env bash\nexit 0\n");
    let cwd_guard = CurrentDirGuard::set(&wrapper_dir);

    let mut config = sample_config();
    config.binary = Some(PathBuf::from("bin/codex"));
    config.default_working_dir = Some(working_dir.clone());

    let resolved = resolve_codex_mcp_command(&config, &AgentWrapperMcpCommandContext::default())
        .expect("resolve");

    assert_eq!(
        resolved.binary_path,
        std::fs::canonicalize(&binary_path).expect("canonicalize fake codex")
    );
    assert!(resolved.binary_path.is_absolute());
    assert_eq!(resolved.working_dir, Some(working_dir.clone()));

    drop(cwd_guard);
    std::fs::remove_dir_all(wrapper_dir).expect("wrapper dir should be removed");
    std::fs::remove_dir_all(working_dir).expect("working dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_mcp_command_prefers_request_path_in_child_env() {
    let _env_lock = test_env_lock();
    let ambient_dir = super::support::temp_test_dir("ambient-command-path");
    let _ambient_path = EnvGuard::set(PATH_ENV, ambient_dir.as_os_str().to_os_string());

    let request_path = "/tmp/request-path".to_string();
    let mut config = sample_config();
    config
        .env
        .insert(PATH_ENV.to_string(), "/tmp/config-path".to_string());
    let mut context = sample_context();
    context
        .env
        .insert(PATH_ENV.to_string(), request_path.clone());

    let resolved = resolve_codex_mcp_command(&config, &context).expect("resolve");

    assert_eq!(resolved.env.get(PATH_ENV), Some(&request_path));

    std::fs::remove_dir_all(ambient_dir).expect("ambient dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_mcp_command_uses_config_path_in_child_env_when_request_missing() {
    let _env_lock = test_env_lock();
    let ambient_dir = super::support::temp_test_dir("ambient-config-path");
    let _ambient_path = EnvGuard::set(PATH_ENV, ambient_dir.as_os_str().to_os_string());

    let config_path = "/tmp/config-path".to_string();
    let mut config = sample_config();
    config.env.insert(PATH_ENV.to_string(), config_path.clone());

    let resolved = resolve_codex_mcp_command(&config, &AgentWrapperMcpCommandContext::default())
        .expect("resolve");

    assert_eq!(resolved.env.get(PATH_ENV), Some(&config_path));

    std::fs::remove_dir_all(ambient_dir).expect("ambient dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_mcp_command_injects_ambient_path_into_child_env_when_unset() {
    let _env_lock = test_env_lock();
    let ambient_dir = super::support::temp_test_dir("ambient-only-command-path");
    let _ambient_path = EnvGuard::set(PATH_ENV, ambient_dir.as_os_str().to_os_string());

    let resolved =
        resolve_codex_mcp_command(&sample_config(), &AgentWrapperMcpCommandContext::default())
            .expect("resolve");

    assert_eq!(
        resolved.env.get(PATH_ENV).map(String::as_str),
        Some(ambient_dir.to_string_lossy().as_ref())
    );

    std::fs::remove_dir_all(ambient_dir).expect("ambient dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_binary_path_uses_effective_path_env_for_unqualified_binary() {
    let temp_dir = super::support::temp_test_dir("binary-path");
    let script_path = super::support::write_fake_codex(&temp_dir, "#!/usr/bin/env bash\nexit 0\n");

    let resolved = resolve_codex_binary_path(
        None,
        Some(OsString::from("codex")),
        Some(temp_dir.to_string_lossy().as_ref()),
        None,
        None,
        None,
    )
    .expect("effective PATH should resolve codex");

    assert_eq!(
        resolved,
        std::fs::canonicalize(&script_path).expect("canonicalize fake codex")
    );

    std::fs::remove_dir_all(temp_dir).expect("temp dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_binary_path_prefers_request_path_over_ambient_path() {
    let _env_lock = test_env_lock();
    let request_dir = super::support::temp_test_dir("request-path");
    let ambient_dir = super::support::temp_test_dir("ambient-path");
    let request_binary =
        super::support::write_fake_codex(&request_dir, "#!/usr/bin/env bash\nexit 0\n");
    let _ambient_binary =
        super::support::write_fake_codex(&ambient_dir, "#!/usr/bin/env bash\nexit 0\n");
    let _ambient_path = EnvGuard::set(PATH_ENV, ambient_dir.as_os_str().to_os_string());

    let resolved = resolve_codex_binary_path(
        None,
        Some(OsString::from("codex")),
        Some(request_dir.to_string_lossy().as_ref()),
        env::var_os(PATH_ENV),
        None,
        None,
    )
    .expect("request PATH should resolve codex");

    assert_eq!(
        resolved,
        std::fs::canonicalize(&request_binary).expect("canonicalize request binary")
    );

    std::fs::remove_dir_all(request_dir).expect("request dir should be removed");
    std::fs::remove_dir_all(ambient_dir).expect("ambient dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_binary_path_uses_ambient_path_when_effective_path_is_absent() {
    let _env_lock = test_env_lock();
    let ambient_dir = super::support::temp_test_dir("ambient-only");
    let ambient_binary =
        super::support::write_fake_codex(&ambient_dir, "#!/usr/bin/env bash\nexit 0\n");
    let _ambient_path = EnvGuard::set(PATH_ENV, ambient_dir.as_os_str().to_os_string());
    let _codex_binary = EnvGuard::unset(CODEX_BINARY_ENV);

    let resolved = resolve_codex_binary_path(None, None, None, env::var_os(PATH_ENV), None, None)
        .expect("ambient PATH should resolve codex");

    assert_eq!(
        resolved,
        std::fs::canonicalize(&ambient_binary).expect("canonicalize ambient binary")
    );

    std::fs::remove_dir_all(ambient_dir).expect("ambient dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_binary_path_skips_non_executable_shadow_file_on_path() {
    let shadow_dir = super::support::temp_test_dir("shadow-path");
    let executable_dir = super::support::temp_test_dir("executable-path");
    let shadow_path = shadow_dir.join("codex");
    let executable_path =
        super::support::write_fake_codex(&executable_dir, "#!/usr/bin/env bash\nexit 0\n");
    fs::write(&shadow_path, "shadow").expect("shadow file should be written");

    let joined_path = env::join_paths([shadow_dir.as_path(), executable_dir.as_path()])
        .expect("join PATH entries");
    let resolved = resolve_codex_binary_path(
        None,
        Some(OsString::from("codex")),
        Some(joined_path.to_string_lossy().as_ref()),
        None,
        None,
        None,
    )
    .expect("effective PATH should skip non-executable shadow");

    assert_eq!(
        resolved,
        fs::canonicalize(&executable_path).expect("canonicalize executable path")
    );

    fs::remove_dir_all(shadow_dir).expect("shadow dir should be removed");
    fs::remove_dir_all(executable_dir).expect("executable dir should be removed");
}

#[test]
fn resolve_codex_binary_path_rejects_unresolved_default_binary() {
    let err = resolve_codex_binary_path(None, None, None, None, None, None)
        .expect_err("default bare codex should fail when PATH cannot resolve it");

    assert_backend_spawn_failure(err);
}

#[cfg(unix)]
#[test]
fn resolve_codex_mcp_command_resolves_relative_request_path_from_request_working_dir() {
    let _env_lock = test_env_lock();
    let request_dir = super::support::temp_test_dir("request-working-dir");
    let default_dir = super::support::temp_test_dir("default-working-dir");
    let request_binary =
        super::support::write_fake_codex(&request_dir.join("bin"), "#!/usr/bin/env bash\nexit 0\n");
    let _default_binary =
        super::support::write_fake_codex(&default_dir.join("bin"), "#!/usr/bin/env bash\nexit 0\n");
    let _codex_binary = EnvGuard::unset(CODEX_BINARY_ENV);

    let mut config = sample_config();
    config.binary = None;
    config.default_working_dir = Some(default_dir.clone());

    let mut context = AgentWrapperMcpCommandContext {
        working_dir: Some(request_dir.clone()),
        ..Default::default()
    };
    context.env.insert(PATH_ENV.to_string(), "bin".to_string());

    let resolved = resolve_codex_mcp_command(&config, &context).expect("resolve");

    assert_eq!(
        resolved.binary_path,
        std::fs::canonicalize(&request_binary).expect("canonicalize request binary")
    );
    assert_eq!(resolved.working_dir, Some(request_dir.clone()));

    std::fs::remove_dir_all(request_dir).expect("request dir should be removed");
    std::fs::remove_dir_all(default_dir).expect("default dir should be removed");
}

#[cfg(unix)]
#[test]
fn resolve_codex_mcp_command_resolves_relative_config_path_from_default_working_dir() {
    let _env_lock = test_env_lock();
    let default_dir = super::support::temp_test_dir("config-default-working-dir");
    let default_binary =
        super::support::write_fake_codex(&default_dir.join("bin"), "#!/usr/bin/env bash\nexit 0\n");
    let _codex_binary = EnvGuard::unset(CODEX_BINARY_ENV);

    let mut config = sample_config();
    config.binary = None;
    config.default_working_dir = Some(default_dir.clone());
    config.env.insert(PATH_ENV.to_string(), "bin".to_string());

    let resolved = resolve_codex_mcp_command(&config, &AgentWrapperMcpCommandContext::default())
        .expect("resolve");

    assert_eq!(
        resolved.binary_path,
        std::fs::canonicalize(&default_binary).expect("canonicalize default binary")
    );
    assert_eq!(resolved.working_dir, Some(default_dir.clone()));

    std::fs::remove_dir_all(default_dir).expect("default dir should be removed");
}