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::*;

const BACKPRESSURE_ASSERT_TIMEOUT: Duration = Duration::from_millis(200);
const COMPLETION_TIMEOUT: Duration = Duration::from_secs(5);

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
}

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

    let adapter = test_adapter_with_config_and_run_start_cwd(
        CodexBackendConfig {
            binary: Some(fake_codex_binary()),
            ..Default::default()
        },
        Some(run_start_cwd),
    );

    let env = BTreeMap::from([
        (
            "FAKE_CODEX_EXPECT_SANDBOX".to_string(),
            "workspace-write".to_string(),
        ),
        (
            "FAKE_CODEX_EXPECT_APPROVAL".to_string(),
            "never".to_string(),
        ),
        (
            "FAKE_CODEX_EXPECT_CWD".to_string(),
            expected_cwd.display().to_string(),
        ),
        (
            "FAKE_CODEX_SCENARIO".to_string(),
            "model_runtime_rejection_after_buffered_events".to_string(),
        ),
        (
            "FAKE_CODEX_EXPECT_MODEL".to_string(),
            "gpt-5-codex".to_string(),
        ),
        (
            "FAKE_CODEX_MODEL_RUNTIME_REJECTION_SECRET".to_string(),
            "MODEL_RUNTIME_REJECTION_SECRET_DO_NOT_LEAK".to_string(),
        ),
        (
            "FAKE_CODEX_BUFFERED_EVENT_COUNT".to_string(),
            "1024".to_string(),
        ),
        (
            "FAKE_CODEX_BUFFERED_EVENT_PADDING_BYTES".to_string(),
            "1024".to_string(),
        ),
        (
            "FAKE_CODEX_RUNTIME_REJECTION_EXIT_CODE".to_string(),
            "0".to_string(),
        ),
    ]);

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

    let mut completion = spawned.completion;
    assert!(
        tokio::time::timeout(BACKPRESSURE_ASSERT_TIMEOUT, &mut completion)
            .await
            .is_err(),
        "completion should remain pending while the buffered backend stream is not drained"
    );

    let backend_events: Vec<_> = spawned
        .events
        .map(|result| result.expect("backend event stream is infallible for fake codex"))
        .collect()
        .await;
    assert!(
        backend_events.len() > crate::backend_harness::DEFAULT_EVENT_CHANNEL_CAPACITY,
        "expected enough buffered events to exercise backpressure"
    );

    let completion = tokio::time::timeout(COMPLETION_TIMEOUT, completion)
        .await
        .expect("completion resolves after the event stream is drained")
        .expect("completion is Ok for fake codex");
    let err = adapter
        .map_completion(completion)
        .expect_err("buffered model rejection must map to Backend error");
    match err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(
                message,
                "codex backend error: model rejected by runtime (details redacted)"
            );
        }
        other => panic!("expected Backend error, got: {other:?}"),
    }
}