ralph_workflow/executor/
mod.rs1pub mod bfs;
50pub mod command;
51mod executor_trait;
52#[cfg(target_os = "macos")]
53pub mod macos;
54#[cfg(any(test, feature = "test-utils"))]
55mod mock;
56pub mod ps;
57mod real;
58mod types;
59
60pub use executor_trait::ProcessExecutor;
62pub use real::RealProcessExecutor;
63pub use types::{
64 AgentChild, AgentChildHandle, AgentCommandResult, AgentSpawnConfig, ChildProcessInfo,
65 ProcessOutput, RealAgentChild, SpawnedProcess,
66};
67
68#[cfg(any(test, feature = "test-utils"))]
69pub use mock::{MockAgentChild, MockProcessExecutor};
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn test_real_executor_can_be_created() {
77 let executor = RealProcessExecutor::new();
78 let _ = executor;
80 }
81
82 #[test]
83 #[cfg(unix)]
84 fn test_real_executor_execute_basic() {
85 let executor = RealProcessExecutor::new();
86 let result = executor.execute("echo", &["hello"], &[], None);
88 assert!(result.is_ok());
90 if let Ok(output) = result {
91 assert!(output.status.success());
92 assert_eq!(output.stdout.trim(), "hello");
93 }
94 }
95
96 mod safety {
102 use super::*;
103 use crate::agents::JsonParserType;
104 use std::collections::HashMap;
105 use tempfile::tempdir;
106
107 #[test]
108 #[cfg(unix)]
109 fn test_nonblocking_io_setup_succeeds() {
110 let executor = RealProcessExecutor::new();
112 let tempdir = tempdir().expect("create tempdir for logfile");
113 let logfile_path = tempdir.path().join("opencode_agent.log");
114 let config = AgentSpawnConfig {
115 command: "echo".to_string(),
116 args: vec!["test".to_string()],
117 env: HashMap::new(),
118 prompt: "test prompt".to_string(),
119 logfile: logfile_path.to_string_lossy().to_string(),
120 parser_type: JsonParserType::OpenCode,
121 };
122
123 let result = executor.spawn_agent(&config);
124
125 assert!(
126 result.is_ok(),
127 "Agent spawn with non-blocking I/O should succeed"
128 );
129
130 if let Ok(mut handle) = result {
132 let _ = handle.inner.wait();
133 }
134 }
135
136 #[test]
137 #[cfg(unix)]
138 fn test_process_termination_cleanup_works() {
139 let executor = RealProcessExecutor::new();
141
142 let result = executor.spawn("sleep", &["10"], &[], None);
144
145 assert!(result.is_ok(), "Process spawn should succeed");
146
147 if let Ok(mut child) = result {
148 assert!(
150 child.try_wait().unwrap().is_none(),
151 "Process should be running"
152 );
153
154 let kill_result = child.kill();
156 assert!(kill_result.is_ok(), "Process termination should succeed");
157
158 let wait_result = child.wait();
160 assert!(
161 wait_result.is_ok(),
162 "Process wait should succeed after kill"
163 );
164 }
165 }
166
167 #[test]
168 #[cfg(unix)]
169 fn test_process_group_creation_succeeds() {
170 let executor = RealProcessExecutor::new();
172 let tempdir = tempdir().expect("create tempdir for logfile");
173 let logfile_path = tempdir.path().join("opencode_agent.log");
174 let config = AgentSpawnConfig {
175 command: "echo".to_string(),
176 args: vec!["test".to_string()],
177 env: HashMap::new(),
178 prompt: "test prompt".to_string(),
179 logfile: logfile_path.to_string_lossy().to_string(),
180 parser_type: JsonParserType::OpenCode,
181 };
182
183 let result = executor.spawn_agent(&config);
185
186 assert!(
187 result.is_ok(),
188 "Agent spawn with process group creation should succeed"
189 );
190
191 if let Ok(mut handle) = result {
193 let _ = handle.inner.wait();
194 }
195 }
196
197 #[test]
198 fn test_executor_handles_invalid_command_gracefully() {
199 let executor = RealProcessExecutor::new();
201
202 let result = executor.spawn("nonexistent_command_12345", &[], &[], None);
204
205 assert!(
206 result.is_err(),
207 "Spawning nonexistent command should fail gracefully"
208 );
209 }
210
211 #[test]
212 #[cfg(unix)]
213 fn test_agent_spawn_handles_env_vars_correctly() {
214 let executor = RealProcessExecutor::new();
217 let mut env = HashMap::new();
218 env.insert("TEST_VAR_1".to_string(), "value1".to_string());
219 env.insert("TEST_VAR_2".to_string(), "value2".to_string());
220
221 let tempdir = tempdir().expect("create tempdir for logfile");
222 let logfile_path = tempdir.path().join("opencode_agent.log");
223
224 let config = AgentSpawnConfig {
225 command: "env".to_string(),
226 args: vec![],
227 env,
228 prompt: String::new(),
229 logfile: logfile_path.to_string_lossy().to_string(),
230 parser_type: JsonParserType::OpenCode,
231 };
232
233 let result = executor.spawn_agent(&config);
234 assert!(
235 result.is_ok(),
236 "Agent spawn with environment variables should succeed"
237 );
238
239 if let Ok(mut handle) = result {
241 let _ = handle.inner.wait();
242 }
243 }
244
245 #[test]
246 fn test_process_executor_execute_with_workdir() {
247 let executor = RealProcessExecutor::new();
249
250 #[cfg(unix)]
252 let result = executor.execute("pwd", &[], &[], Some(std::path::Path::new("/")));
253
254 #[cfg(not(unix))]
255 let result = executor.execute(
256 "cmd",
257 &["/c", "cd"],
258 &[],
259 Some(std::path::Path::new("C:\\")),
260 );
261
262 assert!(result.is_ok(), "Execute with workdir should succeed");
263 }
264 }
265}