rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_proto::{
    PaneOutputCursorRequest, PaneOutputSubscriptionStart, PaneStreamCursorRequest, PaneStreamMode,
    Response, RmuxError, SubscribePaneOutputRequest, UnsubscribePaneOutputRequest,
    UnsubscribePaneStreamRequest,
};

use super::{subscribe, test_pane, RequestHandler, CONNECTION_ID};

const WRONG_LEGACY_KIND: &str = "subscription is not a pane-output subscription";
const WRONG_STREAM_KIND: &str = "subscription is not a pane stream";

fn assert_server_error(response: Response, message: &str) {
    let Response::Error(error) = response else {
        panic!("expected server error, got {response:?}");
    };
    assert_eq!(error.error, RmuxError::Server(message.to_owned()));
}

fn subscription_record(
    handler: &RequestHandler,
    subscription_id: rmux_proto::PaneOutputSubscriptionId,
) -> rmux_core::events::OutputSubscriptionRecord {
    handler
        .subscriptions
        .lock()
        .expect("subscription registry mutex")
        .registry
        .get(subscription_id)
        .expect("subscription must remain registered")
        .clone()
}

async fn assert_stream_is_readable(
    handler: &RequestHandler,
    subscription_id: rmux_proto::PaneOutputSubscriptionId,
) {
    let response = handler
        .handle_pane_stream_cursor(
            CONNECTION_ID,
            PaneStreamCursorRequest {
                subscription_id,
                max_events: Some(8),
            },
        )
        .await;
    assert!(
        matches!(response, Response::PaneStreamCursor(_)),
        "the correct stream endpoint must remain readable, got {response:?}"
    );
}

async fn close_stream(
    handler: &RequestHandler,
    subscription_id: rmux_proto::PaneOutputSubscriptionId,
) {
    let response = handler
        .handle_unsubscribe_pane_stream(
            CONNECTION_ID,
            UnsubscribePaneStreamRequest { subscription_id },
        )
        .await;
    assert!(
        matches!(response, Response::UnsubscribePaneStream(response) if response.removed),
        "the correct stream endpoint must still remove the subscription, got {response:?}"
    );
}

#[tokio::test]
async fn legacy_cursor_rejects_every_pane_stream_mode_without_mutation() {
    let handler = RequestHandler::new();
    let (target, _, _) = test_pane(&handler).await;

    for mode in [PaneStreamMode::Raw, PaneStreamMode::Surface] {
        let subscribed = subscribe(&handler, &target, mode).await;
        let before = subscription_record(&handler, subscribed.subscription_id);
        let response = handler
            .handle_pane_output_cursor(
                CONNECTION_ID,
                PaneOutputCursorRequest {
                    subscription_id: subscribed.subscription_id,
                    max_events: Some(8),
                },
            )
            .await;

        assert_server_error(response, WRONG_LEGACY_KIND);
        assert_eq!(
            subscription_record(&handler, subscribed.subscription_id),
            before,
            "the wrong cursor endpoint must not even touch the stream record"
        );
        assert_stream_is_readable(&handler, subscribed.subscription_id).await;
        close_stream(&handler, subscribed.subscription_id).await;
    }
}

#[tokio::test]
async fn legacy_unsubscribe_rejects_every_pane_stream_mode_without_mutation() {
    let handler = RequestHandler::new();
    let (target, _, _) = test_pane(&handler).await;

    for mode in [PaneStreamMode::Raw, PaneStreamMode::Surface] {
        let subscribed = subscribe(&handler, &target, mode).await;
        let before = subscription_record(&handler, subscribed.subscription_id);
        let response = handler
            .handle_unsubscribe_pane_output(
                CONNECTION_ID,
                UnsubscribePaneOutputRequest {
                    subscription_id: subscribed.subscription_id,
                },
            )
            .await;

        assert_server_error(response, WRONG_LEGACY_KIND);
        assert_eq!(
            subscription_record(&handler, subscribed.subscription_id),
            before,
            "the wrong unsubscribe endpoint must not mutate the stream record"
        );
        assert_stream_is_readable(&handler, subscribed.subscription_id).await;
        close_stream(&handler, subscribed.subscription_id).await;
    }
}

#[tokio::test]
async fn pane_stream_endpoints_reject_legacy_subscription_without_mutation() {
    let handler = RequestHandler::new();
    let (target, output, _) = test_pane(&handler).await;
    let response = handler
        .handle_subscribe_pane_output(
            CONNECTION_ID,
            SubscribePaneOutputRequest {
                target,
                start: PaneOutputSubscriptionStart::Now,
            },
        )
        .await;
    let Response::SubscribePaneOutput(subscribed) = response else {
        panic!("expected legacy subscription, got {response:?}");
    };
    output.send(b"still-readable".to_vec());

    let before = subscription_record(&handler, subscribed.subscription_id);
    let response = handler
        .handle_pane_stream_cursor(
            CONNECTION_ID,
            PaneStreamCursorRequest {
                subscription_id: subscribed.subscription_id,
                max_events: Some(8),
            },
        )
        .await;
    assert_server_error(response, WRONG_STREAM_KIND);
    assert_eq!(
        subscription_record(&handler, subscribed.subscription_id),
        before,
        "the wrong stream cursor must not touch the legacy record"
    );

    let response = handler
        .handle_unsubscribe_pane_stream(
            CONNECTION_ID,
            UnsubscribePaneStreamRequest {
                subscription_id: subscribed.subscription_id,
            },
        )
        .await;
    assert_server_error(response, WRONG_STREAM_KIND);
    assert_eq!(
        subscription_record(&handler, subscribed.subscription_id),
        before,
        "the wrong stream unsubscribe must not mutate the legacy record"
    );

    let response = handler
        .handle_pane_output_cursor(
            CONNECTION_ID,
            PaneOutputCursorRequest {
                subscription_id: subscribed.subscription_id,
                max_events: Some(8),
            },
        )
        .await;
    let Response::PaneOutputCursor(cursor) = response else {
        panic!("the correct legacy endpoint must remain readable, got {response:?}");
    };
    assert_eq!(cursor.events.len(), 1);
    assert_eq!(cursor.events[0].bytes, b"still-readable");
}

#[tokio::test]
async fn revoked_cleanup_rejects_pane_stream_id_at_legacy_unsubscribe() {
    let handler = RequestHandler::new();
    let (target, _, _) = test_pane(&handler).await;
    let subscribed = subscribe(&handler, &target, PaneStreamMode::Raw).await;
    let before = subscription_record(&handler, subscribed.subscription_id);

    let response = handler
        .handle_revoked_cleanup_request(
            CONNECTION_ID,
            rmux_proto::Request::UnsubscribePaneOutput(UnsubscribePaneOutputRequest {
                subscription_id: subscribed.subscription_id,
            }),
        )
        .await
        .expect("unsubscribe is accepted as a revoked cleanup request");

    assert_server_error(response, WRONG_LEGACY_KIND);
    assert_eq!(
        subscription_record(&handler, subscribed.subscription_id),
        before,
        "revoked cleanup must not mutate a stream through the legacy endpoint"
    );
    assert_stream_is_readable(&handler, subscribed.subscription_id).await;
}