prodigy 0.4.4

Turn ad-hoc Claude sessions into reproducible development pipelines with parallel AI agents
Documentation
//! Unit tests for the backward compatibility bridge

#[cfg(test)]
mod tests {
    use super::super::bridge::*;
    use super::super::command::*;
    use super::super::executor::{CommandResult, CommandStatus, UnifiedCommandExecutor};
    use super::super::output::{OutputProcessor, ProcessOutput, ProcessedOutput};
    use super::super::process::ProcessManager;
    use super::super::{ExecutionContext, ExecutionResult};
    use anyhow::Result;
    use async_trait::async_trait;
    use std::collections::HashMap;
    use std::path::{Path, PathBuf};
    use std::sync::Arc;
    use std::time::Duration;

    fn create_test_bridge(_should_succeed: bool, _output: &str) -> LegacyExecutorBridge {
        // Create real components for more realistic testing
        let resource_monitor = Arc::new(super::super::executor::ResourceMonitor);
        let process_manager = Arc::new(ProcessManager::new());
        let output_processor = Arc::new(OutputProcessor::new());
        let observability = Arc::new(super::super::bridge::NoOpObservability);

        let unified_executor = Arc::new(UnifiedCommandExecutor::new(
            process_manager,
            output_processor,
            observability,
            resource_monitor,
        ));

        LegacyExecutorBridge::new(unified_executor)
    }

    #[tokio::test]
    async fn test_legacy_context_conversion() {
        let legacy_context = ExecutionContext {
            working_directory: PathBuf::from("/test"),
            env_vars: HashMap::from([("KEY".to_string(), "VALUE".to_string())]),
            capture_output: true,
            timeout_seconds: Some(30),
            stdin: Some("input".to_string()),
            capture_streaming: false,
            streaming_config: None,
        };

        let unified_context = LegacyExecutorBridge::to_unified_context(&legacy_context);

        assert_eq!(unified_context.working_dir, PathBuf::from("/test"));
        assert_eq!(
            unified_context.env_vars.get("KEY"),
            Some(&"VALUE".to_string())
        );
        assert!(unified_context.capture_output);
        assert_eq!(unified_context.timeout, Some(Duration::from_secs(30)));
        assert_eq!(unified_context.stdin, Some("input".to_string()));
    }

    #[tokio::test]
    async fn test_unified_result_conversion() {
        let unified_result = CommandResult {
            command_id: "test".to_string(),
            command_spec: CommandSpec::Shell {
                command: "test".to_string(),
                shell: None,
                working_dir: None,
                env: None,
            },
            status: CommandStatus::Success,
            output: ProcessedOutput {
                content: ProcessOutput::new()
                    .with_stdout("stdout content".to_string())
                    .with_stderr("stderr content".to_string()),
                format: OutputFormat::PlainText,
                processing_duration: Duration::from_secs(0),
                warnings: Vec::new(),
            },
            execution_time: Duration::from_secs(1),
            resource_usage: super::super::executor::ResourceUsage::default(),
            exit_code: Some(0),
            error: None,
            validation_result: None,
            metadata: super::super::executor::ExecutionMetadata::new(),
        };

        let legacy_result = LegacyExecutorBridge::from_unified_result(unified_result);

        assert!(legacy_result.success);
        assert_eq!(legacy_result.stdout, "stdout content");
        assert_eq!(legacy_result.stderr, "stderr content");
        assert_eq!(legacy_result.exit_code, Some(0));
    }

    #[tokio::test]
    async fn test_unified_result_conversion_failure() {
        let unified_result = CommandResult {
            command_id: "test".to_string(),
            command_spec: CommandSpec::Shell {
                command: "test".to_string(),
                shell: None,
                working_dir: None,
                env: None,
            },
            status: CommandStatus::Failed {
                reason: super::super::executor::FailureReason::NonZeroExit(1),
                retryable: false,
            },
            output: ProcessedOutput {
                content: ProcessOutput::new()
                    .with_stdout("stdout".to_string())
                    .with_stderr("error message".to_string()),
                format: OutputFormat::PlainText,
                processing_duration: Duration::from_secs(0),
                warnings: Vec::new(),
            },
            execution_time: Duration::from_secs(1),
            resource_usage: super::super::executor::ResourceUsage::default(),
            exit_code: Some(1),
            error: None,
            validation_result: None,
            metadata: super::super::executor::ExecutionMetadata::new(),
        };

        let legacy_result = LegacyExecutorBridge::from_unified_result(unified_result);

        assert!(!legacy_result.success);
        assert_eq!(legacy_result.stderr, "error message");
        assert_eq!(legacy_result.exit_code, Some(1));
    }

    #[test]
    fn test_no_op_observability() {
        // Just ensure NoOpObservability can be created and used
        let _observability = super::super::bridge::NoOpObservability;

        // The trait methods are async, so we'd need tokio to test them
        // This test just ensures the struct exists and compiles
        assert!(std::mem::size_of::<super::super::bridge::NoOpObservability>() == 0);
        // Zero-sized type
    }

    #[tokio::test]
    async fn test_no_op_observability_methods() {
        let observability = super::super::bridge::NoOpObservability;

        // These should be no-ops and not panic
        let context = super::super::executor::ExecutionContextInternal {
            id: uuid::Uuid::new_v4(),
            request: CommandRequest {
                spec: CommandSpec::Shell {
                    command: "test".to_string(),
                    shell: None,
                    working_dir: None,
                    env: None,
                },
                execution_config: ExecutionConfig::default(),
                context: super::super::command::ExecutionContext::default(),
                metadata: CommandMetadata::new("test"),
            },
            resource_limits: None,
        };

        use super::super::executor::ObservabilityCollector;
        observability.record_command_start(&context).await;

        let result: Result<CommandResult> = Ok(CommandResult {
            command_id: "test".to_string(),
            command_spec: CommandSpec::Shell {
                command: "test".to_string(),
                shell: None,
                working_dir: None,
                env: None,
            },
            status: CommandStatus::Success,
            output: ProcessedOutput {
                content: ProcessOutput::empty(),
                format: OutputFormat::PlainText,
                processing_duration: Duration::from_secs(0),
                warnings: Vec::new(),
            },
            execution_time: Duration::from_secs(1),
            resource_usage: super::super::executor::ResourceUsage::default(),
            exit_code: Some(0),
            error: None,
            validation_result: None,
            metadata: super::super::executor::ExecutionMetadata::new(),
        });

        observability.record_command_complete(&result).await;
    }

    #[test]
    fn test_create_legacy_executor() {
        // Create a mock command runner
        struct MockRunner;

        #[async_trait]
        impl super::super::runner::CommandRunner for MockRunner {
            async fn run_command(
                &self,
                _cmd: &str,
                _args: &[String],
            ) -> Result<std::process::Output> {
                Ok(std::process::Command::new("echo")
                    .arg("test")
                    .output()
                    .unwrap())
            }

            async fn run_with_context(
                &self,
                _cmd: &str,
                _args: &[String],
                _context: &ExecutionContext,
            ) -> Result<ExecutionResult> {
                Ok(ExecutionResult {
                    success: true,
                    stdout: "test output".to_string(),
                    stderr: String::new(),
                    exit_code: Some(0),
                    metadata: HashMap::new(),
                })
            }
        }

        let runner = MockRunner;
        let _executor = create_legacy_executor(runner);

        // The test passes if it compiles and creates the executor
    }

    #[tokio::test]
    async fn test_command_request_creation_for_claude() {
        let _bridge = create_test_bridge(true, "test output");

        // Test data
        let _command = "test command";
        let _project_path = Path::new("/test/project");
        let _env_vars = HashMap::from([
            ("KEY1".to_string(), "VALUE1".to_string()),
            ("KEY2".to_string(), "VALUE2".to_string()),
        ]);

        // We can't directly test execute_claude_command without a real executor,
        // but we can verify the request would be created correctly
        // by checking the types compile and the bridge is created
        assert!(std::mem::size_of::<LegacyExecutorBridge>() > 0);
    }

    #[tokio::test]
    async fn test_shell_command_detection() {
        // Test that non-claude commands are treated as shell commands
        let _context = ExecutionContext {
            working_directory: PathBuf::from("/test"),
            env_vars: HashMap::new(),
            capture_output: true,
            timeout_seconds: None,
            stdin: None,
            capture_streaming: false,
            streaming_config: None,
        };

        // This test verifies the command type detection logic compiles
        let is_claude = "claude" == "claude";
        assert!(is_claude);

        let is_not_claude = "echo" == "claude";
        assert!(!is_not_claude);
    }

    #[test]
    fn test_capture_output_mode_conversion() {
        // Test that capture_output boolean converts to proper CaptureOutputMode
        let capture_true = if true {
            CaptureOutputMode::Both
        } else {
            CaptureOutputMode::None
        };
        assert!(matches!(capture_true, CaptureOutputMode::Both));

        let capture_false = if false {
            CaptureOutputMode::Both
        } else {
            CaptureOutputMode::None
        };
        assert!(matches!(capture_false, CaptureOutputMode::None));
    }

    #[test]
    fn test_command_metadata_creation() {
        let metadata = CommandMetadata::new("test_type");
        assert_eq!(metadata.tags.get("type"), Some(&"test_type".to_string()));
        assert!(!metadata.command_id.is_empty());
    }
}