unified-agent-api-codex 0.3.5

Async wrapper around the Codex CLI for programmatic prompting
Documentation
use super::*;

#[tokio::test]
async fn exec_applies_guarded_flags_when_supported() {
    let _guard = env_guard_async().await;
    clear_capability_cache();

    let temp = tempfile::tempdir().unwrap();
    let log_path = temp.path().join("exec.log");
    let script = format!(
        r#"#!/bin/bash
log="{log}"
if [[ "$1" == "--version" ]]; then
  echo "codex 1.2.3"
elif [[ "$1" == "features" && "$2" == "list" && "$3" == "--json" ]]; then
  echo '{{"features":["output_schema","add_dir","mcp_login"]}}'
elif [[ "$1" == "features" && "$2" == "list" ]]; then
  echo "output_schema add_dir login --mcp"
elif [[ "$1" == "--help" ]]; then
  echo "Usage: codex --output-schema add-dir login --mcp"
elif [[ "$1" == "exec" ]]; then
  echo "$@" >> "$log"
  echo "ok"
fi
"#,
        log = log_path.display()
    );
    let binary = write_fake_codex(temp.path(), &script);
    let client = CodexClient::builder()
        .binary(&binary)
        .timeout(Duration::from_secs(5))
        .add_dir("src")
        .output_schema(true)
        .quiet(true)
        .mirror_stdout(false)
        .build();

    let response = client.send_prompt("hello").await.unwrap();
    assert_eq!(response.trim(), "ok");

    let logged = std_fs::read_to_string(&log_path).unwrap();
    assert!(logged.contains("--add-dir"));
    assert!(logged.contains("src"));
    assert!(logged.contains("--output-schema"));
}

#[tokio::test]
async fn exec_skips_guarded_flags_when_unknown() {
    let _guard = env_guard_async().await;
    clear_capability_cache();

    let temp = tempfile::tempdir().unwrap();
    let log_path = temp.path().join("exec.log");
    let script = format!(
        r#"#!/bin/bash
log="{log}"
if [[ "$1" == "--version" ]]; then
  echo "codex 0.9.0"
elif [[ "$1" == "features" && "$2" == "list" && "$3" == "--json" ]]; then
  echo "feature list unavailable" >&2
  exit 1
elif [[ "$1" == "features" && "$2" == "list" ]]; then
  echo "feature list unavailable" >&2
  exit 1
elif [[ "$1" == "--help" ]]; then
  echo "Usage: codex exec"
elif [[ "$1" == "exec" ]]; then
  echo "$@" >> "$log"
  echo "ok"
fi
"#,
        log = log_path.display()
    );
    let binary = write_fake_codex(temp.path(), &script);
    let client = CodexClient::builder()
        .binary(&binary)
        .timeout(Duration::from_secs(5))
        .add_dir("src")
        .output_schema(true)
        .quiet(true)
        .mirror_stdout(false)
        .build();

    let response = client.send_prompt("hello").await.unwrap();
    assert_eq!(response.trim(), "ok");

    let logged = std_fs::read_to_string(&log_path).unwrap();
    assert!(!logged.contains("--add-dir"));
    assert!(!logged.contains("--output-schema"));
}

#[tokio::test]
async fn mcp_login_skips_when_unsupported() {
    let _guard = env_guard_async().await;
    clear_capability_cache();

    let temp = tempfile::tempdir().unwrap();
    let log_path = temp.path().join("login.log");
    let script = format!(
        r#"#!/bin/bash
log="{log}"
if [[ "$1" == "--version" ]]; then
  echo "codex 1.0.0"
elif [[ "$1" == "features" && "$2" == "list" && "$3" == "--json" ]]; then
  echo '{{"features":["output_schema","add_dir"]}}'
elif [[ "$1" == "features" && "$2" == "list" ]]; then
  echo "output_schema add-dir"
elif [[ "$1" == "--help" ]]; then
  echo "Usage: codex exec"
elif [[ "$1" == "login" ]]; then
  echo "$@" >> "$log"
  echo "login invoked"
fi
"#,
        log = log_path.display()
    );
    let binary = write_fake_codex(temp.path(), &script);
    let client = CodexClient::builder()
        .binary(&binary)
        .timeout(Duration::from_secs(5))
        .build();

    let login = client.spawn_mcp_login_process().await.unwrap();
    assert!(login.is_none());
    assert!(!log_path.exists());
}

#[tokio::test]
async fn mcp_login_runs_when_supported() {
    let _guard = env_guard_async().await;
    clear_capability_cache();

    let temp = tempfile::tempdir().unwrap();
    let log_path = temp.path().join("login.log");
    let script = format!(
        r#"#!/bin/bash
log="{log}"
if [[ "$1" == "--version" ]]; then
  echo "codex 2.0.0"
elif [[ "$1" == "features" && "$2" == "list" && "$3" == "--json" ]]; then
  echo '{{"features":["output_schema","add_dir"],"mcp_login":true}}'
elif [[ "$1" == "features" && "$2" == "list" ]]; then
  echo "output_schema add_dir login --mcp"
elif [[ "$1" == "--help" ]]; then
  echo "Usage: codex --output-schema add-dir login --mcp"
elif [[ "$1" == "login" ]]; then
  echo "$@" >> "$log"
  echo "login invoked"
fi
"#,
        log = log_path.display()
    );
    let binary = write_fake_codex(temp.path(), &script);
    let client = CodexClient::builder()
        .binary(&binary)
        .timeout(Duration::from_secs(5))
        .build();

    let login = client
        .spawn_mcp_login_process()
        .await
        .unwrap()
        .expect("expected login child");
    let output = login.wait_with_output().await.unwrap();
    assert!(output.status.success());

    let logged = std_fs::read_to_string(&log_path).unwrap();
    assert!(logged.contains("login --mcp"));
}

#[tokio::test]
async fn probe_capabilities_caches_and_invalidates() {
    let _guard = env_guard_async().await;
    clear_capability_cache();

    let temp = tempfile::tempdir().unwrap();
    let script_v1 = r#"#!/bin/bash
if [[ "$1" == "--version" ]]; then
  echo "codex 1.2.3-beta (commit cafe123)"
elif [[ "$1" == "features" && "$2" == "list" && "$3" == "--json" ]]; then
  echo '{"features":["output_schema","add_dir","mcp_login"]}'
elif [[ "$1" == "features" && "$2" == "list" ]]; then
  echo "output_schema add-dir login --mcp"
elif [[ "$1" == "--help" ]]; then
  echo "Usage: codex --output-schema add-dir login --mcp"
fi
"#;
    let binary = write_fake_codex(temp.path(), script_v1);
    let client = CodexClient::builder()
        .binary(&binary)
        .timeout(Duration::from_secs(5))
        .build();

    let first = client.probe_capabilities().await;
    assert_eq!(
        first.version.as_ref().and_then(|v| v.semantic),
        Some((1, 2, 3))
    );
    assert_eq!(
        first.version.as_ref().map(|v| v.channel),
        Some(CodexReleaseChannel::Beta)
    );
    assert_eq!(
        first.version.as_ref().and_then(|v| v.commit.as_deref()),
        Some("cafe123")
    );
    assert!(first.features.supports_features_list);
    assert!(first.features.supports_output_schema);
    assert!(first.features.supports_add_dir);
    assert!(first.features.supports_mcp_login);

    let cached = client.probe_capabilities().await;
    assert_eq!(cached, first);

    let script_v2 = r#"#!/bin/bash
if [[ "$1" == "--version" ]]; then
  echo "codex 2.0.0 (commit deadbeef)"
elif [[ "$1" == "features" && "$2" == "list" && "$3" == "--json" ]]; then
  echo '{"features":["add_dir"]}'
elif [[ "$1" == "features" && "$2" == "list" ]]; then
  echo "add-dir"
elif [[ "$1" == "--help" ]]; then
  echo "Usage: codex add-dir"
fi
"#;
    std_fs::write(&binary, script_v2).unwrap();
    let mut perms = std_fs::metadata(&binary).unwrap().permissions();
    perms.set_mode(0o755);
    std_fs::set_permissions(&binary, perms).unwrap();

    let refreshed = client.probe_capabilities().await;
    assert_ne!(refreshed.version, first.version);
    assert_eq!(
        refreshed.version.as_ref().and_then(|v| v.semantic),
        Some((2, 0, 0))
    );
    assert!(refreshed.features.supports_features_list);
    assert!(refreshed.features.supports_add_dir);
    assert!(!refreshed.features.supports_output_schema);
    assert!(!refreshed.features.supports_mcp_login);
    clear_capability_cache();
}