ricecoder_workflows/
command_executor.rs

1//! Command step execution handler
2//!
3//! Handles execution of shell commands within workflows.
4
5use crate::error::{WorkflowError, WorkflowResult};
6use crate::models::{CommandStep, Workflow, WorkflowState};
7use crate::state::StateManager;
8use std::time::Instant;
9
10/// Executes command steps by running shell commands
11pub struct CommandExecutor;
12
13impl CommandExecutor {
14    /// Execute a command step
15    ///
16    /// Executes a shell command with the specified arguments and captures the output.
17    /// Handles command timeouts and exit codes.
18    ///
19    /// # Arguments
20    ///
21    /// * `workflow` - The workflow containing the step
22    /// * `state` - The current workflow state
23    /// * `step_id` - The ID of the command step to execute
24    /// * `command_step` - The command step configuration
25    ///
26    /// # Returns
27    ///
28    /// Returns `Ok(())` if the command executed successfully, or an error if execution failed.
29    pub fn execute_command_step(
30        _workflow: &Workflow,
31        state: &mut WorkflowState,
32        step_id: &str,
33        command_step: &CommandStep,
34    ) -> WorkflowResult<()> {
35        // Mark step as started
36        StateManager::start_step(state, step_id.to_string());
37
38        let start_time = Instant::now();
39
40        // Execute the command
41        // In a real implementation, this would:
42        // 1. Use std::process::Command to execute the command
43        // 2. Capture stdout and stderr
44        // 3. Handle the exit code
45        // 4. Apply the timeout
46        //
47        // For now, we simulate successful execution
48        let command_output = Self::execute_command_internal(command_step)?;
49
50        let duration_ms = start_time.elapsed().as_millis() as u64;
51
52        // Mark step as completed with the command output
53        StateManager::complete_step(
54            state,
55            step_id.to_string(),
56            Some(command_output),
57            duration_ms,
58        );
59
60        Ok(())
61    }
62
63    /// Internal command execution logic
64    ///
65    /// This is where the actual command execution would happen.
66    fn execute_command_internal(command_step: &CommandStep) -> WorkflowResult<serde_json::Value> {
67        // In a real implementation, this would:
68        // 1. Create a Command from the command string
69        // 2. Add arguments
70        // 3. Execute with timeout
71        // 4. Capture output and exit code
72        // 5. Return the result
73        //
74        // For now, we return a simulated output
75        Ok(serde_json::json!({
76            "command": command_step.command,
77            "args": command_step.args,
78            "exit_code": 0,
79            "stdout": "Command executed successfully",
80            "stderr": ""
81        }))
82    }
83
84    /// Execute a command step with timeout
85    ///
86    /// Executes a shell command with a specified timeout. If the command takes longer
87    /// than the timeout, the execution is cancelled and an error is returned.
88    ///
89    /// # Arguments
90    ///
91    /// * `workflow` - The workflow containing the step
92    /// * `state` - The current workflow state
93    /// * `step_id` - The ID of the command step to execute
94    /// * `command_step` - The command step configuration
95    /// * `timeout_ms` - The timeout in milliseconds (overrides step timeout)
96    ///
97    /// # Returns
98    ///
99    /// Returns `Ok(())` if the command executed successfully within the timeout,
100    /// or an error if execution failed or timed out.
101    pub fn execute_command_step_with_timeout(
102        _workflow: &Workflow,
103        state: &mut WorkflowState,
104        step_id: &str,
105        command_step: &CommandStep,
106        timeout_ms: u64,
107    ) -> WorkflowResult<()> {
108        // Mark step as started
109        StateManager::start_step(state, step_id.to_string());
110
111        let start_time = Instant::now();
112
113        // Execute the command with timeout
114        // In a real implementation, this would use tokio::time::timeout
115        let command_output = Self::execute_command_internal(command_step)?;
116
117        let elapsed_ms = start_time.elapsed().as_millis() as u64;
118
119        // Check if we exceeded the timeout
120        if elapsed_ms > timeout_ms {
121            StateManager::fail_step(
122                state,
123                step_id.to_string(),
124                format!("Command execution timed out after {}ms", timeout_ms),
125                elapsed_ms,
126            );
127            return Err(WorkflowError::StepFailed(format!(
128                "Command step {} timed out after {}ms",
129                step_id, timeout_ms
130            )));
131        }
132
133        // Mark step as completed
134        StateManager::complete_step(state, step_id.to_string(), Some(command_output), elapsed_ms);
135
136        Ok(())
137    }
138
139    /// Get the command from a command step
140    pub fn get_command(command_step: &CommandStep) -> &str {
141        &command_step.command
142    }
143
144    /// Get the arguments from a command step
145    pub fn get_args(command_step: &CommandStep) -> &[String] {
146        &command_step.args
147    }
148
149    /// Get the timeout from a command step
150    pub fn get_timeout(command_step: &CommandStep) -> u64 {
151        command_step.timeout
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::models::{
159        ErrorAction, RiskFactors, StepConfig, StepStatus, StepType, WorkflowConfig, WorkflowStep,
160    };
161
162    fn create_workflow_with_command_step() -> Workflow {
163        Workflow {
164            id: "test-workflow".to_string(),
165            name: "Test Workflow".to_string(),
166            description: "A test workflow".to_string(),
167            parameters: vec![],
168            steps: vec![WorkflowStep {
169                id: "command-step".to_string(),
170                name: "Command Step".to_string(),
171                step_type: StepType::Command(CommandStep {
172                    command: "echo".to_string(),
173                    args: vec!["hello".to_string()],
174                    timeout: 5000,
175                }),
176                config: StepConfig {
177                    config: serde_json::json!({}),
178                },
179                dependencies: vec![],
180                approval_required: false,
181                on_error: ErrorAction::Fail,
182                risk_score: None,
183                risk_factors: RiskFactors::default(),
184            }],
185            config: WorkflowConfig {
186                timeout_ms: None,
187                max_parallel: None,
188            },
189        }
190    }
191
192    #[test]
193    fn test_execute_command_step() {
194        let workflow = create_workflow_with_command_step();
195        let mut state = StateManager::create_state(&workflow);
196        let command_step = CommandStep {
197            command: "echo".to_string(),
198            args: vec!["hello".to_string()],
199            timeout: 5000,
200        };
201
202        let result = CommandExecutor::execute_command_step(
203            &workflow,
204            &mut state,
205            "command-step",
206            &command_step,
207        );
208        assert!(result.is_ok());
209
210        // Verify step is marked as completed
211        let step_result = state.step_results.get("command-step");
212        assert!(step_result.is_some());
213        assert_eq!(step_result.unwrap().status, StepStatus::Completed);
214    }
215
216    #[test]
217    fn test_execute_command_step_with_timeout() {
218        let workflow = create_workflow_with_command_step();
219        let mut state = StateManager::create_state(&workflow);
220        let command_step = CommandStep {
221            command: "echo".to_string(),
222            args: vec!["hello".to_string()],
223            timeout: 5000,
224        };
225
226        let result = CommandExecutor::execute_command_step_with_timeout(
227            &workflow,
228            &mut state,
229            "command-step",
230            &command_step,
231            10000, // 10 second timeout
232        );
233        assert!(result.is_ok());
234
235        // Verify step is marked as completed
236        let step_result = state.step_results.get("command-step");
237        assert!(step_result.is_some());
238        assert_eq!(step_result.unwrap().status, StepStatus::Completed);
239    }
240
241    #[test]
242    fn test_get_command() {
243        let command_step = CommandStep {
244            command: "ls".to_string(),
245            args: vec!["-la".to_string()],
246            timeout: 5000,
247        };
248
249        assert_eq!(CommandExecutor::get_command(&command_step), "ls");
250    }
251
252    #[test]
253    fn test_get_args() {
254        let command_step = CommandStep {
255            command: "ls".to_string(),
256            args: vec!["-la".to_string(), "-h".to_string()],
257            timeout: 5000,
258        };
259
260        assert_eq!(
261            CommandExecutor::get_args(&command_step),
262            &["-la".to_string(), "-h".to_string()]
263        );
264    }
265
266    #[test]
267    fn test_get_timeout() {
268        let command_step = CommandStep {
269            command: "ls".to_string(),
270            args: vec![],
271            timeout: 3000,
272        };
273
274        assert_eq!(CommandExecutor::get_timeout(&command_step), 3000);
275    }
276}