osvm 0.8.3

OpenSVM CLI tool for managing SVM nodes and deployments
Documentation
//! Comprehensive unit tests for agent chat input handling
//!
//! This module tests the refactored input handling functions to ensure
//! proper behavior, error handling, and edge case coverage.

#[cfg(test)]
mod tests {
    use super::super::agent_chat::*;
    use anyhow::Result;
    use crossterm::terminal::{
        disable_raw_mode as crossterm_disable_raw_mode,
        enable_raw_mode as crossterm_enable_raw_mode,
    };
    use std::time::Duration;
    use tokio::sync::mpsc;

    /// Test InputState initialization and configuration
    #[test]
    fn test_input_state_initialization() {
        let state = InputState::new();

        assert_eq!(state.input, "");
        assert_eq!(state.cursor_pos, 0);
        assert_eq!(state.selected_suggestion, 0);
        assert_eq!(state.history_index, 0); // Initial position
        assert_eq!(state.config.max_history_size, 100);
        assert_eq!(state.config.debounce_ms, 150);
        assert_eq!(state.config.max_suggestions, 8);

        // Check default command history
        assert!(state.command_history.contains(&"/balance".to_string()));
        assert!(state.command_history.contains(&"/transactions".to_string()));
    }

    /// Test command history management with size limits
    #[test]
    fn test_command_history_size_limit() {
        let mut state = InputState::new();
        state.config.max_history_size = 8; // Bigger than default 5

        // Add commands beyond the limit
        state.add_to_history("cmd1".to_string());
        state.add_to_history("cmd2".to_string());
        state.add_to_history("cmd3".to_string());
        state.add_to_history("cmd4".to_string());

        // Should have 5 default + 4 new = 9, but limited to 8
        assert_eq!(state.command_history.len(), 8);
        // First default command should be removed to make room
        assert!(!state.command_history.contains(&"/balance".to_string()));
        assert!(state.command_history.contains(&"cmd4".to_string()));
    }

    /// Test empty command filtering
    #[test]
    fn test_empty_command_filtering() {
        let mut state = InputState::new();
        let initial_len = state.command_history.len();

        // Empty commands should not be added
        state.add_to_history("".to_string());
        state.add_to_history("   ".to_string());
        state.add_to_history("\t\n".to_string());

        assert_eq!(state.command_history.len(), initial_len);
    }

    /// Test debouncing behavior
    #[test]
    fn test_suggestion_debouncing() {
        let mut state = InputState::new();
        state.config.debounce_ms = 50; // Short debounce for testing

        // Wait a bit to ensure initial timestamp is old enough
        std::thread::sleep(Duration::from_millis(60));

        // First call should allow update (enough time has passed)
        let first_result = state.should_update_suggestions();
        assert!(first_result);

        // Immediate second call should be debounced
        assert!(!state.should_update_suggestions());

        // Wait for debounce period
        std::thread::sleep(Duration::from_millis(150));

        // Should allow update after debounce period
        assert!(state.should_update_suggestions());
    }

    /// Test input character classification
    #[test]
    fn test_input_char_classification() {
        // Test regular characters
        assert!(matches!(classify_char('a'), InputChar::Regular('a')));
        assert!(matches!(classify_char('1'), InputChar::Regular('1')));
        assert!(matches!(classify_char('/'), InputChar::Regular('/')));

        // Test control characters
        assert!(matches!(classify_char('\n'), InputChar::Enter));
        assert!(matches!(classify_char('\r'), InputChar::Enter));
        assert!(matches!(classify_char('\x7f'), InputChar::Backspace));
        assert!(matches!(classify_char('\x08'), InputChar::Backspace));
        assert!(matches!(classify_char('\x03'), InputChar::CtrlC));
        assert!(matches!(classify_char('\t'), InputChar::Tab));
    }

    /// Test suggestion filtering and limiting
    #[tokio::test]
    async fn test_suggestion_generation() {
        let suggestions = get_instant_suggestions("/bal").await;

        // Should find balance-related suggestions
        assert!(!suggestions.is_empty());

        let balance_suggestion = suggestions.iter().find(|s| s.text.contains("balance"));
        assert!(balance_suggestion.is_some());

        // Check suggestion structure
        if let Some(suggestion) = balance_suggestion {
            assert!(!suggestion.description.is_empty());
            assert!(!suggestion.category.is_empty());
        }
    }

    /// Test suggestion limiting
    #[tokio::test]
    async fn test_suggestion_limiting() {
        let suggestions = get_instant_suggestions("/").await;

        // Should not exceed maximum suggestion count
        assert!(suggestions.len() <= 8); // Default max_suggestions
    }

    /// Test configuration validation
    #[test]
    fn test_input_config_validation() {
        let config = InputConfig::default();

        // Validate reasonable defaults
        assert!(config.max_history_size > 0);
        assert!(config.debounce_ms > 0);
        assert!(config.max_suggestions > 0);
        assert!(config.max_suggestions <= 20); // Reasonable upper bound
        assert!(config.debounce_ms <= 1000); // Not too slow
    }

    /// Test arrow key parsing
    #[test]
    fn test_arrow_key_parsing() {
        // This would test the arrow key parsing logic
        // In a real implementation, we'd need to mock the stdin reading
        let up_sequence = b"\x1b[A";
        let down_sequence = b"\x1b[B";
        let left_sequence = b"\x1b[D";
        let right_sequence = b"\x1b[C";

        // Test that sequences are correctly identified
        assert_eq!(up_sequence[2], b'A');
        assert_eq!(down_sequence[2], b'B');
        assert_eq!(left_sequence[2], b'D');
        assert_eq!(right_sequence[2], b'C');
    }

    /// Test error handling in terminal operations
    #[test]
    fn test_terminal_error_handling() {
        // Test that terminal operations handle errors gracefully
        // This would require mocking libc calls in a real implementation

        // For now, just test that the functions exist and can be called
        #[cfg(unix)]
        {
            // These functions should exist and be callable
            let _enable_result = crossterm_enable_raw_mode();
            let _disable_result = crossterm_disable_raw_mode();
        }
    }

    /// Test memory management for large inputs
    #[test]
    fn test_memory_management() {
        let mut state = InputState::new();

        // Test with very long input
        let long_input = "a".repeat(10000);
        state.input = long_input.clone();
        state.cursor_pos = long_input.len();

        // Should handle large inputs without issues
        assert_eq!(state.input.len(), 10000);
        assert_eq!(state.cursor_pos, 10000);

        // Test history cleanup with many entries
        for i in 0..200 {
            state.add_to_history(format!("command_{}", i));
        }

        // Should respect size limits
        assert!(state.command_history.len() <= state.config.max_history_size);
    }

    /// Test concurrent access safety
    #[tokio::test]
    async fn test_concurrent_suggestions() {
        let (tx, mut rx) = mpsc::unbounded_channel();

        // Simulate concurrent suggestion requests
        let inputs = vec!["bal", "trans", "stake", "price"];

        for input in inputs {
            tx.send(input.to_string()).unwrap();
        }

        // Should handle multiple requests without panicking
        let mut received = 0;
        while let Ok(_) = rx.try_recv() {
            received += 1;
        }

        assert_eq!(received, 4);
    }

    /// Mock function for testing character classification without stdin
    fn classify_char(ch: char) -> InputChar {
        match ch {
            '\n' | '\r' => InputChar::Enter,
            '\x7f' | '\x08' => InputChar::Backspace,
            '\x03' => InputChar::CtrlC,
            '\t' => InputChar::Tab,
            '\x1b' => InputChar::Arrow(ArrowKey::Up), // Simplified for testing
            ch if ch.is_ascii() && !ch.is_control() => InputChar::Regular(ch),
            _ => InputChar::Unknown,
        }
    }

    /// Integration test for complete input flow
    #[tokio::test]
    async fn test_input_flow_integration() {
        let mut state = InputState::new();
        let (tx, _rx) = mpsc::unbounded_channel();

        // Simulate typing "/balance"
        let input_chars = vec!['/', 'b', 'a', 'l', 'a', 'n', 'c', 'e'];

        for ch in input_chars {
            if let Err(e) = handle_regular_character(&mut state, ch, &tx).await {
                panic!("Input handling failed: {}", e);
            }
        }

        assert_eq!(state.input, "/balance");
        assert_eq!(state.cursor_pos, 8);
    }

    /// Test error recovery and cleanup
    #[tokio::test]
    async fn test_error_recovery() {
        let mut state = InputState::new();

        // Test that state remains consistent after errors
        state.input = "test".to_string();
        state.cursor_pos = 4;

        // Simulate error condition and recovery
        // For testing purposes, we'll simulate a ctrl-c error
        let result: Result<()> = Err(anyhow::anyhow!("Ctrl-C detected"));
        assert!(result.is_err());

        // State should remain accessible for cleanup
        assert_eq!(state.input, "test");
    }
}

/// Benchmarks for performance testing
#[cfg(test)]
mod benchmarks {
    use super::super::agent_chat::*;
    use std::time::Instant;

    /// Benchmark suggestion generation performance
    #[tokio::test]
    async fn bench_suggestion_generation() {
        let start = Instant::now();

        for _ in 0..100 {
            let _suggestions = get_instant_suggestions("/bal").await;
        }

        let duration = start.elapsed();
        println!("100 suggestion generations took: {:?}", duration);

        // Should complete within reasonable time
        assert!(duration.as_millis() < 1000);
    }

    /// Benchmark history management performance
    #[test]
    fn bench_history_management() {
        let mut state = InputState::new();
        let start = Instant::now();

        for i in 0..1000 {
            state.add_to_history(format!("command_{}", i));
        }

        let duration = start.elapsed();
        println!("1000 history additions took: {:?}", duration);

        // Should complete quickly
        assert!(duration.as_millis() < 100);
    }
}