redshank-cli 0.2.2

CLI entry point for the Redshank investigation agent
#![cfg(unix)]
#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::panic)]
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use std::io::{Read, Write};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

#[test]
fn tui_subcommand_exits_cleanly_on_quit_command() {
    let pty_system = native_pty_system();
    let pair = pty_system
        .openpty(PtySize {
            rows: 24,
            cols: 80,
            pixel_width: 0,
            pixel_height: 0,
        })
        .expect("open PTY pair");

    let mut cmd = CommandBuilder::new(env!("CARGO_BIN_EXE_redshank"));
    cmd.arg("tui");
    cmd.env("RUST_LOG", "error");

    let mut child = pair.slave.spawn_command(cmd).expect("spawn redshank tui");
    drop(pair.slave);

    let mut reader = pair.master.try_clone_reader().expect("clone PTY reader");
    let mut writer = pair.master.take_writer().expect("take PTY writer");
    let output = Arc::new(Mutex::new(Vec::new()));
    let output_reader = Arc::clone(&output);

    thread::spawn(move || {
        let mut buf = [0_u8; 1024];
        loop {
            match reader.read(&mut buf) {
                Ok(0) | Err(_) => break,
                Ok(n) => output_reader
                    .lock()
                    .expect("lock PTY output")
                    .extend_from_slice(&buf[..n]),
            }
        }
    });

    let startup_deadline = Instant::now() + Duration::from_secs(2);
    while Instant::now() < startup_deadline {
        if !output.lock().expect("lock PTY output").is_empty() {
            break;
        }
        thread::sleep(Duration::from_millis(20));
    }

    for byte in b"/quit\r" {
        writer
            .write_all(std::slice::from_ref(byte))
            .expect("write quit command byte");
        writer.flush().expect("flush quit command byte");
        thread::sleep(Duration::from_millis(30));
    }
    writer.flush().expect("flush quit command");
    drop(writer);

    let (status_tx, status_rx) = mpsc::channel();
    thread::spawn(move || {
        let status = child.wait().expect("wait for tui process");
        let _ = status_tx.send(status);
    });

    let status = status_rx
        .recv_timeout(Duration::from_secs(3))
        .unwrap_or_else(|_| {
            let captured = output.lock().expect("lock PTY output").clone();
            let output = String::from_utf8_lossy(&captured);
            panic!("tui process should exit after /quit; PTY output: {output}");
        });

    assert!(
        status.success(),
        "tui process exited unsuccessfully: {status:?}"
    );
}