ricecoder_execution/
step_action_handler.rs

1//! Step action handlers for different action types
2//!
3//! Implements handlers for each step action type:
4//! - CreateFile: Create new files with content
5//! - ModifyFile: Apply diffs to existing files
6//! - DeleteFile: Delete files using PathResolver
7//! - RunCommand: Execute shell commands
8//! - RunTests: Run tests with framework detection
9//!
10//! **CRITICAL**: All file operations use FileOperations wrapper which ensures
11//! all paths are validated through ricecoder_storage::PathResolver.
12
13use crate::error::{ExecutionError, ExecutionResult};
14use crate::file_operations::FileOperations;
15use std::process::Command;
16use tracing::{debug, error, info};
17
18/// Handles file creation actions
19pub struct CreateFileHandler;
20
21impl CreateFileHandler {
22    /// Create a file with the specified content
23    ///
24    /// # Arguments
25    /// * `path` - File path (validated with PathResolver via FileOperations)
26    /// * `content` - Content to write to the file
27    ///
28    /// # Errors
29    /// Returns error if path is invalid or file creation fails
30    pub fn handle(path: &str, content: &str) -> ExecutionResult<()> {
31        debug!(path = %path, content_len = content.len(), "Creating file");
32
33        // Use FileOperations wrapper which validates path with PathResolver
34        FileOperations::create_file(path, content)?;
35
36        info!(path = %path, "File created successfully");
37        Ok(())
38    }
39}
40
41/// Handles file modification actions
42pub struct ModifyFileHandler;
43
44impl ModifyFileHandler {
45    /// Modify a file by applying a diff
46    ///
47    /// # Arguments
48    /// * `path` - File path (validated with PathResolver via FileOperations)
49    /// * `diff` - Diff to apply to the file
50    ///
51    /// # Errors
52    /// Returns error if path is invalid, file doesn't exist, or diff application fails
53    pub fn handle(path: &str, diff: &str) -> ExecutionResult<()> {
54        debug!(path = %path, diff_len = diff.len(), "Modifying file");
55
56        // Use FileOperations wrapper which validates path with PathResolver
57        FileOperations::modify_file(path, diff)?;
58
59        info!(path = %path, "File modified successfully");
60        Ok(())
61    }
62}
63
64/// Handles file deletion actions
65pub struct DeleteFileHandler;
66
67impl DeleteFileHandler {
68    /// Delete a file
69    ///
70    /// # Arguments
71    /// * `path` - File path (validated with PathResolver via FileOperations)
72    ///
73    /// # Errors
74    /// Returns error if path is invalid or file deletion fails
75    pub fn handle(path: &str) -> ExecutionResult<()> {
76        debug!(path = %path, "Deleting file");
77
78        // Use FileOperations wrapper which validates path with PathResolver
79        FileOperations::delete_file(path)?;
80
81        info!(path = %path, "File deleted successfully");
82        Ok(())
83    }
84}
85
86/// Handles command execution actions
87pub struct CommandHandler;
88
89impl CommandHandler {
90    /// Execute a shell command
91    ///
92    /// # Arguments
93    /// * `command` - Command to execute
94    /// * `args` - Command arguments
95    ///
96    /// # Errors
97    /// Returns error if command execution fails or returns non-zero exit code
98    pub fn handle(command: &str, args: &[String]) -> ExecutionResult<()> {
99        debug!(command = %command, args_count = args.len(), "Running command");
100
101        // Create and execute the command
102        let mut cmd = Command::new(command);
103        cmd.args(args);
104
105        let output = cmd.output().map_err(|e| {
106            ExecutionError::StepFailed(format!("Failed to execute command {}: {}", command, e))
107        })?;
108
109        // Check exit code
110        if !output.status.success() {
111            let stderr = String::from_utf8_lossy(&output.stderr);
112            let exit_code = output.status.code().unwrap_or(-1);
113            error!(
114                command = %command,
115                exit_code = exit_code,
116                stderr = %stderr,
117                "Command failed"
118            );
119            return Err(ExecutionError::StepFailed(format!(
120                "Command {} failed with exit code {}: {}",
121                command, exit_code, stderr
122            )));
123        }
124
125        let stdout = String::from_utf8_lossy(&output.stdout);
126        info!(
127            command = %command,
128            output_len = stdout.len(),
129            "Command executed successfully"
130        );
131
132        Ok(())
133    }
134}
135
136/// Handles test execution actions
137pub struct TestHandler;
138
139impl TestHandler {
140    /// Run tests with optional pattern filtering
141    ///
142    /// # Arguments
143    /// * `pattern` - Optional test pattern to filter tests
144    ///
145    /// # Errors
146    /// Returns error if test framework detection fails or tests fail
147    pub fn handle(pattern: &Option<String>) -> ExecutionResult<()> {
148        debug!(pattern = ?pattern, "Running tests");
149
150        // Detect test framework
151        let framework = Self::detect_test_framework()?;
152
153        // Build test command based on framework
154        let (command, args) = Self::build_test_command(&framework, pattern)?;
155
156        // Execute tests
157        CommandHandler::handle(&command, &args)?;
158
159        info!("Tests executed successfully");
160        Ok(())
161    }
162
163    /// Detect the test framework based on project structure
164    fn detect_test_framework() -> ExecutionResult<TestFramework> {
165        let current_dir = std::env::current_dir().map_err(|e| {
166            ExecutionError::ValidationError(format!("Failed to get current dir: {}", e))
167        })?;
168
169        // Check for Rust (Cargo.toml)
170        if current_dir.join("Cargo.toml").exists() {
171            debug!("Detected Rust project");
172            return Ok(TestFramework::Rust);
173        }
174
175        // Check for TypeScript/Node.js (package.json)
176        if current_dir.join("package.json").exists() {
177            debug!("Detected TypeScript/Node.js project");
178            return Ok(TestFramework::TypeScript);
179        }
180
181        // Check for Python (pytest.ini or setup.py)
182        if current_dir.join("pytest.ini").exists() || current_dir.join("setup.py").exists() {
183            debug!("Detected Python project");
184            return Ok(TestFramework::Python);
185        }
186
187        Err(ExecutionError::ValidationError(
188            "Could not detect test framework".to_string(),
189        ))
190    }
191
192    /// Build test command for the detected framework
193    fn build_test_command(
194        framework: &TestFramework,
195        pattern: &Option<String>,
196    ) -> ExecutionResult<(String, Vec<String>)> {
197        match framework {
198            TestFramework::Rust => {
199                let mut args = vec!["test".to_string()];
200                if let Some(p) = pattern {
201                    args.push(p.clone());
202                }
203                Ok(("cargo".to_string(), args))
204            }
205            TestFramework::TypeScript => {
206                let mut args = vec![];
207                if let Some(p) = pattern {
208                    args.push(p.clone());
209                }
210                Ok(("npm".to_string(), args))
211            }
212            TestFramework::Python => {
213                let mut args = vec![];
214                if let Some(p) = pattern {
215                    args.push(p.clone());
216                }
217                Ok(("pytest".to_string(), args))
218            }
219        }
220    }
221}
222
223/// Test framework type
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225enum TestFramework {
226    /// Rust (cargo test)
227    Rust,
228    /// TypeScript (npm test)
229    TypeScript,
230    /// Python (pytest)
231    Python,
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use tempfile::TempDir;
238
239    #[test]
240    fn test_create_file_handler() {
241        let temp_dir = TempDir::new().unwrap();
242        let file_path = temp_dir.path().join("test.txt");
243        let path_str = file_path.to_string_lossy().to_string();
244
245        let result = CreateFileHandler::handle(&path_str, "test content");
246        assert!(result.is_ok());
247        assert!(file_path.exists());
248
249        let content = std::fs::read_to_string(&file_path).unwrap();
250        assert_eq!(content, "test content");
251    }
252
253    #[test]
254    fn test_create_file_with_parent_dirs() {
255        let temp_dir = TempDir::new().unwrap();
256        let file_path = temp_dir.path().join("subdir/nested/test.txt");
257        let path_str = file_path.to_string_lossy().to_string();
258
259        let result = CreateFileHandler::handle(&path_str, "nested content");
260        assert!(result.is_ok());
261        assert!(file_path.exists());
262    }
263
264    #[test]
265    fn test_delete_file_handler() {
266        let temp_dir = TempDir::new().unwrap();
267        let file_path = temp_dir.path().join("test.txt");
268        let path_str = file_path.to_string_lossy().to_string();
269
270        // Create the file first
271        std::fs::write(&file_path, "content").unwrap();
272        assert!(file_path.exists());
273
274        // Delete it
275        let result = DeleteFileHandler::handle(&path_str);
276        assert!(result.is_ok());
277        assert!(!file_path.exists());
278    }
279
280    #[test]
281    fn test_delete_nonexistent_file() {
282        let result = DeleteFileHandler::handle("/nonexistent/path/file.txt");
283        assert!(result.is_err());
284    }
285
286    #[test]
287    fn test_modify_file_handler() {
288        let temp_dir = TempDir::new().unwrap();
289        let file_path = temp_dir.path().join("test.txt");
290        let path_str = file_path.to_string_lossy().to_string();
291
292        // Create the file first
293        std::fs::write(&file_path, "original content").unwrap();
294
295        // Modify it (with a non-empty diff)
296        let result = ModifyFileHandler::handle(&path_str, "some diff");
297        assert!(result.is_ok());
298    }
299
300    #[test]
301    fn test_modify_nonexistent_file() {
302        let result = ModifyFileHandler::handle("/nonexistent/path/file.txt", "diff");
303        assert!(result.is_err());
304    }
305
306    #[test]
307    fn test_modify_with_empty_diff() {
308        let temp_dir = TempDir::new().unwrap();
309        let file_path = temp_dir.path().join("test.txt");
310        let path_str = file_path.to_string_lossy().to_string();
311
312        std::fs::write(&file_path, "content").unwrap();
313
314        let result = ModifyFileHandler::handle(&path_str, "");
315        assert!(result.is_err());
316    }
317
318    #[test]
319    fn test_command_handler_success() {
320        let result = CommandHandler::handle("echo", &["hello".to_string()]);
321        assert!(result.is_ok());
322    }
323
324    #[test]
325    fn test_command_handler_failure() {
326        let result = CommandHandler::handle("false", &[]);
327        assert!(result.is_err());
328    }
329
330    #[test]
331    fn test_command_handler_nonexistent() {
332        let result = CommandHandler::handle("nonexistent_command_xyz", &[]);
333        assert!(result.is_err());
334    }
335}