unified-agent-api 0.3.5

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

use futures_util::StreamExt;
use serde_json::json;

use crate::{backends::gemini_cli::GeminiCliBackendConfig, AgentWrapperBackend, AgentWrapperError};

use super::support::{backend_with_config, backend_with_env, backend_with_timeout, request};

#[tokio::test]
async fn gemini_backend_validation_uses_fake_binary_timeout_path_without_live_provider_state() {
    let mut env = BTreeMap::new();
    env.insert(
        "FAKE_GEMINI_SCENARIO".to_string(),
        "slow_until_killed".to_string(),
    );

    let backend = backend_with_timeout(env, Duration::from_secs(1));
    let handle = backend
        .run(request("Reply with OK.", None))
        .await
        .expect("run should start");

    let mut events = handle.events;
    assert!(
        tokio::time::timeout(Duration::from_secs(2), events.next())
            .await
            .expect("first event should arrive before the harness timeout path completes")
            .is_some(),
        "fake-binary validation path should still surface an initial event"
    );
    while events.next().await.is_some() {}

    let err = handle
        .completion
        .await
        .expect_err("timeout validation path should surface a backend error");
    match err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(message, "gemini backend error: timeout");
        }
        other => panic!("expected Backend timeout error, got {other:?}"),
    }
}

#[tokio::test]
async fn gemini_backend_missing_binary_surfaces_safe_spawn_error() {
    let backend = backend_with_config(GeminiCliBackendConfig {
        binary: Some(PathBuf::from("/definitely/not/a/real/gemini-binary")),
        default_timeout: None,
        env: BTreeMap::new(),
    });

    let handle = backend
        .run(request("Reply with OK.", None))
        .await
        .expect("run handle should still be returned before startup failure surfaces");

    let mut events = handle.events;
    let first = events.next().await.expect("error event should surface");
    assert_eq!(first.kind, crate::AgentWrapperEventKind::Error);
    assert_eq!(
        first.message.as_deref(),
        Some("gemini backend error: binary not found")
    );
    assert!(
        events.next().await.is_none(),
        "startup failure should close the stream"
    );

    let err = handle
        .completion
        .await
        .expect_err("startup failure should resolve completion as backend error");
    match err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(message, "gemini backend error: binary not found");
        }
        other => panic!("expected Backend spawn error, got {other:?}"),
    }
}

#[tokio::test]
async fn gemini_backend_invalid_input_exit_code_is_translated_conservatively() {
    let mut env = BTreeMap::new();
    env.insert(
        "FAKE_GEMINI_SCENARIO".to_string(),
        "invalid_input".to_string(),
    );

    let backend = backend_with_env(env);
    let handle = backend
        .run(request("Reply with OK.", None))
        .await
        .expect("run should start");

    let mut events = handle.events;
    while events.next().await.is_some() {}

    let err = handle
        .completion
        .await
        .expect_err("invalid input should surface a backend error");
    match err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(message, "gemini backend error: invalid input");
        }
        other => panic!("expected Backend invalid input error, got {other:?}"),
    }
}

#[tokio::test]
async fn gemini_backend_keeps_unknown_backend_namespace_fail_closed() {
    let backend = backend_with_env(Default::default());
    let mut run_request = request("Reply with OK.", None);
    run_request
        .extensions
        .insert("backend.gemini_cli.future_key".to_string(), json!(true));

    let err = backend
        .run(run_request)
        .await
        .expect_err("unsupported backend namespace key must fail closed before spawn");
    match err {
        AgentWrapperError::UnsupportedCapability {
            agent_kind,
            capability,
        } => {
            assert_eq!(agent_kind, "gemini_cli");
            assert_eq!(capability, "backend.gemini_cli.future_key");
        }
        other => panic!("expected UnsupportedCapability, got {other:?}"),
    }
}