unified-agent-api 0.3.5

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

use tempfile::tempdir;

use super::support::*;

fn fake_codex_binary() -> PathBuf {
    if let Some(path) = std::env::var_os("CARGO_BIN_EXE_fake_codex_stream_exec_scenarios_agent_api")
    {
        return PathBuf::from(path);
    }

    let current_exe = std::env::current_exe().expect("resolve current test binary path");
    let target_dir = current_exe
        .parent()
        .and_then(|dir| dir.parent())
        .expect("resolve target dir from current test binary");
    let mut binary = target_dir.join("fake_codex_stream_exec_scenarios_agent_api");
    if cfg!(windows) {
        binary.set_extension("exe");
    }
    binary
}

fn base_env() -> BTreeMap<String, String> {
    [
        (
            "FAKE_CODEX_EXPECT_SANDBOX".to_string(),
            "workspace-write".to_string(),
        ),
        (
            "FAKE_CODEX_EXPECT_APPROVAL".to_string(),
            "never".to_string(),
        ),
    ]
    .into_iter()
    .collect()
}

fn add_dir_expectations(dirs: &[PathBuf]) -> BTreeMap<String, String> {
    let mut env = BTreeMap::from([(
        "FAKE_CODEX_EXPECT_ADD_DIR_COUNT".to_string(),
        dirs.len().to_string(),
    )]);
    for (index, dir) in dirs.iter().enumerate() {
        env.insert(
            format!("FAKE_CODEX_EXPECT_ADD_DIR_{index}"),
            dir.display().to_string(),
        );
    }
    env
}

async fn spawn_and_drain(
    model_id: Option<String>,
    config_model: Option<String>,
    resume: Option<SessionSelectorV1>,
    add_dirs: Vec<PathBuf>,
    env: BTreeMap<String, String>,
    run_start_cwd: PathBuf,
    prompt: &str,
) {
    let config = CodexBackendConfig {
        binary: Some(fake_codex_binary()),
        model: config_model,
        ..Default::default()
    };

    let adapter = test_adapter_with_config_and_run_start_cwd(config, Some(run_start_cwd));

    let spawned = adapter
        .spawn(crate::backend_harness::NormalizedRequest {
            agent_kind: adapter.kind(),
            prompt: prompt.to_string(),
            model_id,
            working_dir: Some(PathBuf::from("repo")),
            effective_timeout: None,
            env,
            policy: CodexExecPolicy {
                add_dirs,
                non_interactive: true,
                external_sandbox: false,
                approval_policy: None,
                sandbox_mode: CodexSandboxMode::WorkspaceWrite,
                resume,
                fork: None,
            },
        })
        .await
        .expect("spawn succeeds");

    let backend_events: Vec<_> = spawned
        .events
        .map(|result| result.expect("backend event stream is infallible for fake codex"))
        .collect()
        .await;

    let completion = tokio::time::timeout(Duration::from_secs(2), spawned.completion)
        .await
        .expect("completion resolves")
        .expect("run completes successfully");
    assert!(
        completion.status.success(),
        "events: {backend_events:?}, completion: {completion:?}"
    );
}

#[tokio::test]
async fn codex_exec_model_id_beats_backend_config_model_and_orders_before_add_dir() {
    let temp = tempdir().expect("tempdir");
    let run_start_cwd = temp.path().join("run-start");
    let expected_cwd = run_start_cwd.join("repo");
    let expected_add_dir = expected_cwd.join("docs");
    std::fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = base_env()
        .into_iter()
        .chain(add_dir_expectations(std::slice::from_ref(
            &expected_add_dir,
        )))
        .chain([
            (
                "FAKE_CODEX_EXPECT_CWD".to_string(),
                expected_cwd.display().to_string(),
            ),
            (
                "FAKE_CODEX_EXPECT_MODEL".to_string(),
                "request-model".to_string(),
            ),
        ])
        .collect();

    spawn_and_drain(
        Some("request-model".to_string()),
        Some("config-model".to_string()),
        None,
        vec![expected_add_dir],
        env,
        run_start_cwd,
        "hello",
    )
    .await;
}

#[tokio::test]
async fn codex_exec_without_model_id_emits_no_model_flag_when_config_model_absent() {
    let temp = tempdir().expect("tempdir");
    let run_start_cwd = temp.path().join("run-start");
    let expected_cwd = run_start_cwd.join("repo");
    let expected_add_dir = expected_cwd.join("docs");
    std::fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = base_env()
        .into_iter()
        .chain(add_dir_expectations(std::slice::from_ref(
            &expected_add_dir,
        )))
        .chain([
            (
                "FAKE_CODEX_EXPECT_CWD".to_string(),
                expected_cwd.display().to_string(),
            ),
            (
                "FAKE_CODEX_EXPECT_MODEL".to_string(),
                "<absent>".to_string(),
            ),
        ])
        .collect();

    spawn_and_drain(
        None,
        None,
        None,
        vec![expected_add_dir],
        env,
        run_start_cwd,
        "hello",
    )
    .await;
}

#[tokio::test]
async fn codex_exec_without_model_id_uses_backend_config_model() {
    let temp = tempdir().expect("tempdir");
    let run_start_cwd = temp.path().join("run-start");
    let expected_cwd = run_start_cwd.join("repo");
    let expected_add_dir = expected_cwd.join("docs");
    std::fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = base_env()
        .into_iter()
        .chain(add_dir_expectations(std::slice::from_ref(
            &expected_add_dir,
        )))
        .chain([
            (
                "FAKE_CODEX_EXPECT_CWD".to_string(),
                expected_cwd.display().to_string(),
            ),
            (
                "FAKE_CODEX_EXPECT_MODEL".to_string(),
                "config-model".to_string(),
            ),
        ])
        .collect();

    spawn_and_drain(
        None,
        Some("config-model".to_string()),
        None,
        vec![expected_add_dir],
        env,
        run_start_cwd,
        "hello",
    )
    .await;
}

#[tokio::test]
async fn codex_resume_emits_model_id_and_orders_before_add_dir() {
    let temp = tempdir().expect("tempdir");
    let run_start_cwd = temp.path().join("run-start");
    let expected_cwd = run_start_cwd.join("repo");
    let expected_add_dir = expected_cwd.join("docs");
    std::fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = base_env()
        .into_iter()
        .chain(add_dir_expectations(std::slice::from_ref(
            &expected_add_dir,
        )))
        .chain([
            (
                "FAKE_CODEX_EXPECT_CWD".to_string(),
                expected_cwd.display().to_string(),
            ),
            (
                "FAKE_CODEX_SCENARIO".to_string(),
                "resume_last_assert".to_string(),
            ),
            (
                "FAKE_CODEX_EXPECT_PROMPT".to_string(),
                "resume me".to_string(),
            ),
            (
                "FAKE_CODEX_EXPECT_MODEL".to_string(),
                "request-model".to_string(),
            ),
        ])
        .collect();

    spawn_and_drain(
        Some("request-model".to_string()),
        None,
        Some(SessionSelectorV1::Last),
        vec![expected_add_dir],
        env,
        run_start_cwd,
        "resume me",
    )
    .await;
}