unified-agent-api 0.2.3

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

use tokio::io::{AsyncWriteExt, DuplexStream};

use crate::{mcp::AgentWrapperMcpCommandContext, AgentWrapperError};

use super::super::PINNED_SPAWN_FAILURE;

#[cfg(unix)]
use std::{
    fs,
    os::unix::fs::PermissionsExt,
    time::{SystemTime, UNIX_EPOCH},
};

pub(super) fn success_exit_status() -> ExitStatus {
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        ExitStatus::from_raw(0)
    }
    #[cfg(windows)]
    {
        use std::os::windows::process::ExitStatusExt;
        ExitStatus::from_raw(0)
    }
}

pub(super) fn exit_status_with_code(code: i32) -> ExitStatus {
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        ExitStatus::from_raw(code << 8)
    }
    #[cfg(windows)]
    {
        use std::os::windows::process::ExitStatusExt;
        ExitStatus::from_raw(code as u32)
    }
}

pub(super) fn sample_config() -> super::super::super::ClaudeCodeBackendConfig {
    super::super::super::ClaudeCodeBackendConfig {
        binary: Some(PathBuf::from("/tmp/fake-claude")),
        claude_home: Some(PathBuf::from("/tmp/claude-home")),
        default_timeout: Some(Duration::from_secs(30)),
        default_working_dir: Some(PathBuf::from("default/workdir")),
        env: BTreeMap::from([
            ("CONFIG_ONLY".to_string(), "config-only".to_string()),
            ("OVERRIDE_ME".to_string(), "config".to_string()),
        ]),
        ..Default::default()
    }
}

pub(super) fn sample_config_without_home() -> super::super::super::ClaudeCodeBackendConfig {
    super::super::super::ClaudeCodeBackendConfig {
        binary: Some(PathBuf::from("/tmp/fake-claude")),
        claude_home: None,
        default_timeout: Some(Duration::from_secs(30)),
        default_working_dir: Some(PathBuf::from("default/workdir")),
        env: BTreeMap::from([
            ("CONFIG_ONLY".to_string(), "config-only".to_string()),
            ("OVERRIDE_ME".to_string(), "config".to_string()),
        ]),
        ..Default::default()
    }
}

pub(super) fn sample_context() -> AgentWrapperMcpCommandContext {
    AgentWrapperMcpCommandContext {
        working_dir: Some(PathBuf::from("request/workdir")),
        timeout: Some(Duration::from_secs(5)),
        env: BTreeMap::from([
            ("OVERRIDE_ME".to_string(), "request".to_string()),
            ("REQUEST_ONLY".to_string(), "request-only".to_string()),
        ]),
    }
}

pub(super) fn test_env_lock() -> crate::backends::test_support::TestEnvLockGuard {
    crate::backends::test_support::test_env_lock()
}

pub(super) struct EnvGuard {
    key: &'static str,
    previous: Option<OsString>,
}

impl EnvGuard {
    pub(super) fn set(key: &'static str, value: impl Into<OsString>) -> Self {
        let previous = env::var_os(key);
        env::set_var(key, value.into());
        Self { key, previous }
    }

    pub(super) fn unset(key: &'static str) -> Self {
        let previous = env::var_os(key);
        env::remove_var(key);
        Self { key, previous }
    }
}

impl Drop for EnvGuard {
    fn drop(&mut self) {
        if let Some(value) = self.previous.take() {
            env::set_var(self.key, value);
        } else {
            env::remove_var(self.key);
        }
    }
}

pub(super) struct CurrentDirGuard {
    previous: PathBuf,
}

impl CurrentDirGuard {
    pub(super) fn set(path: &Path) -> Self {
        let previous = env::current_dir().unwrap_or_else(|_| env::temp_dir());
        env::set_current_dir(path).expect("current dir should be set");
        Self { previous }
    }
}

impl Drop for CurrentDirGuard {
    fn drop(&mut self) {
        if env::set_current_dir(&self.previous).is_err() {
            env::set_current_dir(env::temp_dir()).expect("fallback current dir should be restored");
        }
    }
}

pub(super) fn assert_backend_spawn_failure(err: AgentWrapperError) {
    match err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(message, PINNED_SPAWN_FAILURE);
        }
        other => panic!("expected Backend error, got: {other:?}"),
    }
}

pub(super) async fn write_all_and_close(mut writer: DuplexStream, bytes: Vec<u8>) {
    writer.write_all(&bytes).await.expect("write succeeds");
    writer.shutdown().await.expect("shutdown succeeds");
}

#[cfg(unix)]
pub(super) fn temp_test_dir(label: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock should be after epoch")
        .as_nanos();
    let dir = std::env::temp_dir().join(format!(
        "agent-api-claude-mcp-{label}-{}-{unique}",
        std::process::id()
    ));
    fs::create_dir_all(&dir).expect("temp dir should be created");
    dir
}

#[cfg(unix)]
pub(super) fn write_fake_claude(dir: &std::path::Path, script: &str) -> PathBuf {
    fs::create_dir_all(dir).expect("script directory should be created");
    let path = dir.join("claude");
    fs::write(&path, script).expect("script should be written");
    let mut permissions = fs::metadata(&path)
        .expect("script metadata should exist")
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&path, permissions).expect("script should be executable");
    path
}