Add ACP agent backend and terminal controls
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn codex_preset_is_version_pinned() {
|
||||
let launch = AcpAgentPreset::Codex.launch_config();
|
||||
|
||||
assert_eq!(launch.command, PathBuf::from("npx"));
|
||||
assert_eq!(
|
||||
launch.args,
|
||||
vec![
|
||||
"--yes",
|
||||
&format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
launch.env.get("DEFAULT_AUTH_REQUEST").map(String::as_str),
|
||||
Some(r#"{"methodId":"chat-gpt"}"#)
|
||||
);
|
||||
assert_eq!(
|
||||
launch.env.get("INITIAL_AGENT_MODE").map(String::as_str),
|
||||
Some("read-only")
|
||||
);
|
||||
assert_eq!(
|
||||
launch
|
||||
.preferred_auth_method
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
Some("chat-gpt".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_preset_is_version_pinned() {
|
||||
let launch = AcpAgentPreset::OpenCode.launch_config();
|
||||
|
||||
assert_eq!(launch.command, PathBuf::from("npx"));
|
||||
assert_eq!(
|
||||
launch.args,
|
||||
vec![
|
||||
"--yes",
|
||||
&format!("opencode-ai@{OPENCODE_NPM_VERSION}"),
|
||||
"acp"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_opencode_prefers_the_native_executable() {
|
||||
let launch = AcpAgentPreset::OpenCode
|
||||
.resolve_launch_config_with(|command| match command {
|
||||
"opencode" => Some(PathBuf::from("/opt/bin/opencode")),
|
||||
"npx" => Some(PathBuf::from("/opt/bin/npx")),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(launch.command, PathBuf::from("/opt/bin/opencode"));
|
||||
assert_eq!(launch.args, vec!["acp"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_codex_falls_back_to_bun_compatibility_mode() {
|
||||
let resolve = |command: &str| (command == "bunx").then(|| PathBuf::from("/opt/bin/bunx"));
|
||||
let codex = AcpAgentPreset::Codex
|
||||
.resolve_launch_config_with(resolve)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(codex.command, PathBuf::from("/opt/bin/bunx"));
|
||||
assert_eq!(
|
||||
codex.args,
|
||||
vec![
|
||||
"--bun".to_owned(),
|
||||
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
codex.env.get("INITIAL_AGENT_MODE").map(String::as_str),
|
||||
Some("read-only")
|
||||
);
|
||||
assert_eq!(
|
||||
codex
|
||||
.preferred_auth_method
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
Some("chat-gpt".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_presets_explain_missing_launchers() {
|
||||
let error = AcpAgentPreset::Codex
|
||||
.resolve_launch_config_with(|_| None)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains("requires npx or bunx"));
|
||||
|
||||
let opencode_error = AcpAgentPreset::OpenCode
|
||||
.resolve_launch_config_with(|command| {
|
||||
(command == "bunx").then(|| PathBuf::from("/opt/bin/bunx"))
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(opencode_error.contains("requires the opencode executable or npx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_config_is_a_comparable_fingerprint() {
|
||||
let first = AcpLaunchConfig::new("/usr/bin/npx")
|
||||
.args(["agent", "acp"])
|
||||
.env("TOKEN", "first")
|
||||
.preferred_auth_method("browser");
|
||||
let same = first.clone();
|
||||
let different = first.clone().env("TOKEN", "second");
|
||||
let different_auth = first.clone().preferred_auth_method("api-key");
|
||||
|
||||
assert_eq!(first, same);
|
||||
assert_ne!(first, different);
|
||||
assert_ne!(first, different_auth);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_environment_clears_credentials_and_preserves_runtime_paths() {
|
||||
let parent = [
|
||||
("PATH", "/usr/bin"),
|
||||
("HOME", "/Users/test"),
|
||||
("XDG_CONFIG_HOME", "/Users/test/.config"),
|
||||
("OPENAI_API_KEY", "secret-openai-key"),
|
||||
("AWS_SECRET_ACCESS_KEY", "secret-aws-key"),
|
||||
("HTTPS_PROXY", "https://user:secret@example.com"),
|
||||
("GALAXY_INTERNAL_SECRET", "secret-galaxy-value"),
|
||||
("XDG_AGENT_TOKEN", "secret-xdg-value"),
|
||||
("LC_AGENT_TOKEN", "secret-locale-value"),
|
||||
]
|
||||
.map(|(name, value)| (OsString::from(name), OsString::from(value)));
|
||||
let explicit = BTreeMap::from([
|
||||
("INITIAL_AGENT_MODE".to_owned(), "read-only".to_owned()),
|
||||
(
|
||||
"DEFAULT_AUTH_REQUEST".to_owned(),
|
||||
r#"{"methodId":"chat-gpt"}"#.to_owned(),
|
||||
),
|
||||
]);
|
||||
|
||||
let overrides = sanitized_environment_overrides(parent, &explicit);
|
||||
|
||||
assert!(!overrides.contains_key("PATH"));
|
||||
assert!(!overrides.contains_key("HOME"));
|
||||
assert!(!overrides.contains_key("XDG_CONFIG_HOME"));
|
||||
assert_eq!(
|
||||
overrides.get("OPENAI_API_KEY").map(String::as_str),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.get("AWS_SECRET_ACCESS_KEY").map(String::as_str),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(overrides.get("HTTPS_PROXY").map(String::as_str), Some(""));
|
||||
assert_eq!(
|
||||
overrides.get("GALAXY_INTERNAL_SECRET").map(String::as_str),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.get("XDG_AGENT_TOKEN").map(String::as_str),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.get("LC_AGENT_TOKEN").map(String::as_str),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.get("INITIAL_AGENT_MODE").map(String::as_str),
|
||||
Some("read-only")
|
||||
);
|
||||
assert_eq!(
|
||||
overrides.get("DEFAULT_AUTH_REQUEST").map(String::as_str),
|
||||
Some(r#"{"methodId":"chat-gpt"}"#)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_agent_environment_wins_over_scrubbing() {
|
||||
let parent = [(
|
||||
OsString::from("AGENT_AUTH_TOKEN"),
|
||||
OsString::from("parent-secret"),
|
||||
)];
|
||||
let explicit = BTreeMap::from([("AGENT_AUTH_TOKEN".to_owned(), "explicit-value".to_owned())]);
|
||||
|
||||
let overrides = sanitized_environment_overrides(parent, &explicit);
|
||||
|
||||
assert_eq!(
|
||||
overrides.get("AGENT_AUTH_TOKEN").map(String::as_str),
|
||||
Some("explicit-value")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_commands_are_resolved_before_launch() {
|
||||
let executable = std::env::current_exe().unwrap();
|
||||
let launch = AcpLaunchConfig::new(&executable).resolve_command().unwrap();
|
||||
|
||||
assert_eq!(launch.command, executable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_resolution_uses_platform_extensions_in_order() {
|
||||
let temp_dir = tempfile::TempDir::new().unwrap();
|
||||
let command_path = temp_dir.path().join("npx.cmd");
|
||||
std::fs::write(&command_path, "@echo off\r\n").unwrap();
|
||||
|
||||
let resolved = find_executable_in_directories(
|
||||
"npx",
|
||||
[temp_dir.path().to_owned()],
|
||||
&[".exe".to_owned(), ".cmd".to_owned()],
|
||||
);
|
||||
|
||||
assert_eq!(resolved, Some(command_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_resolution_does_not_append_extensions_to_explicit_extension() {
|
||||
let temp_dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(temp_dir.path().join("agent.exe.cmd"), "@echo off\r\n").unwrap();
|
||||
|
||||
let resolved = find_executable_in_directories(
|
||||
"agent.exe",
|
||||
[temp_dir.path().to_owned()],
|
||||
&[".cmd".to_owned()],
|
||||
);
|
||||
|
||||
assert_eq!(resolved, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_path_extensions_are_parsed_in_declared_order() {
|
||||
let extensions = windows_executable_extensions(Some(std::ffi::OsStr::new(".COM;.EXE; .CMD;")));
|
||||
|
||||
assert_eq!(extensions, vec![".COM", ".EXE", ".CMD"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_path_extensions_use_standard_fallback_when_missing_or_empty() {
|
||||
let expected = vec![".COM", ".EXE", ".BAT", ".CMD"];
|
||||
|
||||
assert_eq!(windows_executable_extensions(None), expected);
|
||||
assert_eq!(
|
||||
windows_executable_extensions(Some(std::ffi::OsStr::new(" ; "))),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_path_sets_the_adapter_environment_variable() {
|
||||
let launch = AcpLaunchConfig::new("npx").codex_path(Path::new("/opt/codex"));
|
||||
|
||||
assert_eq!(
|
||||
launch.env.get("CODEX_PATH").map(String::as_str),
|
||||
Some("/opt/codex")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialization_timeout_is_configurable_and_bounded_by_default() {
|
||||
let default = AcpManagerConfig::new(AcpLaunchConfig::new("agent"));
|
||||
let custom = AcpManagerConfig::new(AcpLaunchConfig::new("agent"))
|
||||
.initialization_timeout(Duration::from_secs(2))
|
||||
.authentication_timeout(Duration::from_secs(3));
|
||||
|
||||
assert_eq!(default.initialization_timeout, Duration::from_secs(30));
|
||||
assert_eq!(default.authentication_timeout, Duration::from_secs(5 * 60));
|
||||
assert_eq!(custom.initialization_timeout, Duration::from_secs(2));
|
||||
assert_eq!(custom.authentication_timeout, Duration::from_secs(3));
|
||||
}
|
||||
Reference in New Issue
Block a user