rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::attach_support::ActiveAttachIdentity;
use super::{with_expected_attach_and_session_identity, RequestHandler};
use rmux_proto::{
    KillPaneRequest, KillSessionRequest, LinkWindowRequest, MoveWindowRequest, MoveWindowTarget,
    NewSessionRequest, NewWindowRequest, PaneTarget, Request, RespawnPaneRequest,
    RespawnWindowRequest, Response, SessionId, SessionName, SwapWindowRequest, TerminalSize,
    UnlinkWindowRequest, WindowTarget,
};

async fn create_session(handler: &RequestHandler, value: &str) -> SessionName {
    let session_name = SessionName::new(value).expect("valid session name");
    let response = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: session_name.clone(),
            detached: true,
            size: Some(TerminalSize { cols: 80, rows: 24 }),
            environment: None,
        }))
        .await;
    assert!(matches!(response, Response::NewSession(_)), "{response:?}");
    session_name
}

async fn create_window(handler: &RequestHandler, session_name: &SessionName, index: u32) {
    let response = handler
        .handle(Request::NewWindow(Box::new(NewWindowRequest {
            target: session_name.clone(),
            name: None,
            detached: true,
            environment: None,
            command: None,
            start_directory: None,
            target_window_index: Some(index),
            insert_at_target: false,
            process_command: None,
        })))
        .await;
    assert!(matches!(response, Response::NewWindow(_)), "{response:?}");
}

async fn session_id(handler: &RequestHandler, session_name: &SessionName) -> SessionId {
    handler
        .state
        .lock()
        .await
        .sessions
        .session(session_name)
        .expect("session exists")
        .id()
}

async fn replace_session(handler: &RequestHandler, session_name: &SessionName) -> SessionId {
    let stale_id = session_id(handler, session_name).await;
    let killed = handler
        .handle(Request::KillSession(KillSessionRequest {
            target: session_name.clone(),
            kill_all_except_target: false,
            clear_alerts: false,
            kill_group: false,
        }))
        .await;
    assert!(matches!(killed, Response::KillSession(_)), "{killed:?}");
    let recreated = create_session(handler, session_name.as_str()).await;
    assert_eq!(&recreated, session_name);
    assert_ne!(session_id(handler, session_name).await, stale_id);
    stale_id
}

async fn run_as_stale_attached_session(
    handler: &RequestHandler,
    session_name: SessionName,
    stale_id: SessionId,
    request: Request,
) -> Response {
    with_expected_attach_and_session_identity(
        ActiveAttachIdentity::new(990_001, 1, stale_id),
        session_name,
        stale_id,
        handler.handle(request),
    )
    .await
}

#[tokio::test]
async fn stale_attached_session_cannot_respawn_replacement_window() {
    let handler = RequestHandler::new();
    let session_name = create_session(&handler, "identity-respawn-window").await;
    let stale_id = replace_session(&handler, &session_name).await;

    let response = run_as_stale_attached_session(
        &handler,
        session_name.clone(),
        stale_id,
        Request::RespawnWindow(Box::new(RespawnWindowRequest {
            target: WindowTarget::with_window(session_name.clone(), 0),
            kill: true,
            environment: None,
            command: None,
            start_directory: None,
        })),
    )
    .await;

    assert!(matches!(response, Response::Error(_)), "{response:?}");
    assert_ne!(session_id(&handler, &session_name).await, stale_id);
}

#[tokio::test]
async fn stale_attached_session_cannot_respawn_or_kill_replacement_pane() {
    let handler = RequestHandler::new();
    let session_name = create_session(&handler, "identity-pane-mutations").await;
    let stale_id = replace_session(&handler, &session_name).await;
    let target = PaneTarget::with_window(session_name.clone(), 0, 0);

    let respawn = run_as_stale_attached_session(
        &handler,
        session_name.clone(),
        stale_id,
        Request::RespawnPane(Box::new(RespawnPaneRequest {
            target: target.clone(),
            kill: true,
            start_directory: None,
            environment: None,
            command: None,
            process_command: None,
        })),
    )
    .await;
    assert!(matches!(respawn, Response::Error(_)), "{respawn:?}");

    let killed = run_as_stale_attached_session(
        &handler,
        session_name.clone(),
        stale_id,
        Request::KillPane(KillPaneRequest {
            target,
            kill_all_except: false,
        }),
    )
    .await;
    assert!(matches!(killed, Response::Error(_)), "{killed:?}");
    assert_ne!(session_id(&handler, &session_name).await, stale_id);
}

#[tokio::test]
async fn stale_attached_session_cannot_unlink_recreated_window_slot() {
    let handler = RequestHandler::new();
    let session_name = create_session(&handler, "identity-unlink-window").await;
    create_window(&handler, &session_name, 1).await;
    let stale_id = replace_session(&handler, &session_name).await;
    create_window(&handler, &session_name, 1).await;

    let response = run_as_stale_attached_session(
        &handler,
        session_name.clone(),
        stale_id,
        Request::UnlinkWindow(UnlinkWindowRequest {
            target: WindowTarget::with_window(session_name.clone(), 1),
            kill_if_last: true,
        }),
    )
    .await;

    assert!(matches!(response, Response::Error(_)), "{response:?}");
    let state = handler.state.lock().await;
    assert!(
        state
            .sessions
            .session(&session_name)
            .and_then(|session| session.window_at(1))
            .is_some(),
        "replacement window must survive the stale unlink"
    );
}

#[tokio::test]
async fn stale_attached_session_cannot_drive_cross_session_window_mutations() {
    let handler = RequestHandler::new();
    let attached = create_session(&handler, "identity-window-source").await;
    let other = create_session(&handler, "identity-window-other").await;
    let stale_id = replace_session(&handler, &attached).await;

    let move_response = run_as_stale_attached_session(
        &handler,
        attached.clone(),
        stale_id,
        Request::MoveWindow(MoveWindowRequest {
            source: Some(WindowTarget::with_window(attached.clone(), 0)),
            target: MoveWindowTarget::Window(WindowTarget::with_window(other.clone(), 1)),
            renumber: false,
            kill_destination: false,
            detached: true,
            after: false,
            before: false,
        }),
    )
    .await;
    assert!(
        matches!(move_response, Response::Error(_)),
        "{move_response:?}"
    );

    let swap_response = run_as_stale_attached_session(
        &handler,
        attached.clone(),
        stale_id,
        Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(attached.clone(), 0),
            target: WindowTarget::with_window(other.clone(), 0),
            detached: true,
        }),
    )
    .await;
    assert!(
        matches!(swap_response, Response::Error(_)),
        "{swap_response:?}"
    );

    let link_response = run_as_stale_attached_session(
        &handler,
        attached.clone(),
        stale_id,
        Request::LinkWindow(LinkWindowRequest {
            source: WindowTarget::with_window(other, 0),
            target: WindowTarget::with_window(attached.clone(), 0),
            after: false,
            before: false,
            kill_destination: true,
            detached: true,
        }),
    )
    .await;
    assert!(
        matches!(link_response, Response::Error(_)),
        "{link_response:?}"
    );
    assert_ne!(session_id(&handler, &attached).await, stale_id);
}

#[tokio::test]
async fn attached_queue_can_still_mutate_explicit_other_sessions() {
    let handler = RequestHandler::new();
    let attached = create_session(&handler, "identity-explicit-attached").await;
    let source = create_session(&handler, "identity-explicit-source").await;
    let destination = create_session(&handler, "identity-explicit-destination").await;
    let attached_id = session_id(&handler, &attached).await;

    let response = with_expected_attach_and_session_identity(
        ActiveAttachIdentity::new(990_002, 1, attached_id),
        attached,
        attached_id,
        handler.handle(Request::LinkWindow(LinkWindowRequest {
            source: WindowTarget::with_window(source, 0),
            target: WindowTarget::with_window(destination.clone(), 1),
            after: false,
            before: false,
            kill_destination: false,
            detached: true,
        })),
    )
    .await;

    assert!(matches!(response, Response::LinkWindow(_)), "{response:?}");
    assert!(handler
        .state
        .lock()
        .await
        .sessions
        .session(&destination)
        .and_then(|session| session.window_at(1))
        .is_some());
}