pub mod bfs;
pub mod command;
mod executor_trait;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(any(test, feature = "test-utils"))]
mod mock;
pub mod ps;
mod real;
mod types;
pub use executor_trait::ProcessExecutor;
pub use real::RealProcessExecutor;
pub use types::{
AgentChild, AgentChildHandle, AgentCommandResult, AgentSpawnConfig, ChildProcessInfo,
ProcessOutput, RealAgentChild, SpawnedProcess,
};
#[cfg(any(test, feature = "test-utils"))]
pub use mock::{MockAgentChild, MockProcessExecutor};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_real_executor_can_be_created() {
let executor = RealProcessExecutor::new();
let _ = executor;
}
#[test]
#[cfg(unix)]
fn test_real_executor_execute_basic() {
let executor = RealProcessExecutor::new();
let result = executor.execute("echo", &["hello"], &[], None);
assert!(result.is_ok());
if let Ok(output) = result {
assert!(output.status.success());
assert_eq!(output.stdout.trim(), "hello");
}
}
mod safety {
use super::*;
use crate::agents::JsonParserType;
use std::collections::HashMap;
use tempfile::tempdir;
#[test]
#[cfg(unix)]
fn test_nonblocking_io_setup_succeeds() {
let executor = RealProcessExecutor::new();
let tempdir = tempdir().expect("create tempdir for logfile");
let logfile_path = tempdir.path().join("opencode_agent.log");
let config = AgentSpawnConfig {
command: "echo".to_string(),
args: vec!["test".to_string()],
env: HashMap::new(),
prompt: "test prompt".to_string(),
logfile: logfile_path.to_string_lossy().to_string(),
parser_type: JsonParserType::OpenCode,
};
let result = executor.spawn_agent(&config);
assert!(
result.is_ok(),
"Agent spawn with non-blocking I/O should succeed"
);
if let Ok(mut handle) = result {
let _ = handle.inner.wait();
}
}
#[test]
#[cfg(unix)]
fn test_process_termination_cleanup_works() {
let executor = RealProcessExecutor::new();
let result = executor.spawn("sleep", &["10"], &[], None);
assert!(result.is_ok(), "Process spawn should succeed");
if let Ok(mut child) = result {
assert!(
child.try_wait().unwrap().is_none(),
"Process should be running"
);
let kill_result = child.kill();
assert!(kill_result.is_ok(), "Process termination should succeed");
let wait_result = child.wait();
assert!(
wait_result.is_ok(),
"Process wait should succeed after kill"
);
}
}
#[test]
#[cfg(unix)]
fn test_process_group_creation_succeeds() {
let executor = RealProcessExecutor::new();
let tempdir = tempdir().expect("create tempdir for logfile");
let logfile_path = tempdir.path().join("opencode_agent.log");
let config = AgentSpawnConfig {
command: "echo".to_string(),
args: vec!["test".to_string()],
env: HashMap::new(),
prompt: "test prompt".to_string(),
logfile: logfile_path.to_string_lossy().to_string(),
parser_type: JsonParserType::OpenCode,
};
let result = executor.spawn_agent(&config);
assert!(
result.is_ok(),
"Agent spawn with process group creation should succeed"
);
if let Ok(mut handle) = result {
let _ = handle.inner.wait();
}
}
#[test]
fn test_executor_handles_invalid_command_gracefully() {
let executor = RealProcessExecutor::new();
let result = executor.spawn("nonexistent_command_12345", &[], &[], None);
assert!(
result.is_err(),
"Spawning nonexistent command should fail gracefully"
);
}
#[test]
#[cfg(unix)]
fn test_agent_spawn_handles_env_vars_correctly() {
let executor = RealProcessExecutor::new();
let mut env = HashMap::new();
env.insert("TEST_VAR_1".to_string(), "value1".to_string());
env.insert("TEST_VAR_2".to_string(), "value2".to_string());
let tempdir = tempdir().expect("create tempdir for logfile");
let logfile_path = tempdir.path().join("opencode_agent.log");
let config = AgentSpawnConfig {
command: "env".to_string(),
args: vec![],
env,
prompt: String::new(),
logfile: logfile_path.to_string_lossy().to_string(),
parser_type: JsonParserType::OpenCode,
};
let result = executor.spawn_agent(&config);
assert!(
result.is_ok(),
"Agent spawn with environment variables should succeed"
);
if let Ok(mut handle) = result {
let _ = handle.inner.wait();
}
}
#[test]
fn test_process_executor_execute_with_workdir() {
let executor = RealProcessExecutor::new();
#[cfg(unix)]
let result = executor.execute("pwd", &[], &[], Some(std::path::Path::new("/")));
#[cfg(not(unix))]
let result = executor.execute(
"cmd",
&["/c", "cd"],
&[],
Some(std::path::Path::new("C:\\")),
);
assert!(result.is_ok(), "Execute with workdir should succeed");
}
}
}