unified-agent-api 0.3.5

Agent-agnostic facade and registry for wrapper backends
Documentation
use std::fs;

use tempfile::tempdir;

use super::support::*;

#[test]
fn claude_backend_advertises_agent_api_config_model_v1() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    assert!(backend
        .capabilities()
        .contains(crate::EXT_AGENT_API_CONFIG_MODEL_V1));
}

#[test]
fn claude_backend_reports_required_capabilities() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    let capabilities = backend.capabilities();
    assert!(capabilities.contains("agent_api.run"));
    assert!(capabilities.contains("agent_api.events"));
    assert!(capabilities.contains("agent_api.events.live"));
    assert!(capabilities.contains(crate::CAPABILITY_CONTROL_CANCEL_V1));
    assert!(capabilities.contains(CAP_TOOLS_STRUCTURED_V1));
    assert!(capabilities.contains(CAP_TOOLS_RESULTS_V1));
    assert!(capabilities.contains(CAP_ARTIFACTS_FINAL_TEXT_V1));
    assert!(capabilities.contains(CAP_SESSION_HANDLE_V1));
    assert!(capabilities.contains(crate::EXT_AGENT_API_CONFIG_MODEL_V1));
    assert!(capabilities.contains(EXT_ADD_DIRS_V1));
    assert!(capabilities.contains(EXT_SESSION_RESUME_V1));
    assert!(capabilities.contains(EXT_SESSION_FORK_V1));
}

#[test]
fn claude_add_dirs_capability_and_supported_key_surfaces_stay_aligned() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    assert!(backend.capabilities().contains(EXT_ADD_DIRS_V1));

    let default_adapter = new_adapter();
    assert!(default_adapter
        .supported_extension_keys()
        .contains(&EXT_ADD_DIRS_V1));

    let external_sandbox_adapter = new_adapter_with_config(ClaudeCodeBackendConfig {
        allow_external_sandbox_exec: true,
        ..Default::default()
    });
    assert!(external_sandbox_adapter
        .supported_extension_keys()
        .contains(&EXT_ADD_DIRS_V1));
}

#[test]
fn claude_add_dirs_normalize_request_accepts_supported_key_and_extracts_policy() {
    let temp = tempdir().expect("tempdir");
    let working_dir = temp.path().join("workspace");
    let child_dir = working_dir.join("child");
    fs::create_dir_all(&child_dir).expect("create child directory");

    let adapter = new_adapter();
    let defaults = crate::backend_harness::BackendDefaults::default();
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        working_dir: Some(working_dir),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["child"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let normalized = crate::backend_harness::normalize_request(&adapter, &defaults, request)
        .expect("add_dirs should pass R0 gating and policy extraction");

    assert_eq!(normalized.policy.add_dirs, vec![child_dir]);
}

#[test]
fn claude_backend_mcp_write_capabilities_are_disabled_by_default() {
    assert!(!ClaudeCodeBackendConfig::default().allow_mcp_write);

    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    let capabilities = backend.capabilities();
    assert_eq!(
        capabilities.contains(CAPABILITY_MCP_LIST_V1),
        claude_mcp_list_supported_on_target()
    );
    assert_eq!(
        capabilities.contains(CAPABILITY_MCP_GET_V1),
        claude_mcp_get_supported_on_target()
    );
    assert!(!capabilities.contains(CAPABILITY_MCP_ADD_V1));
    assert!(!capabilities.contains(CAPABILITY_MCP_REMOVE_V1));
}

#[test]
fn claude_backend_mcp_write_capabilities_require_opt_in_and_target_support() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig {
        allow_mcp_write: true,
        ..Default::default()
    });
    let capabilities = backend.capabilities();
    assert_eq!(
        capabilities.contains(CAPABILITY_MCP_LIST_V1),
        claude_mcp_list_supported_on_target()
    );
    assert_eq!(
        capabilities.contains(CAPABILITY_MCP_GET_V1),
        claude_mcp_get_supported_on_target()
    );
    assert_eq!(
        capabilities.contains(CAPABILITY_MCP_ADD_V1),
        claude_mcp_get_supported_on_target()
    );
    assert_eq!(
        capabilities.contains(CAPABILITY_MCP_REMOVE_V1),
        claude_mcp_get_supported_on_target()
    );
}

#[tokio::test]
async fn claude_backend_mcp_list_fails_closed_when_read_capability_is_unavailable() {
    if claude_mcp_list_supported_on_target() {
        return;
    }

    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    let err = backend
        .mcp_list(crate::mcp::AgentWrapperMcpListRequest::default())
        .await
        .expect_err("unsupported target should fail closed");

    match err {
        AgentWrapperError::UnsupportedCapability {
            agent_kind,
            capability,
        } => {
            assert_eq!(agent_kind, "claude_code");
            assert_eq!(capability, CAPABILITY_MCP_LIST_V1);
        }
        other => panic!("expected UnsupportedCapability, got: {other:?}"),
    }
}

#[tokio::test]
async fn claude_backend_mcp_get_fails_closed_when_read_capability_is_unavailable() {
    if claude_mcp_get_supported_on_target() {
        return;
    }

    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    let err = backend
        .mcp_get(crate::mcp::AgentWrapperMcpGetRequest {
            name: "demo".to_string(),
            context: Default::default(),
        })
        .await
        .expect_err("unsupported target should fail closed");

    match err {
        AgentWrapperError::UnsupportedCapability {
            agent_kind,
            capability,
        } => {
            assert_eq!(agent_kind, "claude_code");
            assert_eq!(capability, CAPABILITY_MCP_GET_V1);
        }
        other => panic!("expected UnsupportedCapability, got: {other:?}"),
    }
}

#[tokio::test]
async fn claude_backend_mcp_add_fails_closed_when_write_capability_is_disabled() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    let err = backend
        .mcp_add(sample_mcp_add_request())
        .await
        .expect_err("write support should stay disabled by default");

    match err {
        AgentWrapperError::UnsupportedCapability {
            agent_kind,
            capability,
        } => {
            assert_eq!(agent_kind, "claude_code");
            assert_eq!(capability, CAPABILITY_MCP_ADD_V1);
        }
        other => panic!("expected UnsupportedCapability, got: {other:?}"),
    }
}

#[tokio::test]
async fn claude_backend_mcp_remove_fails_closed_when_write_capability_is_disabled() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    let err = backend
        .mcp_remove(sample_mcp_remove_request())
        .await
        .expect_err("write support should stay disabled by default");

    match err {
        AgentWrapperError::UnsupportedCapability {
            agent_kind,
            capability,
        } => {
            assert_eq!(agent_kind, "claude_code");
            assert_eq!(capability, CAPABILITY_MCP_REMOVE_V1);
        }
        other => panic!("expected UnsupportedCapability, got: {other:?}"),
    }
}

#[test]
fn claude_backend_registers_under_claude_code_kind_id() {
    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig::default());
    assert_eq!(backend.kind().as_str(), "claude_code");
}