sage-cli 0.13.57

Command-line interface for Sage Agent
//! PTY Integration Tests for Sage CLI UI
//!
//! These tests spawn the actual sage binary in a pseudo-terminal
//! and verify UI behavior including error display, input handling, etc.

use rexpect::spawn_bash;
use std::time::Duration;

const TIMEOUT_MS: u64 = 30000;

/// Helper to get the sage binary path
fn sage_binary() -> String {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    format!("{}/../../target/debug/sage", manifest_dir)
}

/// Test that sage starts and shows the header
#[test]
fn test_sage_starts_with_header() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    // Start sage in print mode with a simple command
    p.send_line(&format!("{} -p 'echo hello'", sage_binary()))
        .expect("Failed to send command");

    // Should see some output (header or response)
    p.exp_regex(r"(?i)(sage|thinking|hello)")
        .expect("Should see sage output");
}

/// Test that invalid provider shows error message
#[test]
fn test_invalid_provider_shows_error() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    // Set invalid API key to trigger auth error
    p.send_line("export ANTHROPIC_API_KEY=invalid_key_12345")
        .expect("Failed to set env");

    // Run sage with print mode
    p.send_line(&format!("{} -p 'hello'", sage_binary()))
        .expect("Failed to send command");

    // Should see error message (authentication, invalid key, etc.)
    let result = p.exp_regex(r"(?i)(error|failed|invalid|unauthorized|401)");

    // Either we get an error message or the test passes if API works
    match result {
        Ok(_) => println!("Error message displayed correctly"),
        Err(_) => println!("No error (API might be valid or different error path)"),
    }
}

/// Test that Ctrl+C cancellation works
#[test]
fn test_ctrl_c_cancellation() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    // Start sage interactively
    p.send_line(&sage_binary().to_string())
        .expect("Failed to start sage");

    // Wait a bit for startup
    std::thread::sleep(Duration::from_millis(500));

    // Send Ctrl+C
    p.send_control('c').expect("Failed to send Ctrl+C");

    // Should exit gracefully
    std::thread::sleep(Duration::from_millis(500));
}

/// Test reading a non-existent file shows error
#[test]
fn test_read_nonexistent_file_error() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    // Run sage with a task that will fail
    p.send_line(&format!(
        "{} -p 'read the file /nonexistent/path/file_that_does_not_exist_12345.txt'",
        sage_binary()
    ))
    .expect("Failed to send command");

    // Should see some response about the file not existing
    let result = p.exp_regex(r"(?i)(not found|does not exist|no such file|error|cannot)");

    match result {
        Ok(_) => println!("File error displayed correctly"),
        Err(e) => println!("Test result: {:?}", e),
    }
}

/// Test that help command works
#[test]
fn test_help_command() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    p.send_line(&format!("{} --help", sage_binary()))
        .expect("Failed to send command");

    // Should see help text
    p.exp_regex(r"(?i)(usage|options|sage|cli)")
        .expect("Should see help output");
}

/// Test version command
#[test]
fn test_version_command() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    p.send_line(&format!("{} --version", sage_binary()))
        .expect("Failed to send command");

    // Should see version number
    p.exp_regex(r"\d+\.\d+\.\d+")
        .expect("Should see version number");
}

/// Test doctor command (health check)
#[test]
fn test_doctor_command() {
    let mut p = spawn_bash(Some(TIMEOUT_MS)).expect("Failed to spawn bash");

    p.send_line(&format!("{} doctor", sage_binary()))
        .expect("Failed to send command");

    // Should see health check output
    p.exp_regex(r"(?i)(health|check|config|provider|summary)")
        .expect("Should see doctor output");
}

#[cfg(test)]
mod ui_state_tests {
    //! Unit tests for UI state transitions

    use parking_lot::RwLock;
    use std::sync::Arc;

    /// Mock UI state for testing
    #[derive(Default)]
    struct MockUiState {
        is_busy: bool,
        error_displayed: bool,
        error_message: Option<String>,
    }

    #[test]
    fn test_error_state_sets_flags() {
        let state = Arc::new(RwLock::new(MockUiState::default()));

        // Simulate error event
        {
            let mut s = state.write();
            s.is_busy = false;
            s.error_displayed = true;
            s.error_message = Some("Test error".to_string());
        }

        // Verify state
        let s = state.read();
        assert!(!s.is_busy);
        assert!(s.error_displayed);
        assert_eq!(s.error_message, Some("Test error".to_string()));
    }

    #[test]
    fn test_error_flag_resets_on_new_task() {
        let state = Arc::new(RwLock::new(MockUiState {
            is_busy: false,
            error_displayed: true,
            error_message: Some("Old error".to_string()),
        }));

        // Simulate new task starting
        {
            let mut s = state.write();
            s.is_busy = true;
            s.error_displayed = false;
            s.error_message = None;
        }

        // Verify state reset
        let s = state.read();
        assert!(s.is_busy);
        assert!(!s.error_displayed);
        assert!(s.error_message.is_none());
    }
}