rmux-server 0.1.1

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

#[tokio::test]
async fn move_window_with_d_keeps_the_next_window_active_when_moving_the_current_slot() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    create_session(&handler, "alpha").await;
    insert_window(&handler, &alpha, 2).await;

    {
        let mut state = handler.state.lock().await;
        let session = state
            .sessions
            .session_mut(&alpha)
            .expect("alpha should exist");
        session.select_window(2).expect("window 2 select succeeds");
        session.select_window(0).expect("window 0 select succeeds");
    }

    let response = handler
        .handle(Request::MoveWindow(MoveWindowRequest {
            source: Some(WindowTarget::with_window(alpha.clone(), 0)),
            target: MoveWindowTarget::Window(WindowTarget::with_window(alpha.clone(), 4)),
            renumber: false,
            kill_destination: false,
            detached: true,
        }))
        .await;

    assert_eq!(
        response,
        Response::MoveWindow(rmux_proto::MoveWindowResponse {
            session_name: alpha.clone(),
            target: Some(WindowTarget::with_window(alpha.clone(), 4)),
        })
    );

    let state = handler.state.lock().await;
    let session = state.sessions.session(&alpha).expect("alpha should exist");
    assert_eq!(
        session.windows().keys().copied().collect::<Vec<_>>(),
        vec![2, 4]
    );
    assert_eq!(session.active_window_index(), 2);
    assert_eq!(session.last_window_index(), None);
}

#[tokio::test]
async fn swap_window_same_source_and_destination_is_a_noop() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    create_session(&handler, "alpha").await;
    insert_window(&handler, &alpha, 2).await;

    let response = handler
        .handle(Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(alpha.clone(), 2),
            detached: false,
        }))
        .await;

    assert_eq!(
        response,
        Response::SwapWindow(rmux_proto::SwapWindowResponse {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(alpha.clone(), 2),
        })
    );
}

#[tokio::test]
async fn swap_window_without_d_preserves_active_slot() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    create_session(&handler, "alpha").await;
    insert_window(&handler, &alpha, 2).await;
    insert_window(&handler, &alpha, 5).await;

    {
        let mut state = handler.state.lock().await;
        let session = state
            .sessions
            .session_mut(&alpha)
            .expect("alpha should exist");
        session.select_window(5).expect("window 5 select succeeds");
        session.select_window(2).expect("window 2 select succeeds");
    }

    // Without -d, tmux preserves the active winlink. Here it already points to
    // index 2, so active remains 2 while the swapped content changes.
    let response = handler
        .handle(Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(alpha.clone(), 5),
            detached: false,
        }))
        .await;

    assert_eq!(
        response,
        Response::SwapWindow(rmux_proto::SwapWindowResponse {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(alpha.clone(), 5),
        })
    );

    let state = handler.state.lock().await;
    let session = state.sessions.session(&alpha).expect("alpha should exist");
    assert_eq!(session.active_window_index(), 2);
    assert_eq!(session.last_window_index(), Some(5));
}

#[tokio::test]
async fn swap_window_without_d_preserves_active_when_active_is_elsewhere() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    create_session(&handler, "alpha").await;
    insert_window(&handler, &alpha, 2).await;
    insert_window(&handler, &alpha, 5).await;

    // Active is at window 0 (default). Source=2, target=5.
    let response = handler
        .handle(Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(alpha.clone(), 5),
            detached: false,
        }))
        .await;

    assert!(matches!(response, Response::SwapWindow(_)));

    // Without -d, tmux preserves the active winlink at 0.
    let state = handler.state.lock().await;
    let session = state.sessions.session(&alpha).expect("alpha should exist");
    assert_eq!(session.active_window_index(), 0);
    assert_eq!(session.last_window_index(), None);
}

#[tokio::test]
async fn swap_window_with_d_selects_target_window_within_session() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    create_session(&handler, "alpha").await;
    insert_window(&handler, &alpha, 2).await;
    insert_window(&handler, &alpha, 5).await;

    // Active is at window 0 (default). Source=2, target=5.
    let response = handler
        .handle(Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(alpha.clone(), 5),
            detached: true,
        }))
        .await;

    assert!(matches!(response, Response::SwapWindow(_)));

    // With -d, tmux selects the destination winlink after swapping.
    let state = handler.state.lock().await;
    let session = state.sessions.session(&alpha).expect("alpha should exist");
    assert_eq!(session.active_window_index(), 5);
    assert_eq!(session.last_window_index(), Some(0));
}

#[tokio::test]
async fn move_window_reindex_rejects_source_when_renumber_is_set() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    create_session(&handler, "alpha").await;

    let response = handler
        .handle(Request::MoveWindow(MoveWindowRequest {
            source: Some(WindowTarget::with_window(alpha.clone(), 0)),
            target: MoveWindowTarget::Session(alpha.clone()),
            renumber: true,
            kill_destination: false,
            detached: false,
        }))
        .await;

    assert!(matches!(response, Response::Error(_)));
}

#[tokio::test]
async fn move_window_across_sessions_rejects_last_window_removal() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    let beta = session_name("beta");
    create_session(&handler, "alpha").await;
    create_session(&handler, "beta").await;

    let response = handler
        .handle(Request::MoveWindow(MoveWindowRequest {
            source: Some(WindowTarget::with_window(alpha.clone(), 0)),
            target: MoveWindowTarget::Window(WindowTarget::with_window(beta.clone(), 5)),
            renumber: false,
            kill_destination: false,
            detached: false,
        }))
        .await;

    assert!(matches!(response, Response::Error(_)));

    let state = handler.state.lock().await;
    let alpha_session = state.sessions.session(&alpha).expect("alpha should exist");
    assert_eq!(
        alpha_session.windows().keys().copied().collect::<Vec<_>>(),
        vec![0]
    );
}

#[tokio::test]
async fn swap_window_rejects_cross_session_swap_within_same_session_group() {
    let handler = RequestHandler::new();
    create_session(&handler, "alpha").await;
    let alpha = session_name("alpha");
    let beta = session_name("beta");

    // Create beta as a grouped session in the same group as alpha.
    {
        let mut state = handler.state.lock().await;
        state
            .sessions
            .create_grouped_session_with_base_index(
                beta.clone(),
                TerminalSize { cols: 80, rows: 24 },
                0,
                alpha.clone(),
            )
            .expect("grouped session creation succeeds");
    }

    let response = handler
        .handle(Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(alpha.clone(), 0),
            target: WindowTarget::with_window(beta.clone(), 0),
            detached: false,
        }))
        .await;

    assert!(
        matches!(&response, Response::Error(e) if e.error.to_string().contains("sessions are grouped")),
        "expected session-group guard error, got {response:?}"
    );
}

#[tokio::test]
async fn swap_window_allows_cross_session_swap_between_different_groups() {
    let handler = RequestHandler::new();
    create_session(&handler, "alpha").await;
    create_session(&handler, "beta").await;
    let alpha = session_name("alpha");
    let beta = session_name("beta");
    insert_window(&handler, &alpha, 1).await;
    insert_window(&handler, &beta, 1).await;

    let response = handler
        .handle(Request::SwapWindow(SwapWindowRequest {
            source: WindowTarget::with_window(alpha.clone(), 0),
            target: WindowTarget::with_window(beta.clone(), 0),
            detached: false,
        }))
        .await;

    assert!(
        matches!(response, Response::SwapWindow(_)),
        "expected swap success between ungrouped sessions, got {response:?}"
    );
}