unified-agent-api 0.3.5

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

use tempfile::tempdir;

use super::support::*;

fn add_dirs_json(dirs: &[PathBuf]) -> JsonValue {
    add_dirs_payload(
        &dirs
            .iter()
            .map(|dir| dir.display().to_string())
            .collect::<Vec<_>>(),
    )
}

async fn run_claude_assertion(
    prompt: &str,
    scenario: &str,
    mut env: BTreeMap<String, String>,
    extensions: BTreeMap<String, JsonValue>,
) {
    let _env_lock = test_env_lock();
    env.insert("FAKE_CLAUDE_SCENARIO".to_string(), scenario.to_string());
    env.insert("FAKE_CLAUDE_EXPECT_PROMPT".to_string(), prompt.to_string());

    let backend = ClaudeCodeBackend::new(ClaudeCodeBackendConfig {
        binary: Some(fake_claude_binary()),
        env,
        ..Default::default()
    });

    let handle = backend
        .run(AgentWrapperRunRequest {
            prompt: prompt.to_string(),
            extensions,
            ..Default::default()
        })
        .await
        .expect("Claude run should start");

    let mut events = handle.events;
    let _seen = drain_to_none(events.as_mut(), Duration::from_secs(2)).await;

    let completion = tokio::time::timeout(Duration::from_secs(2), handle.completion)
        .await
        .expect("completion resolves")
        .expect("run completes successfully");
    assert!(
        completion.status.success(),
        "expected successful fake Claude run, events: {:?}",
        _seen
    );
}

async fn run_claude_assertion_with_adapter(
    prompt: &str,
    scenario: &str,
    mut config: ClaudeCodeBackendConfig,
    run_start_cwd: Option<PathBuf>,
    request_working_dir: Option<PathBuf>,
    extensions: BTreeMap<String, JsonValue>,
) {
    let _env_lock = test_env_lock();
    config
        .env
        .insert("FAKE_CLAUDE_SCENARIO".to_string(), scenario.to_string());
    config
        .env
        .insert("FAKE_CLAUDE_EXPECT_PROMPT".to_string(), prompt.to_string());
    let adapter = Arc::new(new_adapter_with_config_and_run_start_cwd(
        config.clone(),
        run_start_cwd,
    ));
    let defaults = crate::backend_harness::BackendDefaults {
        env: config.env.clone(),
        default_timeout: config.default_timeout,
    };
    let handle = crate::backend_harness::run_harnessed_backend(
        adapter,
        defaults,
        AgentWrapperRunRequest {
            prompt: prompt.to_string(),
            working_dir: request_working_dir,
            extensions,
            ..Default::default()
        },
    )
    .await
    .expect("Claude run should start");

    let mut events = handle.events;
    let _seen = drain_to_none(events.as_mut(), Duration::from_secs(2)).await;

    let completion = tokio::time::timeout(Duration::from_secs(2), handle.completion)
        .await
        .expect("completion resolves")
        .expect("run completes successfully");
    assert!(
        completion.status.success(),
        "expected successful fake Claude run, events: {:?}",
        _seen
    );
}

#[tokio::test]
async fn fresh_run_emits_one_variadic_add_dir_group_before_verbose() {
    let prompt = "hello world";
    let temp = tempdir().expect("tempdir");
    let dir_a = temp.path().join("alpha");
    let dir_b = temp.path().join("beta");
    fs::create_dir_all(&dir_a).expect("alpha dir");
    fs::create_dir_all(&dir_b).expect("beta dir");
    let add_dirs = vec![dir_a, dir_b];

    let env = expected_add_dirs_env(&add_dirs);
    let extensions = [(
        "agent_api.exec.add_dirs.v1".to_string(),
        add_dirs_json(&add_dirs),
    )]
    .into_iter()
    .collect();

    run_claude_assertion(prompt, "fresh_assert", env, extensions).await;
}

#[tokio::test]
async fn fresh_run_accepts_absolute_add_dirs_without_effective_working_dir() {
    let prompt = "hello world";
    let temp = tempdir().expect("tempdir");
    let absolute_dir = temp.path().join("shared-context");
    fs::create_dir_all(&absolute_dir).expect("absolute dir");

    let env = expected_add_dirs_env(std::slice::from_ref(&absolute_dir));
    let config = ClaudeCodeBackendConfig {
        binary: Some(fake_claude_binary()),
        env,
        ..Default::default()
    };
    let extensions = [(
        "agent_api.exec.add_dirs.v1".to_string(),
        add_dirs_json(std::slice::from_ref(&absolute_dir)),
    )]
    .into_iter()
    .collect();

    run_claude_assertion_with_adapter(prompt, "fresh_assert", config, None, None, extensions).await;
}

#[tokio::test]
async fn fresh_run_omits_add_dir_when_extension_key_is_absent() {
    run_claude_assertion(
        "hello world",
        "fresh_assert",
        expect_no_add_dir_env(),
        BTreeMap::new(),
    )
    .await;
}

#[tokio::test]
async fn resume_last_keeps_add_dir_group_before_continue() {
    let prompt = "hello world";
    let temp = tempdir().expect("tempdir");
    let dir_a = temp.path().join("alpha");
    let dir_b = temp.path().join("beta");
    fs::create_dir_all(&dir_a).expect("alpha dir");
    fs::create_dir_all(&dir_b).expect("beta dir");
    let add_dirs = vec![dir_a, dir_b];

    let env = expected_add_dirs_env(&add_dirs);
    let extensions = [
        (
            "agent_api.exec.add_dirs.v1".to_string(),
            add_dirs_json(&add_dirs),
        ),
        (
            "agent_api.session.resume.v1".to_string(),
            serde_json::json!({"selector": "last"}),
        ),
    ]
    .into_iter()
    .collect();

    run_claude_assertion(prompt, "resume_last_assert", env, extensions).await;
}

#[tokio::test]
async fn resume_id_keeps_add_dir_group_before_resume_flag() {
    let prompt = "hello world";
    let resume_id = "sess-123";
    let temp = tempdir().expect("tempdir");
    let dir_a = temp.path().join("alpha");
    let dir_b = temp.path().join("beta");
    fs::create_dir_all(&dir_a).expect("alpha dir");
    fs::create_dir_all(&dir_b).expect("beta dir");
    let add_dirs = vec![dir_a, dir_b];

    let mut env = expected_add_dirs_env(&add_dirs);
    env.insert(
        "FAKE_CLAUDE_EXPECT_RESUME_ID".to_string(),
        resume_id.to_string(),
    );
    let extensions = [
        (
            "agent_api.exec.add_dirs.v1".to_string(),
            add_dirs_json(&add_dirs),
        ),
        (
            "agent_api.session.resume.v1".to_string(),
            serde_json::json!({"selector": "id", "id": resume_id}),
        ),
    ]
    .into_iter()
    .collect();

    run_claude_assertion(prompt, "resume_id_assert", env, extensions).await;
}

#[tokio::test]
async fn fork_last_keeps_add_dir_group_before_continue_and_fork_session() {
    let prompt = "hello world";
    let temp = tempdir().expect("tempdir");
    let dir_a = temp.path().join("alpha");
    let dir_b = temp.path().join("beta");
    fs::create_dir_all(&dir_a).expect("alpha dir");
    fs::create_dir_all(&dir_b).expect("beta dir");
    let add_dirs = vec![dir_a, dir_b];

    let env = expected_add_dirs_env(&add_dirs);
    let extensions = [
        (
            "agent_api.exec.add_dirs.v1".to_string(),
            add_dirs_json(&add_dirs),
        ),
        (
            "agent_api.session.fork.v1".to_string(),
            serde_json::json!({"selector": "last"}),
        ),
    ]
    .into_iter()
    .collect();

    run_claude_assertion(prompt, "fork_last_assert", env, extensions).await;
}

#[tokio::test]
async fn fork_id_keeps_add_dir_group_before_fork_session_and_resume() {
    let prompt = "hello world";
    let fork_id = "sess-123";
    let temp = tempdir().expect("tempdir");
    let dir_a = temp.path().join("alpha");
    let dir_b = temp.path().join("beta");
    fs::create_dir_all(&dir_a).expect("alpha dir");
    fs::create_dir_all(&dir_b).expect("beta dir");
    let add_dirs = vec![dir_a, dir_b];

    let mut env = expected_add_dirs_env(&add_dirs);
    env.insert(
        "FAKE_CLAUDE_EXPECT_RESUME_ID".to_string(),
        fork_id.to_string(),
    );
    let extensions = [
        (
            "agent_api.exec.add_dirs.v1".to_string(),
            add_dirs_json(&add_dirs),
        ),
        (
            "agent_api.session.fork.v1".to_string(),
            serde_json::json!({"selector": "id", "id": fork_id}),
        ),
    ]
    .into_iter()
    .collect();

    run_claude_assertion(prompt, "fork_id_assert", env, extensions).await;
}

#[tokio::test]
async fn fresh_run_resolves_relative_request_working_dir_before_add_dirs_and_spawn() {
    let prompt = "hello world";
    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");
    fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = expected_add_dirs_env(std::slice::from_ref(&expected_add_dir))
        .into_iter()
        .chain([(
            "FAKE_CLAUDE_EXPECT_CWD".to_string(),
            expected_cwd.display().to_string(),
        )])
        .collect();
    let config = ClaudeCodeBackendConfig {
        binary: Some(fake_claude_binary()),
        env,
        ..Default::default()
    };
    let extensions = [(
        "agent_api.exec.add_dirs.v1".to_string(),
        add_dirs_payload(&["docs"]),
    )]
    .into_iter()
    .collect();

    run_claude_assertion_with_adapter(
        prompt,
        "fresh_assert",
        config,
        Some(run_start_cwd),
        Some(PathBuf::from("repo")),
        extensions,
    )
    .await;
}

#[tokio::test]
async fn fresh_run_resolves_relative_default_working_dir_before_add_dirs_and_spawn() {
    let prompt = "hello world";
    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");
    fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = expected_add_dirs_env(std::slice::from_ref(&expected_add_dir))
        .into_iter()
        .chain([(
            "FAKE_CLAUDE_EXPECT_CWD".to_string(),
            expected_cwd.display().to_string(),
        )])
        .collect();
    let config = ClaudeCodeBackendConfig {
        binary: Some(fake_claude_binary()),
        default_working_dir: Some(PathBuf::from("repo")),
        env,
        ..Default::default()
    };
    let extensions = [(
        "agent_api.exec.add_dirs.v1".to_string(),
        add_dirs_payload(&["docs"]),
    )]
    .into_iter()
    .collect();

    run_claude_assertion_with_adapter(
        prompt,
        "fresh_assert",
        config,
        Some(run_start_cwd),
        None,
        extensions,
    )
    .await;
}

#[cfg(windows)]
#[tokio::test]
async fn fresh_run_deduplicates_case_insensitive_add_dirs_before_spawn() {
    let prompt = "hello world";
    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");
    fs::create_dir_all(&expected_add_dir).expect("create add-dir target");

    let env = expected_add_dirs_env(std::slice::from_ref(&expected_add_dir))
        .into_iter()
        .chain([(
            "FAKE_CLAUDE_EXPECT_CWD".to_string(),
            expected_cwd.display().to_string(),
        )])
        .collect();
    let config = ClaudeCodeBackendConfig {
        binary: Some(fake_claude_binary()),
        env,
        ..Default::default()
    };
    let extensions = [(
        "agent_api.exec.add_dirs.v1".to_string(),
        add_dirs_payload(&["docs", "DOCS"]),
    )]
    .into_iter()
    .collect();

    run_claude_assertion_with_adapter(
        prompt,
        "fresh_assert",
        config,
        Some(run_start_cwd),
        Some(PathBuf::from("repo")),
        extensions,
    )
    .await;
}