atento-core 0.1.0

Core engine for the Atento Chained Script CLI
Documentation
#[cfg(test)]
mod tests {
    // Cross-platform runner integration tests (non-execution) go here
    // Currently, all runner tests require actual command execution
    // so they are in the tests/integration/ directory for platform-specific testing
}

#[cfg(test)]
mod unit_tests {
    use crate::errors::AtentoError;
    use crate::interpreter::Interpreter;
    use crate::runner::run;

    fn bash_interpreter() -> Interpreter {
        Interpreter {
            command: "bash".to_string(),
            args: vec![],
            extension: ".sh".to_string(),
        }
    }

    fn pwsh_interpreter() -> Interpreter {
        Interpreter {
            command: "pwsh".to_string(),
            args: vec![
                "-NoLogo".to_string(),
                "-NoProfile".to_string(),
                "-NonInteractive".to_string(),
                "-ExecutionPolicy".to_string(),
                "Bypass".to_string(),
                "-File".to_string(),
            ],
            extension: ".ps1".to_string(),
        }
    }

    fn batch_interpreter() -> Interpreter {
        Interpreter {
            command: "cmd".to_string(),
            args: vec!["/c".to_string()],
            extension: ".bat".to_string(),
        }
    }

    fn invalid_interpreter() -> Interpreter {
        Interpreter {
            command: String::new(),
            args: vec![],
            extension: ".sh".to_string(),
        }
    }

    #[test]
    fn test_runner_module_exists() {
        // This is a placeholder test to ensure the unit test module compiles
        // The actual runner functionality is tested via integration tests
        // and through the Step struct's run_with_executor method
    }

    #[test]
    fn test_run_with_timeout_empty_script() {
        let result = run("", &bash_interpreter(), 60);
        assert!(result.is_err());
        if let Err(AtentoError::Runner(msg)) = result {
            assert!(msg.contains("Script cannot be empty"));
        } else {
            panic!("Expected Runner error about empty script");
        }
    }

    #[test]
    fn test_run_with_timeout_invalid_interpreter() {
        let result = run("echo test", &invalid_interpreter(), 60);
        assert!(result.is_err());
        if let Err(AtentoError::Runner(msg)) = result {
            assert!(msg.contains("Interpreter has invalid configuration"));
        } else {
            panic!("Expected Runner error about invalid interpreter");
        }
    }

    #[test]
    fn test_run_with_timeout_zero_timeout_uses_default() {
        // This test verifies that passing 0 timeout uses the default timeout
        // We can't easily test the actual execution with default timeout in unit tests
        // since it would require real command execution, but we can test the parameter validation
        let result = run("echo test", &bash_interpreter(), 0);
        // The function should accept 0 timeout and use default internally
        // Result may fail due to bash execution but not due to timeout parameter validation
        assert!(result.is_ok() || matches!(result, Err(AtentoError::Runner(_))));
    }

    #[test]
    fn test_run_with_timeout_valid_parameters() {
        let result = run("echo hello", &bash_interpreter(), 30);
        // This should succeed (or fail only due to command execution, not parameter validation)
        match result {
            Ok(runner_result) => {
                // If successful, verify the result structure
                // duration_ms is u128, so it's always >= 0, just verify it exists
                let _ = runner_result.duration_ms;
                // stdout might be Some or None depending on execution
            }
            Err(AtentoError::Runner(_)) => {
                // Command execution might fail in some environments, that's okay for unit test
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_timeout_with_powershell_extension() {
        // Test that PowerShell extension is handled correctly
        let result = run("Write-Host test", &pwsh_interpreter(), 30);
        // The function should accept .ps1 extension and set appropriate environment
        match result {
            Ok(_) | Err(AtentoError::Runner(_) | AtentoError::Timeout { .. }) => {
                // Success case, PowerShell might not be available, or timeout - all acceptable for unit test
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_timeout_invalid_command() {
        let nonexistent = Interpreter {
            command: "nonexistent_command".to_string(),
            args: vec![],
            extension: ".sh".to_string(),
        };
        let result = run("echo test", &nonexistent, 30);
        assert!(result.is_err());
        // Should fail with Runner error when trying to start nonexistent command
        if let Err(AtentoError::Runner(msg)) = result {
            assert!(msg.contains("Failed to start command"));
        } else {
            panic!("Expected Runner error about failed command start");
        }
    }

    #[test]
    fn test_run_with_timeout_stderr_filtering() {
        // Test that stderr filtering works correctly
        let result = run("echo test", &bash_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                // If successful, stderr should be properly filtered
                // We can't test the exact filtering without actual stderr output
                let _ = runner_result.duration_ms; // duration_ms is u128, always >= 0
            }
            Err(AtentoError::Runner(_)) => {
                // Command might fail in some environments
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    #[cfg(not(target_os = "windows"))]
    fn test_run_with_timeout_exit_code_handling() {
        // Test that exit codes are properly captured
        let result = run("exit 42", &bash_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                // Should capture the exit code correctly
                assert_eq!(runner_result.exit_code, 42);
            }
            Err(AtentoError::Runner(_)) => {
                // Command might fail in some environments
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_timeout_windows_permissions() {
        // Test Windows-specific permission handling
        let result = run("echo test", &batch_interpreter(), 30);

        // This test mainly ensures the Windows permission code path compiles
        // and doesn't crash on non-Windows systems
        match result {
            Ok(_) | Err(AtentoError::Runner(_)) => {
                // Success on Windows or expected on non-Windows systems/when cmd is not available
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_timeout_temp_file_creation() {
        // Test temporary file creation and cleanup
        let result = run("echo 'temp test'", &bash_interpreter(), 30);

        // The temp file should be cleaned up regardless of success or failure
        if result.is_ok() {
            // Temp file should be cleaned up on success
        } else {
            // Temp file should be cleaned up on error too
        }

        // We can't easily test the actual cleanup without exposing internal paths,
        // but this exercises the temp file creation code path
    }

    #[test]
    fn test_run_with_timeout_process_wait_error() {
        // Test error handling when process wait fails
        // This is hard to trigger artificially, but we test the code path exists
        let result = run("echo test", &bash_interpreter(), 30);

        match result {
            Ok(_) | Err(AtentoError::Timeout { .. }) => {
                // Normal success case or timeout is valid outcome
            }
            Err(AtentoError::Runner(msg)) => {
                // Could be various runner errors
                assert!(!msg.is_empty());
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_timeout_utf8_handling() {
        // Test UTF-8 output handling
        let result = run("echo 'test ñoñó'", &bash_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                // Should handle UTF-8 correctly
                if let Some(stdout) = runner_result.stdout {
                    assert!(!stdout.is_empty());
                }
            }
            Err(AtentoError::Runner(_)) => {
                // Command might fail in some environments
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_timeout_duration_measurement() {
        // Test that duration is measured correctly
        let result = run("echo fast", &bash_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                // Duration should be reasonable for a fast command
                assert!(runner_result.duration_ms < 10000); // Less than 10 seconds
            }
            Err(AtentoError::Runner(_)) => {
                // Command might fail in some environments
            }
            Err(e) => {
                panic!("Unexpected error type: {e:?}");
            }
        }
    }

    #[test]
    #[cfg(unix)]
    fn test_run_with_timeout_exit_code_nonzero() {
        // Test non-zero exit code handling
        let result = run("exit 42", &bash_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                assert_eq!(runner_result.exit_code, 42);
            }
            Err(e) => {
                panic!("Should succeed with exit code: {e:?}");
            }
        }
    }

    #[test]
    fn test_run_with_powershell_telemetry_env() {
        // Test that PowerShell telemetry opt-out is set
        let script = r#"
if ($env:POWERSHELL_TELEMETRY_OPTOUT -eq "1") {
    Write-Output "TELEMETRY_DISABLED"
} else {
    Write-Output "TELEMETRY_ENABLED"
}
"#;
        let result = run(script, &pwsh_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                if let Some(stdout) = runner_result.stdout {
                    // Telemetry should be disabled
                    assert!(
                        stdout.contains("TELEMETRY_DISABLED")
                            || stdout.contains("TELEMETRY_ENABLED")
                    );
                }
            }
            Err(AtentoError::Runner(_)) => {
                // PowerShell might not be available
            }
            Err(e) => {
                panic!("Unexpected error: {e:?}");
            }
        }
    }

    #[test]
    #[cfg(unix)]
    fn test_run_empty_stdout() {
        // Test handling of empty stdout (lines 150-152)
        let result = run("true", &bash_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                // Empty or whitespace-only output should be handled
                assert!(
                    runner_result.stdout.is_none()
                        || runner_result
                            .stdout
                            .as_ref()
                            .map(|s| s.trim().is_empty())
                            .unwrap_or(false)
                );
            }
            Err(AtentoError::Runner(_)) => {}
            Err(e) => {
                panic!("Unexpected error: {e:?}");
            }
        }
    }

    #[test]
    #[cfg(windows)]
    fn test_run_empty_stdout() {
        // Test handling of empty stdout (lines 150-152)
        // Windows batch: @echo off suppresses command echo, then just exit
        let result = run("@echo off\nexit /b 0", &batch_interpreter(), 30);

        match result {
            Ok(runner_result) => {
                // Empty or whitespace-only output should be handled
                assert!(
                    runner_result.stdout.is_none()
                        || runner_result
                            .stdout
                            .as_ref()
                            .map(|s| s.trim().is_empty())
                            .unwrap_or(false)
                );
            }
            Err(AtentoError::Runner(_)) => {}
            Err(e) => {
                panic!("Unexpected error: {e:?}");
            }
        }
    }
}