prodigy 0.4.4

Turn ad-hoc Claude sessions into reproducible development pipelines with parallel AI agents
Documentation
//! Comprehensive tests for signal handling functionality

#[cfg(test)]
mod tests {
    use crate::cook::signal_handler::*;
    use std::process::{Command, Stdio};
    use std::time::Duration;

    #[test]
    fn test_setup_simple_interrupt_handler_doesnt_panic() {
        // Test that setting up the simple handler doesn't panic
        let result = setup_simple_interrupt_handler();
        assert!(result.is_ok());
    }

    #[cfg(unix)]
    #[test]
    fn test_signal_terminates_process() {
        use nix::sys::signal::{self, Signal};
        use nix::unistd::Pid;

        // Spawn a simple long-running process
        let mut child = Command::new("sleep")
            .arg("60")
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .expect("Failed to spawn child process");

        let child_pid = child.id() as i32;

        // Give the child time to start
        std::thread::sleep(Duration::from_millis(100));

        // Send SIGINT to the child
        signal::kill(Pid::from_raw(child_pid), Signal::SIGINT).unwrap();

        // Wait for the child to exit
        let exit_status = child.wait().expect("Failed to wait for child");

        // The process should have been terminated by the signal
        // Different systems may report this differently
        assert!(
            !exit_status.success(),
            "Process should not exit successfully after SIGINT"
        );
    }

    #[cfg(unix)]
    #[test]
    fn test_process_group_termination() {
        use nix::sys::signal::{self, Signal};
        use nix::unistd::Pid;

        // Create a simple process that we can terminate
        // Use a much shorter sleep time to make the test faster
        let mut parent = Command::new("sleep")
            .arg("0.5")
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .expect("Failed to spawn parent process");

        let parent_pid = parent.id() as i32;

        // Give the process a moment to start
        std::thread::sleep(Duration::from_millis(50));

        // Send SIGTERM to the process
        let _ = signal::kill(Pid::from_raw(parent_pid), Signal::SIGTERM);

        // Wait for process to terminate
        let wait_result = parent.wait();

        match wait_result {
            Ok(status) => {
                // On Unix, a process terminated by signal will not have success status
                // The exact behavior varies by platform, so we just check it terminated
                assert!(
                    status.code().is_none() || !status.success(),
                    "Process should be terminated by signal"
                );
            }
            Err(e) => panic!("Failed to wait for process: {}", e),
        }
    }
}

#[cfg(unix)]
#[cfg(test)]
mod integration_tests {
    use crate::cook::signal_handler::*;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use std::thread;
    use std::time::Duration;

    #[test]
    fn test_multiple_signal_handlers_dont_conflict() {
        // Test that we can set up multiple handlers without conflict
        let result1 = setup_simple_interrupt_handler();
        assert!(result1.is_ok());

        // Setting up another handler should not cause issues
        // (the second one will replace the first in the actual implementation)
        let result2 = setup_simple_interrupt_handler();
        assert!(result2.is_ok());
    }

    #[test]
    fn test_signal_handler_thread_spawns() {
        let handler_started = Arc::new(AtomicBool::new(false));
        let handler_clone = handler_started.clone();

        // Spawn a thread similar to our signal handler
        thread::spawn(move || {
            handler_clone.store(true, Ordering::SeqCst);
            // Simulate waiting for signals
            thread::sleep(Duration::from_millis(10));
        });

        // Give the thread time to start
        thread::sleep(Duration::from_millis(50));

        assert!(
            handler_started.load(Ordering::SeqCst),
            "Handler thread should start"
        );
    }
}

#[cfg(unix)]
#[cfg(test)]
mod subprocess_tests {
    use crate::subprocess::runner::TokioProcessRunner;
    use crate::subprocess::{ProcessCommand, ProcessRunner};
    use std::collections::HashMap;
    use std::time::Duration;

    #[tokio::test]
    async fn test_subprocess_with_process_group() {
        let runner = TokioProcessRunner;

        // Create a command that spawns child processes
        let command = ProcessCommand {
            program: "sh".to_string(),
            args: vec![
                "-c".to_string(),
                "echo 'parent'; sleep 0.1; echo 'done'".to_string(),
            ],
            env: HashMap::new(),
            working_dir: None,
            timeout: Some(Duration::from_secs(1)),
            stdin: None,
            suppress_stderr: false,
        };

        let result = runner.run(command).await;
        assert!(result.is_ok());

        let output = result.unwrap();
        assert!(output.stdout.contains("parent"));
        assert!(output.stdout.contains("done"));
    }

    #[tokio::test]
    async fn test_subprocess_timeout_kills_process_group() {
        let runner = TokioProcessRunner;

        // Create a command that would run for a reasonable test duration
        let command = ProcessCommand {
            program: "sh".to_string(),
            args: vec!["-c".to_string(), "sleep 2 & sleep 2 & wait".to_string()],
            env: HashMap::new(),
            working_dir: None,
            timeout: Some(Duration::from_millis(100)),
            stdin: None,
            suppress_stderr: false,
        };

        let result = runner.run(command).await;
        assert!(result.is_err());

        if let Err(e) = result {
            // Should be a timeout error
            assert!(e.to_string().contains("Timeout") || e.to_string().contains("timed out"));
        }
    }
}

#[cfg(test)]
mod mock_tests {
    use crate::cook::execution::command::CommandType;
    use crate::cook::execution::process::{ProcessManager, UnifiedProcess};
    use std::process::Stdio;
    use tokio::process::Command;

    #[tokio::test]
    async fn test_unified_process_kill_terminates_immediately() {
        // Create a long-running process
        let mut command = Command::new("sleep");
        command.arg("60");
        command.stdout(Stdio::null());
        command.stderr(Stdio::null());

        let child = command.spawn().unwrap();
        let mut process = UnifiedProcess::new(child, CommandType::Shell);

        // Get process ID before killing
        let pid = process.id();
        assert!(pid.0 > 0);

        // Kill should succeed
        let kill_result = process.kill().await;
        assert!(kill_result.is_ok(), "Kill should succeed");

        // Process should be terminated
        // Note: We can't easily check if the process is actually dead without
        // platform-specific code, but the kill should have been sent
    }

    #[tokio::test]
    async fn test_process_manager_cleanup_after_kill() {
        let manager = ProcessManager::new();

        // We can't easily test the full spawn -> kill -> cleanup cycle
        // without mocking, but we can test that the manager is created
        // and basic operations don't panic

        let process_id = crate::cook::execution::process::ProcessId(12345);

        // Cleanup of non-existent process should be a no-op
        let result = manager.cleanup_process(process_id).await;
        assert!(result.is_ok());
    }
}