ssh-cli 0.5.2

Native Rust CLI that gives LLMs (Claude Code, Cursor, Windsurf) the ability to operate remote servers via SSH over stdin/stdout
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// G-ERR-12: unit tests for ssh client (split from monĂ³lito client.rs).
#![allow(clippy::unwrap_used, clippy::expect_used)]
    use super::*;
    use secrecy::SecretString;

    fn valid_cfg() -> ConnectionConfig {
        use crate::domain::{SshHost, SshPort, SshUser, TimeoutMs};
        ConnectionConfig {
            host: SshHost::try_new("127.0.0.1").unwrap(),
            port: SshPort::try_new(22).unwrap(),
            username: SshUser::try_new("root").unwrap(),
            password: SecretString::from("senha-exemplo".to_string()),
            key_path: None,
            key_passphrase: None,
            timeout_ms: TimeoutMs::try_new(5000).unwrap(),
            known_hosts_path: None,
            replace_host_key: false,
            tls: None,
            use_agent: false,
            agent_socket: None,
        }
    }
    #[test]
    fn domain_rejects_empty_host() {
        assert!(crate::domain::SshHost::try_new("").is_err());
        assert!(crate::domain::SshHost::try_new("   ").is_err());
    }

    #[test]
    fn validate_requires_auth_material() {
        let mut c = valid_cfg();
        c.password = SecretString::from(String::new());
        c.key_path = None;
        let r = c.validate();
        assert!(r.is_err());
        let msg = r.unwrap_err().to_string();
        assert!(msg.contains("auth") || msg.contains("password") || msg.contains("key"));
    }

    #[test]
    fn domain_rejects_port_zero_and_empty_user() {
        assert!(crate::domain::SshPort::try_new(0).is_err());
        assert!(crate::domain::SshUser::try_new("").is_err());
    }

    #[test]
    fn validate_correct_config_returns_ok() {
        assert!(valid_cfg().validate().is_ok());
    }

    #[test]
    fn debug_does_not_expose_password() {
        let c = valid_cfg();
        let dbg = format!("{c:?}");
        assert!(!dbg.contains("senha-exemplo"));
        assert!(dbg.contains("redacted"));
    }

    #[test]
    fn exec_capture_byte_cap_scales_and_hard_caps() {
        assert_eq!(exec_capture_byte_cap(0), 0);
        assert_eq!(exec_capture_byte_cap(10), 10 * 4 + 4);
        assert_eq!(
            exec_capture_byte_cap(usize::MAX),
            EXEC_CAPTURE_HARD_MAX_BYTES
        );
        assert_eq!(
            exec_capture_byte_cap(EXEC_CAPTURE_HARD_MAX_BYTES),
            EXEC_CAPTURE_HARD_MAX_BYTES
        );
    }

    #[test]
    fn append_capped_respects_limit() {
        let mut buf = Vec::new();
        let mut truncated = false;
        append_capped(&mut buf, b"hello", 3, &mut truncated);
        assert_eq!(buf, b"hel");
        assert!(truncated);
        truncated = false;
        append_capped(&mut buf, b"xx", 3, &mut truncated);
        assert_eq!(buf, b"hel");
        assert!(truncated);
    }

    #[test]
    fn execution_output_debug_does_not_crash() {
        let s = ExecutionOutput {
            stdout: "ok".into(),
            stderr: String::new(),
            exit_code: Some(0),
            truncated_stdout: false,
            truncated_stderr: false,
            duration_ms: 42,
        };
        let _ = format!("{s:?}");
    }

    #[test]
    fn duration_ms_type_compatible() {
        // Static guarantee that Instant::elapsed fits in u64.
        let fake: u64 = 1234;
        assert_eq!(fake, 1234_u64);
    }