prodigy 0.4.4

Turn ad-hoc Claude sessions into reproducible development pipelines with parallel AI agents
Documentation
//! Tests for shell command failure handling in workflows
//!
//! These tests verify that shell commands properly handle failures
//! and respect on_failure configurations.

#[cfg(test)]
mod tests {
    use crate::config::mapreduce::parse_mapreduce_workflow;
    #[allow(unused_imports)]
    use tempfile::TempDir;

    /// Test parsing of on_failure configuration
    #[test]
    fn test_on_failure_parsing() {
        let yaml = r#"
name: test-on-failure
mode: mapreduce

setup:
  - shell: "exit 1"
    on_failure:
      fail_workflow: true
  - shell: "echo 'second'"

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo test"
"#;

        let config = parse_mapreduce_workflow(yaml).unwrap();
        assert!(config.setup.is_some());
        let setup = config.setup.unwrap();
        assert_eq!(setup.commands.len(), 2);

        // Check first step has on_failure configured
        let first_step = &setup.commands[0];
        assert!(first_step.on_failure.is_some());

        // The on_failure should indicate to fail the workflow
        let on_failure = first_step.on_failure.as_ref().unwrap();
        assert!(on_failure.should_fail_workflow());
    }

    /// Test that setup phase fails when a shell command returns non-zero
    #[tokio::test]
    async fn test_setup_fails_on_shell_error() {
        let yaml = r#"
name: test-shell-failure
mode: mapreduce

setup:
  - shell: "exit 1"  # This should fail and stop the workflow
  - shell: "echo 'should not execute'"  # This should never run

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo 'should not reach map phase'"
"#;

        let config = parse_mapreduce_workflow(yaml).unwrap();
        assert_eq!(config.name, "test-shell-failure");

        // TODO: Execute the workflow and verify it fails at the first setup step
        // The workflow should NOT continue to the second setup step or map phase
    }

    /// Test that on_failure with continue allows workflow to proceed
    #[tokio::test]
    async fn test_on_failure_continue() {
        let yaml = r#"
name: test-on-failure-continue
mode: mapreduce

setup:
  - shell: "exit 1"
    on_failure:
      shell: "echo 'Handling failure'"
      continue_workflow: true  # Proposed: continue after handling
  - shell: "echo 'This should execute'"

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo 'map phase'"
"#;

        let _config = parse_mapreduce_workflow(yaml).unwrap();

        // TODO: Execute and verify the second setup step runs
    }

    /// Test that on_failure with fail_workflow stops execution
    #[tokio::test]
    async fn test_on_failure_fail_workflow() {
        let yaml = r#"
name: test-on-failure-fail
mode: mapreduce

setup:
  - shell: "exit 1"
    on_failure:
      shell: "echo 'Handling failure'"
      fail_workflow: true  # Stop after handling
  - shell: "echo 'This should NOT execute'"

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo 'should not reach map phase'"
"#;

        let _config = parse_mapreduce_workflow(yaml).unwrap();

        // TODO: Execute and verify workflow stops after on_failure handler
    }

    /// Test ignore_errors flag (alternative to on_failure)
    #[tokio::test]
    async fn test_ignore_errors_flag() {
        let _yaml = r#"
name: test-ignore-errors
mode: mapreduce

setup:
  - shell: "exit 1"
    ignore_errors: true  # Proposed: simple flag to ignore failures
  - shell: "echo 'This should execute despite error'"

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo 'map phase'"
"#;

        // This would be a simpler alternative to on_failure
        // Similar to Ansible's ignore_errors
    }

    /// Test on_failure with retry logic
    #[tokio::test]
    async fn test_on_failure_with_retry() {
        let _yaml = r#"
name: test-retry-on-failure
mode: mapreduce

setup:
  - shell: "test -f /tmp/test_marker || exit 1"
    on_failure:
      shell: "touch /tmp/test_marker && echo 'Created marker'"
      retry_original: true  # Proposed: retry the original command
      max_retries: 2
  - shell: "echo 'Should execute after retry succeeds'"

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo 'map phase'"
"#;

        // Test that on_failure can fix issues and retry
    }

    /// Test mixed success and failure handling
    #[tokio::test]
    async fn test_mixed_commands() {
        let _yaml = r#"
name: test-mixed
mode: mapreduce

setup:
  - shell: "echo 'First command succeeds'"
  - shell: "exit 1"
    ignore_errors: true
  - shell: "echo 'Third command runs despite second failing'"
  - shell: "exit 2"  # This should fail the workflow
  - shell: "echo 'This should NOT run'"

map:
  input: dummy.json
  json_path: "$[*]"
  max_parallel: 1
  agent_template:
    commands:
      - shell: "echo 'should not reach here'"
"#;

        // Verify mixed behavior works correctly
    }

    /// Test that regular workflows (non-mapreduce) also handle failures correctly
    #[tokio::test]
    async fn test_regular_workflow_shell_failure() {
        let _yaml = r#"
- shell: "echo 'Starting workflow'"
- shell: "exit 1"  # Should fail here
- shell: "echo 'Should not execute'"
"#;

        // TODO: Parse as regular workflow and test execution
    }

    /// Test the actual command execution to ensure errors are propagated
    #[tokio::test]
    async fn test_shell_command_error_propagation() {
        use crate::cook::orchestrator::ExecutionEnvironment;
        use crate::cook::workflow::{CaptureOutput, WorkflowStep};
        use std::sync::Arc;

        let _step = WorkflowStep {
            name: Some("failing-command".to_string()),
            shell: Some("exit 42".to_string()),
            claude: None,
            test: None,
            foreach: None,
            write_file: None,
            command: None,
            handler: None,
            capture: None,
            capture_format: None,
            capture_streams: Default::default(),
            output_file: None,
            timeout: None,
            capture_output: CaptureOutput::Disabled,
            on_failure: None,
            retry: None,
            on_success: None,
            on_exit_code: Default::default(),
            commit_required: false,
            auto_commit: false,
            commit_config: None,
            validate: None,
            step_validate: None,
            skip_validation: false,
            validation_timeout: None,
            ignore_validation_failure: false,
            working_dir: None,
            env: Default::default(),
            when: None,
        };

        // Create a minimal execution environment
        let temp_dir = TempDir::new().unwrap();
        let _env = ExecutionEnvironment {
            working_dir: Arc::new(temp_dir.path().to_path_buf()),
            project_dir: Arc::new(temp_dir.path().to_path_buf()),
            worktree_name: None,
            session_id: Arc::from("test-session"),
        };

        // TODO: Create executor and run the step
        // Verify it returns an error with exit code 42
    }
}