rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::RequestHandler;
use rmux_core::PaneId;
use rmux_proto::{
    ErrorResponse, KillPaneRequest, LinkWindowRequest, NewSessionRequest, NewWindowRequest,
    PaneTarget, Request, Response, RmuxError, SessionName, SplitDirection, SplitWindowRequest,
    SplitWindowTarget, TerminalSize, WindowTarget,
};

fn session_name(value: &str) -> SessionName {
    SessionName::new(value).expect("valid session name")
}

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

async fn split(handler: &RequestHandler, session: &SessionName) {
    let response = handler
        .handle(Request::SplitWindow(SplitWindowRequest {
            target: SplitWindowTarget::Session(session.clone()),
            direction: SplitDirection::Vertical,
            before: false,
            environment: None,
        }))
        .await;
    assert!(matches!(response, Response::SplitWindow(_)), "{response:?}");
}

async fn create_window(handler: &RequestHandler, session: &SessionName, index: u32) {
    let response = handler
        .handle(Request::NewWindow(Box::new(NewWindowRequest {
            target: session.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 link_window(handler: &RequestHandler, owner: &SessionName, alias: &SessionName) {
    let response = handler
        .handle(Request::LinkWindow(LinkWindowRequest {
            source: WindowTarget::with_window(owner.clone(), 0),
            target: WindowTarget::with_window(alias.clone(), 0),
            after: false,
            before: false,
            kill_destination: true,
            detached: true,
        }))
        .await;
    assert!(matches!(response, Response::LinkWindow(_)), "{response:?}");
}

fn pane_ids(state: &crate::pane_terminals::HandlerState, session: &SessionName) -> Vec<PaneId> {
    state
        .sessions
        .session(session)
        .expect("session exists")
        .window_at(0)
        .expect("window exists")
        .panes()
        .iter()
        .map(|pane| pane.id())
        .collect()
}

#[tokio::test]
async fn linked_pane_kill_from_alias_updates_owner_and_alias() {
    let handler = RequestHandler::new();
    let owner = create_session(&handler, "linked-kill-owner").await;
    split(&handler, &owner).await;
    let alias = create_session(&handler, "linked-kill-alias").await;
    link_window(&handler, &owner, &alias).await;
    handler.wait_for_initial_panes_for_test().await;

    let removed_pane_id = {
        let state = handler.state.lock().await;
        pane_ids(&state, &owner)[1]
    };
    let response = handler
        .handle(Request::KillPane(KillPaneRequest {
            target: PaneTarget::with_window(alias.clone(), 0, 1),
            kill_all_except: false,
        }))
        .await;
    assert!(matches!(response, Response::KillPane(_)), "{response:?}");

    let state = handler.state.lock().await;
    let owner_ids = pane_ids(&state, &owner);
    assert_eq!(owner_ids, pane_ids(&state, &alias));
    assert_eq!(owner_ids.len(), 1);
    assert_ne!(owner_ids[0], removed_pane_id);
    state
        .ensure_window_panes_exist(&alias, 0, &owner_ids)
        .expect("linked alias resolves the surviving owner runtime");
}

#[tokio::test]
async fn linked_pane_kill_all_except_from_owner_updates_every_alias() {
    let handler = RequestHandler::new();
    let owner = create_session(&handler, "linked-kill-all-owner").await;
    split(&handler, &owner).await;
    split(&handler, &owner).await;
    let alias = create_session(&handler, "linked-kill-all-alias").await;
    link_window(&handler, &owner, &alias).await;
    handler.wait_for_initial_panes_for_test().await;

    let kept_pane_id = {
        let state = handler.state.lock().await;
        pane_ids(&state, &owner)[1]
    };
    let response = handler
        .handle(Request::KillPane(KillPaneRequest {
            target: PaneTarget::with_window(owner.clone(), 0, 1),
            kill_all_except: true,
        }))
        .await;
    assert!(matches!(response, Response::KillPane(_)), "{response:?}");

    let state = handler.state.lock().await;
    assert_eq!(pane_ids(&state, &owner), vec![kept_pane_id]);
    assert_eq!(pane_ids(&state, &alias), vec![kept_pane_id]);
    state
        .ensure_window_panes_exist(&alias, 0, &[kept_pane_id])
        .expect("the kept pane runtime remains reachable through the alias");
}

#[tokio::test]
async fn linked_alias_kill_resize_rollback_restores_shared_runtime() {
    let handler = RequestHandler::new();
    let owner = create_session(&handler, "linked-rollback-owner").await;
    split(&handler, &owner).await;
    let alias = create_session(&handler, "linked-rollback-alias").await;
    link_window(&handler, &owner, &alias).await;
    handler.wait_for_initial_panes_for_test().await;

    let (pane_id, pane_pid) = {
        let mut state = handler.state.lock().await;
        let pane_id = pane_ids(&state, &owner)[1];
        let pane_pid = state
            .pane_pid_in_window(&owner, 0, 1)
            .expect("second pane has a runtime process");
        assert!(state
            .toggle_marked_pane(&PaneTarget::with_window(owner.clone(), 0, 1))
            .expect("pane can be marked"));
        state.fail_next_resize_for_test();
        (pane_id, pane_pid)
    };

    let response = handler
        .handle(Request::KillPane(KillPaneRequest {
            target: PaneTarget::with_window(alias.clone(), 0, 1),
            kill_all_except: false,
        }))
        .await;
    assert_eq!(
        response,
        Response::Error(ErrorResponse {
            error: RmuxError::Server("injected pane terminal resize failure".to_owned()),
        })
    );

    let state = handler.state.lock().await;
    for session in [&owner, &alias] {
        assert!(pane_ids(&state, session).contains(&pane_id));
    }
    state
        .ensure_window_panes_exist(&alias, 0, &[pane_id])
        .expect("rollback restores the owner runtime for the alias");
    state
        .pane_output_for_target(&alias, 0, 1)
        .expect("rollback restores pane output for the alias");
    assert!(state.pane_is_marked(&PaneTarget::with_window(owner.clone(), 0, 1)));
    assert_eq!(
        state
            .pane_pid_in_window(&alias, 0, 1)
            .expect("restored pane remains inspectable"),
        pane_pid
    );
}

#[tokio::test]
async fn linked_last_pane_kill_removes_shared_window_from_all_surviving_sessions() {
    let handler = RequestHandler::new();
    let owner = create_session(&handler, "linked-last-owner").await;
    create_window(&handler, &owner, 1).await;
    let alias = create_session(&handler, "linked-last-alias").await;
    create_window(&handler, &alias, 1).await;
    link_window(&handler, &owner, &alias).await;
    handler.wait_for_initial_panes_for_test().await;

    let response = handler
        .handle(Request::KillPane(KillPaneRequest {
            target: PaneTarget::with_window(alias.clone(), 0, 0),
            kill_all_except: false,
        }))
        .await;
    assert!(matches!(response, Response::KillPane(_)), "{response:?}");

    let state = handler.state.lock().await;
    for session_name in [&owner, &alias] {
        let session = state
            .sessions
            .session(session_name)
            .expect("session survives");
        assert!(session.window_at(0).is_none());
        assert!(session.window_at(1).is_some());
    }
}

#[tokio::test]
async fn linked_last_pane_kill_destroys_only_alias_with_no_surviving_window() {
    let handler = RequestHandler::new();
    let owner = create_session(&handler, "linked-last-owner-survivor").await;
    create_window(&handler, &owner, 1).await;
    let alias = create_session(&handler, "linked-last-only-alias").await;
    link_window(&handler, &owner, &alias).await;
    handler.wait_for_initial_panes_for_test().await;

    let response = handler
        .handle(Request::KillPane(KillPaneRequest {
            target: PaneTarget::with_window(alias.clone(), 0, 0),
            kill_all_except: false,
        }))
        .await;
    assert!(matches!(response, Response::KillPane(_)), "{response:?}");

    let state = handler.state.lock().await;
    assert!(state.sessions.session(&alias).is_none());
    let owner_session = state.sessions.session(&owner).expect("owner survives");
    assert!(owner_session.window_at(0).is_none());
    assert!(owner_session.window_at(1).is_some());
}