rmux-server 0.10.0

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

use rmux_proto::{LinkWindowRequest, WindowTarget};

use crate::pane_terminals::WindowLinkOccurrenceId;

use super::super::mode_tree_model::{ChooseTreeTarget, ModeTreeActionIdentity};
use super::super::mode_tree_order::{pane_item_id, window_item_id};

struct LinkedOccurrenceFixture {
    session_name: SessionName,
    attach_pid: u32,
    attach_id: u64,
    session_id: rmux_proto::SessionId,
    window_id: rmux_proto::WindowId,
    pane_id: rmux_proto::PaneId,
    pane_output_generation: u64,
    old_occurrence_id: WindowLinkOccurrenceId,
    old_window_item_id: String,
    _control_rx: mpsc::UnboundedReceiver<crate::pane_io::AttachControl>,
}

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

    let (session_id, window_id, pane_id, pane_output_generation, old_occurrence_id) = {
        let state = handler.state.lock().await;
        let session = state
            .sessions
            .session(&session_name)
            .expect("session exists");
        let window = session.window_at(2).expect("linked occurrence exists");
        let pane = window.active_pane().expect("active pane exists");
        (
            session.id(),
            window.id(),
            pane.id(),
            state.pane_output_generation_for_target(
                &rmux_proto::PaneTarget::with_window(session_name.clone(), 2, pane.index()),
                pane.id(),
            ),
            state
                .window_link_occurrence_id(&session_name, 2)
                .expect("linked occurrence has an identity"),
        )
    };
    let old_window_item_id = window_item_id(session_id, 2, window_id, old_occurrence_id);

    let attach_pid = std::process::id().saturating_add(attach_pid_offset);
    let (control_tx, control_rx) = mpsc::unbounded_channel();
    let attach_id = handler
        .register_attach(attach_pid, session_name.clone(), control_tx)
        .await;
    let parsed = CommandParser::new()
        .parse_arguments(["choose-tree", "-w"])
        .expect("choose-tree parses");
    let command = RequestHandler::parse_mode_tree_queue_command(parsed.commands()[0].clone())
        .expect("mode-tree command parses")
        .expect("mode-tree command recognized");
    handler
        .execute_queued_mode_tree(
            attach_pid,
            command,
            &QueueExecutionContext::without_caller_cwd(),
        )
        .await
        .expect("mode-tree opens");

    LinkedOccurrenceFixture {
        session_name,
        attach_pid,
        attach_id,
        session_id,
        window_id,
        pane_id,
        pane_output_generation,
        old_occurrence_id,
        old_window_item_id,
        _control_rx: control_rx,
    }
}

async fn link_replacement_occurrence(handler: &RequestHandler, session_name: &SessionName) {
    let response = handler
        .handle(Request::LinkWindow(LinkWindowRequest {
            source: WindowTarget::with_window(session_name.clone(), 0),
            target: WindowTarget::with_window(session_name.clone(), 2),
            after: false,
            before: false,
            kill_destination: false,
            detached: true,
        }))
        .await;
    assert!(matches!(response, Response::LinkWindow(_)), "{response:?}");
}

async fn replace_linked_occurrence(
    handler: &RequestHandler,
    fixture: &LinkedOccurrenceFixture,
) -> WindowLinkOccurrenceId {
    let mut state = handler.state.lock().await;
    state
        .unlink_window(
            WindowTarget::with_window(fixture.session_name.clone(), 2),
            false,
        )
        .expect("old occurrence unlinks");
    state
        .link_window(LinkWindowRequest {
            source: WindowTarget::with_window(fixture.session_name.clone(), 0),
            target: WindowTarget::with_window(fixture.session_name.clone(), 2),
            after: false,
            before: false,
            kill_destination: false,
            detached: true,
        })
        .expect("replacement occurrence links");
    let replacement = state
        .sessions
        .session(&fixture.session_name)
        .and_then(|session| session.window_at(2))
        .expect("replacement occurrence exists");
    assert_eq!(replacement.id(), fixture.window_id);
    let replacement_occurrence_id = state
        .window_link_occurrence_id(&fixture.session_name, 2)
        .expect("replacement occurrence has an identity");
    assert_ne!(replacement_occurrence_id, fixture.old_occurrence_id);
    replacement_occurrence_id
}

async fn set_mode_tree_selection(handler: &RequestHandler, attach_pid: u32, selected_id: String) {
    handler
        .active_attach
        .lock()
        .await
        .by_pid
        .get_mut(&attach_pid)
        .and_then(|active| active.mode_tree.as_mut())
        .expect("mode-tree remains active")
        .selected_id = Some(selected_id);
}

async fn assert_replacement_survives(
    handler: &RequestHandler,
    fixture: &LinkedOccurrenceFixture,
    occurrence_id: WindowLinkOccurrenceId,
) {
    let state = handler.state.lock().await;
    let window = state
        .sessions
        .session(&fixture.session_name)
        .and_then(|session| session.window_at(2))
        .expect("replacement occurrence survives");
    assert_eq!(window.id(), fixture.window_id);
    assert_eq!(
        state.window_link_occurrence_id(&fixture.session_name, 2),
        Some(occurrence_id)
    );
}

#[tokio::test]
async fn choose_tree_stale_window_action_rejects_relinked_same_window_at_mutation_lock() {
    let handler = RequestHandler::new();
    let fixture =
        linked_occurrence_fixture(&handler, "choose-tree-window-occurrence-action-aba", 501).await;
    let stale_action = ModeTreeAction::window_tree_target(
        fixture.session_name.clone(),
        fixture.session_id,
        2,
        fixture.window_id,
        fixture.old_occurrence_id,
    );
    let replacement_occurrence_id = replace_linked_occurrence(&handler, &fixture).await;

    let error = handler
        .perform_tree_kill_actions(fixture.attach_pid, vec![stale_action])
        .await
        .expect_err("stale occurrence action must fail closed");
    assert!(
        matches!(error, RmuxError::InvalidTarget { .. }),
        "{error:?}"
    );
    assert_replacement_survives(&handler, &fixture, replacement_occurrence_id).await;
}

#[tokio::test]
async fn choose_tree_stale_pane_action_rejects_relinked_window_occurrence() {
    let handler = RequestHandler::new();
    let fixture =
        linked_occurrence_fixture(&handler, "choose-tree-pane-occurrence-action-aba", 502).await;
    let stale_action = ModeTreeAction::pane_tree_target(
        rmux_proto::PaneTarget::with_window(fixture.session_name.clone(), 2, 0),
        fixture.session_id,
        fixture.window_id,
        fixture.old_occurrence_id,
        fixture.pane_id,
        fixture.pane_output_generation,
    );
    let replacement_occurrence_id = replace_linked_occurrence(&handler, &fixture).await;

    let error = handler
        .perform_tree_kill_actions(fixture.attach_pid, vec![stale_action])
        .await
        .expect_err("stale pane occurrence action must fail closed");
    assert!(
        matches!(error, RmuxError::InvalidTarget { .. }),
        "{error:?}"
    );
    assert_replacement_survives(&handler, &fixture, replacement_occurrence_id).await;
}

#[tokio::test]
async fn choose_tree_pane_action_resolves_exact_alias_when_pane_id_is_duplicated() {
    let handler = RequestHandler::new();
    let fixture =
        linked_occurrence_fixture(&handler, "choose-tree-pane-duplicate-alias", 506).await;
    let action = ModeTreeAction::pane_tree_target(
        rmux_proto::PaneTarget::with_window(fixture.session_name.clone(), 2, 0),
        fixture.session_id,
        fixture.window_id,
        fixture.old_occurrence_id,
        fixture.pane_id,
        fixture.pane_output_generation,
    );

    handler
        .perform_tree_kill_actions(fixture.attach_pid, vec![action])
        .await
        .expect("current pane occurrence action succeeds");

    let state = handler.state.lock().await;
    let session = state
        .sessions
        .session(&fixture.session_name)
        .expect("session survives");
    assert_eq!(
        session.window_at(0).map(rmux_core::Window::id),
        Some(fixture.window_id),
        "the other alias sharing the PaneId must survive"
    );
    assert!(
        session.window_at(2).is_none(),
        "the selected occurrence must be the one removed"
    );
}

#[tokio::test]
async fn choose_tree_stale_current_selection_does_not_kill_relinked_occurrence() {
    let handler = RequestHandler::new();
    let fixture =
        linked_occurrence_fixture(&handler, "choose-tree-window-occurrence-selection-aba", 503)
            .await;
    set_mode_tree_selection(
        &handler,
        fixture.attach_pid,
        fixture.old_window_item_id.clone(),
    )
    .await;
    let replacement_occurrence_id = replace_linked_occurrence(&handler, &fixture).await;

    handler
        .perform_tree_kill_current(fixture.attach_pid)
        .await
        .expect("stale current selection is a no-op");
    assert_replacement_survives(&handler, &fixture, replacement_occurrence_id).await;
}

#[tokio::test]
async fn choose_tree_stale_tag_does_not_kill_relinked_occurrence() {
    let handler = RequestHandler::new();
    let fixture =
        linked_occurrence_fixture(&handler, "choose-tree-window-occurrence-tag-aba", 504).await;
    handler
        .active_attach
        .lock()
        .await
        .by_pid
        .get_mut(&fixture.attach_pid)
        .and_then(|active| active.mode_tree.as_mut())
        .expect("mode-tree remains active")
        .tagged
        .insert(fixture.old_window_item_id.clone());
    let replacement_occurrence_id = replace_linked_occurrence(&handler, &fixture).await;

    handler
        .perform_tree_kill_tagged(fixture.attach_pid)
        .await
        .expect("stale tagged selection is a no-op");
    assert_replacement_survives(&handler, &fixture, replacement_occurrence_id).await;
}

#[tokio::test]
async fn choose_tree_accept_does_not_select_relinked_stale_occurrence() {
    let handler = RequestHandler::new();
    let fixture =
        linked_occurrence_fixture(&handler, "choose-tree-window-occurrence-accept-aba", 505).await;
    set_mode_tree_selection(
        &handler,
        fixture.attach_pid,
        fixture.old_window_item_id.clone(),
    )
    .await;
    let replacement_occurrence_id = replace_linked_occurrence(&handler, &fixture).await;

    handler
        .accept_mode_tree_selection(fixture.attach_pid)
        .await
        .expect("stale accept selection is a no-op");
    assert_replacement_survives(&handler, &fixture, replacement_occurrence_id).await;
    assert_eq!(
        handler
            .state
            .lock()
            .await
            .sessions
            .session(&fixture.session_name)
            .expect("session survives")
            .active_window_index(),
        0,
        "stale accept must not select the replacement at index 2",
    );
}

#[tokio::test]
async fn choose_tree_default_accept_revalidates_occurrence_at_selection_lock() {
    let handler = RequestHandler::new();
    let fixture = linked_occurrence_fixture(
        &handler,
        "choose-tree-window-occurrence-accept-lock-aba",
        507,
    )
    .await;
    let stale_target = ChooseTreeTarget {
        session_name: fixture.session_name.clone(),
        session_id: fixture.session_id,
        window_index: Some(2),
        window_id: Some(fixture.window_id),
        window_occurrence_id: Some(fixture.old_occurrence_id),
        pane_index: None,
        pane_id: None,
        pane_output_generation: None,
    };
    let replacement_occurrence_id = replace_linked_occurrence(&handler, &fixture).await;
    let action_identity = {
        let active_attach = handler.active_attach.lock().await;
        let active = active_attach
            .by_pid
            .get(&fixture.attach_pid)
            .expect("choose-tree host remains attached");
        assert_eq!(active.id, fixture.attach_id);
        ModeTreeActionIdentity::new(fixture.attach_pid, active.id, active.mode_tree_state_id)
    };

    let error = handler
        .apply_choose_tree_default_target(action_identity, stale_target)
        .await
        .expect_err("default accept must reject a replaced occurrence at the state lock");
    assert!(
        matches!(error, RmuxError::InvalidTarget { .. }),
        "{error:?}"
    );
    assert_replacement_survives(&handler, &fixture, replacement_occurrence_id).await;
}

#[test]
fn pane_item_identity_changes_with_its_window_link_occurrence() {
    assert_ne!(
        pane_item_id(
            rmux_proto::SessionId::new(1),
            2,
            rmux_proto::WindowId::new(3),
            WindowLinkOccurrenceId::new_for_test(4),
            rmux_proto::PaneId::new(5),
        ),
        pane_item_id(
            rmux_proto::SessionId::new(1),
            2,
            rmux_proto::WindowId::new(3),
            WindowLinkOccurrenceId::new_for_test(6),
            rmux_proto::PaneId::new(5),
        ),
    );
}