unified-agent-api-codex 0.3.5

Async wrapper around the Codex CLI for programmatic prompting
Documentation
use super::super::test_support::{prelude::*, *};
use super::super::*;

#[test]
fn runtime_api_lists_launchers_without_changing_config() {
    let (dir, manager) = temp_config_manager();
    let stdio_env_key = "MCP_RUNTIME_API_STDIO_ENV";
    let request_env_key = "MCP_RUNTIME_API_REQUEST_ENV";
    let http_env_key = "MCP_RUNTIME_API_HTTP_ENV";
    env::set_var(http_env_key, "token-api");

    let mut stdio = stdio_definition("runtime-api-stdio");
    stdio.description = Some("stdio runtime".into());
    stdio.tags = vec!["local".into()];
    stdio.tools = Some(McpToolConfig {
        enabled: vec!["fmt".into()],
        disabled: vec!["lint".into()],
    });
    if let McpTransport::Stdio(ref mut stdio_def) = stdio.transport {
        stdio_def.args.push("--flag".into());
        stdio_def
            .env
            .insert(stdio_env_key.into(), "runtime-env".into());
        stdio_def.timeout_ms = Some(2400);
    }

    let mut env_map = BTreeMap::new();
    env_map.insert(request_env_key.to_string(), "injected".to_string());

    manager
        .add_server(AddServerRequest {
            name: "local-api".into(),
            definition: stdio,
            overwrite: false,
            env: env_map,
            bearer_token: None,
        })
        .expect("add stdio server");

    let mut http = streamable_definition("https://example.test/runtime-api", http_env_key);
    http.description = Some("http runtime".into());
    http.tags = vec!["remote".into()];
    http.tools = Some(McpToolConfig {
        enabled: vec!["alpha".into()],
        disabled: vec!["beta".into()],
    });
    if let McpTransport::StreamableHttp(ref mut http_def) = http.transport {
        http_def.headers.insert("X-Req".into(), "true".into());
        http_def.request_timeout_ms = Some(2200);
    }

    manager
        .add_server(AddServerRequest {
            name: "remote-api".into(),
            definition: http,
            overwrite: false,
            env: BTreeMap::new(),
            bearer_token: None,
        })
        .expect("add http server");

    let before = fs::read_to_string(manager.config_path()).expect("read config before");
    let cwd = dir.path().join("cwd");

    let defaults = StdioServerConfig {
        binary: PathBuf::from("codex"),
        code_home: Some(dir.path().to_path_buf()),
        current_dir: Some(cwd.clone()),
        env: vec![
            (OsString::from("DEFAULT_ONLY"), OsString::from("default")),
            (
                OsString::from(request_env_key),
                OsString::from("base-default"),
            ),
        ],
        app_server_analytics_default_enabled: false,
        mirror_stdio: true,
        startup_timeout: Duration::from_secs(3),
    };

    let api = McpRuntimeApi::from_config(&manager, &defaults).expect("runtime api");

    let available = api.available();
    assert_eq!(available.len(), 2);

    let stdio_summary = available
        .iter()
        .find(|entry| entry.name == "local-api")
        .expect("stdio summary");
    assert_eq!(stdio_summary.transport, McpRuntimeSummaryTransport::Stdio);
    let stdio_tools = stdio_summary.tools.as_ref().expect("stdio tools");
    assert_eq!(stdio_tools.enabled, vec!["fmt".to_string()]);
    assert_eq!(stdio_tools.disabled, vec!["lint".to_string()]);

    let stdio_launcher = api.stdio_launcher("local-api").expect("stdio launcher");
    assert_eq!(stdio_launcher.args, vec!["--flag".to_string()]);
    assert_eq!(stdio_launcher.timeout, Duration::from_millis(2400));
    assert!(stdio_launcher.mirror_stdio);
    assert_eq!(stdio_launcher.current_dir.as_deref(), Some(cwd.as_path()));

    let env_map: HashMap<OsString, OsString> = stdio_launcher.env.into_iter().collect();
    assert_eq!(
        env_map.get(&OsString::from("CODEX_HOME")),
        Some(&dir.path().as_os_str().to_os_string())
    );
    assert_eq!(
        env_map.get(&OsString::from("DEFAULT_ONLY")),
        Some(&OsString::from("default"))
    );
    assert_eq!(
        env_map.get(&OsString::from(request_env_key)),
        Some(&OsString::from("injected"))
    );
    assert_eq!(
        env_map.get(&OsString::from(stdio_env_key)),
        Some(&OsString::from("runtime-env"))
    );

    let http_connector = api.http_connector("remote-api").expect("http connector");
    assert_eq!(http_connector.bearer_token.as_deref(), Some("token-api"));
    assert_eq!(
        http_connector
            .headers
            .get("Authorization")
            .map(String::as_str),
        Some("Bearer token-api")
    );
    assert_eq!(
        http_connector.headers.get("X-Req").map(String::as_str),
        Some("true")
    );
    assert_eq!(
        http_connector.request_timeout,
        Some(Duration::from_millis(2200))
    );

    let http_tools = available
        .iter()
        .find(|entry| entry.name == "remote-api")
        .and_then(|entry| entry.tools.as_ref())
        .expect("http tools");
    assert_eq!(http_tools.enabled, vec!["alpha".to_string()]);
    assert_eq!(http_tools.disabled, vec!["beta".to_string()]);

    match api.stdio_launcher("remote-api") {
        Err(McpRuntimeError::UnsupportedTransport {
            name,
            expected,
            actual,
        }) => {
            assert_eq!(name, "remote-api");
            assert_eq!(expected, "stdio");
            assert_eq!(actual, "streamable_http");
        }
        other => panic!("unexpected result: {other:?}"),
    }

    match api.http_connector("local-api") {
        Err(McpRuntimeError::UnsupportedTransport {
            name,
            expected,
            actual,
        }) => {
            assert_eq!(name, "local-api");
            assert_eq!(expected, "streamable_http");
            assert_eq!(actual, "stdio");
        }
        other => panic!("unexpected http result: {other:?}"),
    }

    let after = fs::read_to_string(manager.config_path()).expect("read config after");
    assert_eq!(before, after);

    env::remove_var(http_env_key);
    env::remove_var(request_env_key);
}

#[test]
fn runtime_api_prepare_http_is_non_destructive() {
    let (dir, manager) = temp_config_manager();
    let env_var = "MCP_RUNTIME_API_PREPARE";
    env::set_var(env_var, "prepare-token");

    let mut http = streamable_definition("https://example.test/prepare", env_var);
    http.tags = vec!["prepare".into()];
    http.tools = Some(McpToolConfig {
        enabled: vec!["delta".into()],
        disabled: vec![],
    });

    manager
        .add_server(AddServerRequest {
            name: "prepare-http".into(),
            definition: http,
            overwrite: false,
            env: BTreeMap::new(),
            bearer_token: None,
        })
        .expect("add http server");

    let before = fs::read_to_string(manager.config_path()).expect("read config before");

    let defaults = StdioServerConfig {
        binary: PathBuf::from("codex"),
        code_home: Some(dir.path().to_path_buf()),
        current_dir: None,
        env: Vec::new(),
        app_server_analytics_default_enabled: false,
        mirror_stdio: false,
        startup_timeout: Duration::from_secs(2),
    };

    let api = McpRuntimeApi::from_config(&manager, &defaults).expect("runtime api");
    let handle = api.prepare("prepare-http").expect("prepare http");

    match handle {
        McpRuntimeHandle::StreamableHttp(http_handle) => {
            assert_eq!(http_handle.name, "prepare-http");
            assert_eq!(
                http_handle.connector.bearer_token.as_deref(),
                Some("prepare-token")
            );
            assert_eq!(
                http_handle
                    .connector
                    .headers
                    .get("Authorization")
                    .map(String::as_str),
                Some("Bearer prepare-token")
            );
            let tools = http_handle.tools.expect("tool hints");
            assert_eq!(tools.enabled, vec!["delta".to_string()]);
        }
        other => panic!("expected http handle, got {other:?}"),
    }

    let after = fs::read_to_string(manager.config_path()).expect("read config after");
    assert_eq!(before, after);

    env::remove_var(env_var);
}