rmux-server 0.1.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::*;

#[tokio::test]
async fn attached_client_flags_keep_tmux_order_for_extended_flag_sets() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let (control_tx, _control_rx) = mpsc::unbounded_channel();
    let _attach_id = handler
        .register_attach_with_terminal_context(
            requester_pid,
            alpha,
            control_tx,
            crate::outer_terminal::OuterTerminalContext::default().with_client_terminal(
                &rmux_proto::ClientTerminalContext {
                    terminal_features: Vec::new(),
                    utf8: true,
                },
            ),
        )
        .await;

    {
        let mut active_attach = handler.active_attach.lock().await;
        let active = active_attach
            .by_pid
            .get_mut(&requester_pid)
            .expect("attached client exists");
        active.flags.insert(super::super::ClientFlags::IGNORESIZE);
        active
            .flags
            .insert(super::super::ClientFlags::NO_DETACH_ON_DESTROY);
        active.flags.insert(super::super::ClientFlags::READONLY);
        active.flags.insert(super::super::ClientFlags::ACTIVEPANE);
        active.suspended = true;
    }

    let active_attach = handler.active_attach.lock().await;
    let active = active_attach
        .by_pid
        .get(&requester_pid)
        .expect("attached client exists");
    assert_eq!(
        super::super::format_attached_client_flags(active),
        "attached,ignore-size,no-detach-on-destroy,read-only,active-pane,suspended,UTF-8"
    );
}

#[tokio::test]
async fn control_client_flags_keep_tmux_order_for_extended_flag_sets() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    let _control_id = handler
        .register_control_with_closing(
            requester_pid,
            ControlModeUpgrade {
                mode: ControlMode::Plain,
                terminal_context: crate::outer_terminal::OuterTerminalContext::default()
                    .with_client_terminal(&rmux_proto::ClientTerminalContext {
                        terminal_features: Vec::new(),
                        utf8: true,
                    }),
            },
            event_tx,
            Arc::new(AtomicBool::new(false)),
        )
        .await;
    handler
        .set_control_session(requester_pid, Some(alpha))
        .await
        .expect("set control session");

    {
        let mut active_control = handler.active_control.lock().await;
        let active = active_control
            .by_pid
            .get_mut(&requester_pid)
            .expect("control client exists");
        active.flags.no_output = true;
        active.flags.wait_exit = true;
        active.flags.pause_after_millis = Some(3_000);
    }

    let active_control = handler.active_control.lock().await;
    let active = active_control
        .by_pid
        .get(&requester_pid)
        .expect("control client exists");
    assert_eq!(
        super::super::format_control_client_flags(active),
        "attached,focused,control-mode,no-output,wait-exit,pause-after=3,UTF-8"
    );
}

#[tokio::test]
async fn control_client_flags_without_session_emit_only_control_mode() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    let _control_id = handler
        .register_control_with_closing(
            requester_pid,
            ControlModeUpgrade {
                mode: ControlMode::Plain,
                terminal_context: crate::outer_terminal::OuterTerminalContext::default()
                    .with_client_terminal(&rmux_proto::ClientTerminalContext {
                        terminal_features: Vec::new(),
                        utf8: true,
                    }),
            },
            event_tx,
            Arc::new(AtomicBool::new(false)),
        )
        .await;

    let active_control = handler.active_control.lock().await;
    let active = active_control
        .by_pid
        .get(&requester_pid)
        .expect("control client exists");
    assert_eq!(
        super::super::format_control_client_flags(active),
        "control-mode"
    );
}

#[tokio::test]
async fn list_clients_exposes_pid_and_tty_format_variables_for_attached_clients() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let (control_tx, _control_rx) = mpsc::unbounded_channel();
    handler
        .register_attach(requester_pid, alpha.clone(), control_tx)
        .await;

    let response = handler
        .handle(Request::ListClients(rmux_proto::ListClientsRequest {
            format: Some("#{client_name}|#{client_pid}|#{client_tty}".to_owned()),
            target_session: None,
            filter: None,
            sort_order: None,
            reversed: false,
        }))
        .await;
    let Response::ListClients(response) = response else {
        panic!("expected list-clients response");
    };
    let output = String::from_utf8(response.output.stdout().to_vec()).expect("utf-8");
    let line = output.lines().next().expect("client line");
    let parts = line.split('|').collect::<Vec<_>>();
    assert_eq!(parts.len(), 3);
    assert_eq!(parts[1], requester_pid.to_string());
    #[cfg(unix)]
    assert!(!parts[2].is_empty(), "client_tty should be populated");
    #[cfg(windows)]
    assert_eq!(parts[2], "");
}

#[tokio::test]
async fn attach_session_returns_an_upgrade_response_for_existing_sessions() {
    let handler = RequestHandler::new();
    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: session_name("alpha"),
            detached: true,
            size: None,

            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    assert_eq!(
        handler
            .handle(Request::AttachSession(rmux_proto::AttachSessionRequest {
                target: session_name("alpha"),
            }))
            .await,
        Response::AttachSession(rmux_proto::AttachSessionResponse {
            session_name: session_name("alpha"),
        })
    );
}

#[tokio::test]
async fn attach_session_dispatch_populates_the_upgrade_field() {
    let handler = RequestHandler::new();
    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: session_name("alpha"),
            detached: true,
            size: None,

            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let outcome = handler
        .dispatch(
            std::process::id(),
            Request::AttachSession(rmux_proto::AttachSessionRequest {
                target: session_name("alpha"),
            }),
        )
        .await;

    assert!(
        matches!(outcome.response, Response::AttachSession(_)),
        "response should be AttachSession"
    );
    assert!(
        outcome.attach.is_some(),
        "dispatch must populate the attach upgrade field"
    );
}

#[tokio::test]
async fn attach_session_to_missing_session_returns_session_not_found() {
    let handler = RequestHandler::new();

    let outcome = handler
        .dispatch(
            std::process::id(),
            Request::AttachSession(rmux_proto::AttachSessionRequest {
                target: session_name("missing"),
            }),
        )
        .await;

    assert_eq!(
        outcome.response,
        Response::Error(ErrorResponse {
            error: RmuxError::SessionNotFound("missing".to_owned()),
        })
    );
    assert!(
        outcome.attach.is_none(),
        "attach field must be None for missing sessions"
    );
}

#[tokio::test]
async fn switch_client_requires_an_attached_client() {
    let handler = RequestHandler::new();
    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: session_name("alpha"),
            detached: true,
            size: None,

            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    assert_eq!(
        handler
            .handle(Request::SwitchClient(rmux_proto::SwitchClientRequest {
                target: session_name("alpha"),
            }))
            .await,
        Response::Error(rmux_proto::ErrorResponse {
            error: RmuxError::Server("switch-client requires an attached client".to_owned()),
        })
    );
}

#[tokio::test]
async fn detach_client_requires_an_attached_client() {
    let handler = RequestHandler::new();

    assert_eq!(
        handler
            .handle(Request::DetachClient(rmux_proto::DetachClientRequest))
            .await,
        Response::Error(rmux_proto::ErrorResponse {
            error: RmuxError::Server("detach-client requires an attached client".to_owned()),
        })
    );
}