unified-agent-api-codex 0.3.5

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

#[cfg(unix)]
#[tokio::test]
async fn features_list_maps_overrides_and_json_flag() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
echo "$PWD" 1>&2
printf "%s\n" "$@" 1>&2
cat <<'JSON'
[{"name":"json-stream","stage":"stable","enabled":true},{"name":"cloud-exec","stage":"experimental","enabled":false}]
JSON
"#,
    );

    let workdir = dir.path().join("features-workdir");
    std_fs::create_dir_all(&workdir).unwrap();

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .working_dir(&workdir)
        .approval_policy(ApprovalPolicy::OnRequest)
        .search(true)
        .build();

    let output = client
        .list_features(
            FeaturesListRequest::new()
                .json(true)
                .profile("dev")
                .config_override("features.extras", "true"),
        )
        .await
        .unwrap();

    assert_eq!(output.format, FeaturesListFormat::Json);
    assert_eq!(output.features.len(), 2);
    assert_eq!(output.features[0].stage, Some(CodexFeatureStage::Stable));
    assert!(output.features[0].enabled);
    assert!(!output.features[1].enabled);

    let mut lines = output.stderr.lines();
    let pwd = lines.next().unwrap();
    let pwd = std_fs::canonicalize(Path::new(pwd)).unwrap();
    let workdir = std_fs::canonicalize(&workdir).unwrap();
    assert_eq!(pwd, workdir);

    let args: Vec<_> = lines.map(str::to_string).collect();
    assert_eq!(
        args,
        vec![
            "--config",
            "features.extras=true",
            "--profile",
            "dev",
            "--ask-for-approval",
            "on-request",
            "--search",
            "features",
            "list",
            "--json"
        ]
    );
}

#[cfg(unix)]
#[tokio::test]
async fn supports_help_review_fork_resume_and_features_commands() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
printf "%s\n" "$@"
"#,
    );

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();

    let features = client
        .features(FeaturesCommandRequest::new())
        .await
        .unwrap();
    assert_eq!(
        features.stdout.lines().collect::<Vec<_>>(),
        vec!["features"]
    );

    let help = client
        .help(HelpCommandRequest::new(HelpScope::Root).command(["exec", "review"]))
        .await
        .unwrap();
    assert_eq!(
        help.stdout.lines().collect::<Vec<_>>(),
        vec!["help", "exec", "review"]
    );

    let review = client
        .review(
            ReviewCommandRequest::new()
                .base("main")
                .commit("abc123")
                .title("hello")
                .uncommitted(true)
                .prompt("please review"),
        )
        .await
        .unwrap();
    assert_eq!(
        review.stdout.lines().collect::<Vec<_>>(),
        vec![
            "review",
            "--base",
            "main",
            "--commit",
            "abc123",
            "--title",
            "hello",
            "--uncommitted",
            "please review"
        ]
    );

    let exec_review = client
        .exec_review(
            ExecReviewCommandRequest::new()
                .base("main")
                .commit("abc123")
                .title("hello")
                .uncommitted(true)
                .json(true)
                .prompt("please review"),
        )
        .await
        .unwrap();
    assert_eq!(
        exec_review.stdout.lines().collect::<Vec<_>>(),
        vec![
            "exec",
            "review",
            "--base",
            "main",
            "--commit",
            "abc123",
            "--json",
            "--skip-git-repo-check",
            "--title",
            "hello",
            "--uncommitted",
            "please review"
        ]
    );

    let resume = client
        .resume_session(
            ResumeSessionRequest::new()
                .all(true)
                .last(true)
                .session_id("sess-1")
                .prompt("resume prompt"),
        )
        .await
        .unwrap();
    assert_eq!(
        resume.stdout.lines().collect::<Vec<_>>(),
        vec!["resume", "--all", "--last", "sess-1", "resume prompt"]
    );

    let fork = client
        .fork_session(
            ForkSessionRequest::new()
                .all(true)
                .last(true)
                .session_id("sess-1")
                .prompt("fork prompt"),
        )
        .await
        .unwrap();
    assert_eq!(
        fork.stdout.lines().collect::<Vec<_>>(),
        vec!["fork", "--all", "--last", "sess-1", "fork prompt"]
    );
}

#[cfg(unix)]
#[tokio::test]
async fn supports_debug_models_prompt_input_and_plugin_commands() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
printf "%s\n" "$@"
"#,
    );

    let image_one = dir.path().join("prompt-1.png");
    let image_two = dir.path().join("prompt-2.png");

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();

    let debug_models = client
        .debug_models(DebugModelsRequest::new().bundled(true))
        .await
        .unwrap();
    assert_eq!(
        debug_models.stdout.lines().collect::<Vec<_>>(),
        vec!["debug", "models", "--bundled"]
    );

    let prompt_input = client
        .debug_prompt_input(
            DebugPromptInputRequest::new()
                .image(&image_one)
                .image(&image_two)
                .prompt("hello"),
        )
        .await
        .unwrap();
    assert_eq!(
        prompt_input.stdout.lines().collect::<Vec<_>>(),
        vec![
            "debug",
            "prompt-input",
            "--image",
            image_one.to_string_lossy().as_ref(),
            "--image",
            image_two.to_string_lossy().as_ref(),
            "hello",
        ]
    );

    let plugin = client.plugin(PluginCommandRequest::new()).await.unwrap();
    assert_eq!(plugin.stdout.lines().collect::<Vec<_>>(), vec!["plugin"]);

    let plugin_help = client
        .plugin_help(PluginHelpRequest::new().command(["marketplace", "add"]))
        .await
        .unwrap();
    assert_eq!(
        plugin_help.stdout.lines().collect::<Vec<_>>(),
        vec!["plugin", "help", "marketplace", "add"]
    );

    let plugin_marketplace = client
        .plugin_marketplace(PluginMarketplaceCommandRequest::new())
        .await
        .unwrap();
    assert_eq!(
        plugin_marketplace.stdout.lines().collect::<Vec<_>>(),
        vec!["plugin", "marketplace"]
    );

    let plugin_marketplace_help = client
        .plugin_marketplace_help(PluginMarketplaceHelpRequest::new().command(["upgrade"]))
        .await
        .unwrap();
    assert_eq!(
        plugin_marketplace_help.stdout.lines().collect::<Vec<_>>(),
        vec!["plugin", "marketplace", "help", "upgrade"]
    );

    let plugin_marketplace_add = client
        .plugin_marketplace_add(
            PluginMarketplaceAddRequest::new("owner/repo")
                .source_ref("main")
                .sparse_path("marketplaces/core"),
        )
        .await
        .unwrap();
    assert_eq!(
        plugin_marketplace_add.stdout.lines().collect::<Vec<_>>(),
        vec![
            "plugin",
            "marketplace",
            "add",
            "--ref",
            "main",
            "--sparse",
            "marketplaces/core",
            "owner/repo",
        ]
    );

    let plugin_marketplace_remove = client
        .plugin_marketplace_remove(PluginMarketplaceRemoveRequest::new("primary"))
        .await
        .unwrap();
    assert_eq!(
        plugin_marketplace_remove.stdout.lines().collect::<Vec<_>>(),
        vec!["plugin", "marketplace", "remove", "primary"]
    );

    let plugin_marketplace_upgrade = client
        .plugin_marketplace_upgrade(
            PluginMarketplaceUpgradeRequest::new().marketplace_name("primary"),
        )
        .await
        .unwrap();
    assert_eq!(
        plugin_marketplace_upgrade
            .stdout
            .lines()
            .collect::<Vec<_>>(),
        vec!["plugin", "marketplace", "upgrade", "primary"]
    );
}