unified-agent-api 0.2.3

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

use tempfile::tempdir;

use super::support::*;

#[cfg(windows)]
use std::path::{Component, Path, Prefix};

const EXT_ADD_DIRS_V1: &str = "agent_api.exec.add_dirs.v1";

#[test]
fn claude_policy_add_dirs_absent_key_returns_empty_vec() {
    let adapter = new_adapter();
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        ..Default::default()
    };

    let policy = adapter
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

    assert!(policy.add_dirs.is_empty());
}

#[test]
fn claude_policy_add_dirs_supported_extension_allowlist_admits_key() {
    let adapter = new_adapter();
    assert!(adapter
        .supported_extension_keys()
        .contains(&EXT_ADD_DIRS_V1));

    let temp = tempdir().expect("tempdir");
    let request_root = temp.path().join("request-root");
    let request_docs = request_root.join("docs");
    fs::create_dir_all(&request_docs).expect("create request docs");

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

    crate::backend_harness::normalize_request(&adapter, &defaults, request)
        .expect("add-dir extension key should pass Claude allowlist gating");
}

#[test]
fn claude_policy_add_dirs_request_working_dir_beats_default_and_run_start_cwd() {
    let temp = tempdir().expect("tempdir");
    let request_root = temp.path().join("request-root");
    let default_root = temp.path().join("default-root");
    let run_start_root = temp.path().join("run-start-root");
    let request_docs = request_root.join("docs");
    let default_docs = default_root.join("docs");
    let run_start_docs = run_start_root.join("docs");
    fs::create_dir_all(&request_docs).expect("create request docs");
    fs::create_dir_all(&default_docs).expect("create default docs");
    fs::create_dir_all(&run_start_docs).expect("create run-start docs");

    let adapter = new_adapter_with_config_and_run_start_cwd(
        ClaudeCodeBackendConfig {
            default_working_dir: Some(default_root),
            ..Default::default()
        },
        Some(run_start_root),
    );
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        working_dir: Some(request_root),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let policy = adapter
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

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

#[test]
fn claude_policy_add_dirs_backend_default_beats_run_start_cwd() {
    let temp = tempdir().expect("tempdir");
    let default_root = temp.path().join("default-root");
    let run_start_root = temp.path().join("run-start-root");
    let default_docs = default_root.join("docs");
    let run_start_docs = run_start_root.join("docs");
    fs::create_dir_all(&default_docs).expect("create default docs");
    fs::create_dir_all(&run_start_docs).expect("create run-start docs");

    let adapter = new_adapter_with_config_and_run_start_cwd(
        ClaudeCodeBackendConfig {
            default_working_dir: Some(default_root),
            ..Default::default()
        },
        Some(run_start_root),
    );
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let policy = adapter
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

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

#[test]
fn claude_policy_add_dirs_run_start_cwd_is_final_fallback() {
    let temp = tempdir().expect("tempdir");
    let run_start_root = temp.path().join("run-start-root");
    let run_start_docs = run_start_root.join("docs");
    fs::create_dir_all(&run_start_docs).expect("create run-start docs");

    let adapter = new_adapter_with_run_start_cwd(Some(run_start_root));
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let policy = adapter
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

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

#[test]
fn claude_policy_add_dirs_accepts_absolute_entries_without_effective_working_dir() {
    let temp = tempdir().expect("tempdir");
    let absolute_docs = temp.path().join("shared-context");
    fs::create_dir_all(&absolute_docs).expect("create absolute add-dir");
    let absolute_docs_text = absolute_docs.to_string_lossy().into_owned();
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        extensions: [(
            EXT_ADD_DIRS_V1.to_string(),
            add_dirs_payload(&[absolute_docs_text.as_str()]),
        )]
        .into_iter()
        .collect(),
        ..Default::default()
    };

    let policy = new_adapter()
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

    assert_eq!(policy.add_dirs, vec![absolute_docs]);
    assert_eq!(policy.resolved_working_dir, None);
}

#[test]
fn claude_policy_add_dirs_relative_entries_without_effective_working_dir_fail_safely() {
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let err = adapter_error(new_adapter().validate_and_extract_policy(&request));
    match &err {
        AgentWrapperError::InvalidRequest { message } => {
            assert_eq!(message, "invalid agent_api.exec.add_dirs.v1.dirs[0]");
        }
        other => panic!("expected InvalidRequest, got: {other:?}"),
    }
}

#[test]
fn claude_policy_add_dirs_invalid_input_uses_safe_message_without_leakage() {
    let temp = tempdir().expect("tempdir");
    let request_root = temp.path().join("request-root");
    fs::create_dir_all(&request_root).expect("create request root");
    let leaked = "missing-secret-dir";
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        working_dir: Some(request_root),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&[leaked]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let err = adapter_error(new_adapter().validate_and_extract_policy(&request));
    match &err {
        AgentWrapperError::InvalidRequest { message } => {
            assert_eq!(message, "invalid agent_api.exec.add_dirs.v1.dirs[0]");
        }
        other => panic!("expected InvalidRequest, got: {other:?}"),
    }
    assert!(
        !err.to_string().contains(leaked),
        "error display leaked raw path text"
    );
}

#[test]
fn claude_policy_add_dirs_resolves_relative_request_working_dir_from_run_start_cwd() {
    let temp = tempdir().expect("tempdir");
    let run_start_root = temp.path().join("run-start");
    let request_docs = run_start_root.join("repo").join("docs");
    fs::create_dir_all(&request_docs).expect("create request docs");

    let adapter = new_adapter_with_run_start_cwd(Some(run_start_root));
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        working_dir: Some(std::path::PathBuf::from("repo")),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let policy = adapter
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

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

#[test]
fn claude_policy_add_dirs_resolves_relative_default_working_dir_from_run_start_cwd() {
    let temp = tempdir().expect("tempdir");
    let run_start_root = temp.path().join("run-start");
    let default_docs = run_start_root.join("repo").join("docs");
    fs::create_dir_all(&default_docs).expect("create default docs");

    let adapter = new_adapter_with_config_and_run_start_cwd(
        ClaudeCodeBackendConfig {
            default_working_dir: Some(std::path::PathBuf::from("repo")),
            ..Default::default()
        },
        Some(run_start_root),
    );
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let policy = adapter
        .validate_and_extract_policy(&request)
        .expect("policy extraction should succeed");

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

#[test]
fn claude_policy_relative_working_dir_without_run_start_cwd_fails_safely() {
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        working_dir: Some(std::path::PathBuf::from("repo")),
        ..Default::default()
    };

    let err = adapter_error(new_adapter().validate_and_extract_policy(&request));
    match &err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(
                message,
                super::super::util::PINNED_WORKING_DIR_RESOLUTION_FAILURE
            );
        }
        other => panic!("expected Backend, got: {other:?}"),
    }
}

#[cfg(windows)]
#[test]
fn claude_policy_cross_drive_drive_relative_working_dir_rejects_before_add_dir_resolution() {
    let temp = tempdir().expect("tempdir");
    let run_start_root = temp.path().join("run-start");
    let adapter = new_adapter_with_run_start_cwd(Some(run_start_root.clone()));
    let request = AgentWrapperRunRequest {
        prompt: "hello".to_string(),
        working_dir: Some(windows_drive_relative_on_other_drive(
            "repo",
            &run_start_root,
        )),
        extensions: [(EXT_ADD_DIRS_V1.to_string(), add_dirs_payload(&["docs"]))]
            .into_iter()
            .collect(),
        ..Default::default()
    };

    let err = adapter_error(adapter.validate_and_extract_policy(&request));
    match &err {
        AgentWrapperError::Backend { message } => {
            assert_eq!(
                message,
                super::super::util::PINNED_WORKING_DIR_RESOLUTION_FAILURE
            );
        }
        other => panic!("expected Backend, got: {other:?}"),
    }
}

fn adapter_error(
    result: Result<super::super::harness::ClaudeExecPolicy, AgentWrapperError>,
) -> AgentWrapperError {
    result.expect_err("policy extraction should fail")
}

#[cfg(windows)]
fn windows_drive_relative_on_other_drive(
    relative: &str,
    absolute_path: &Path,
) -> std::path::PathBuf {
    let current_drive = absolute_path
        .components()
        .find_map(|component| match component {
            Component::Prefix(value) => match value.kind() {
                Prefix::Disk(drive) | Prefix::VerbatimDisk(drive) => {
                    Some(drive.to_ascii_lowercase())
                }
                _ => None,
            },
            _ => None,
        })
        .expect("absolute windows path should include a disk prefix");
    let alternate_drive = if current_drive == b'c' { 'd' } else { 'c' };
    std::path::PathBuf::from(format!("{alternate_drive}:{relative}"))
}