rmux-server 0.2.0

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

#[tokio::test]
async fn session_lease_rejects_too_short_ttl() {
    let handler = RequestHandler::new();
    let alpha = session_name("lease-short");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let created_lease = handler
        .handle(Request::CreateSessionLease(
            rmux_proto::CreateSessionLeaseRequest {
                session_name: alpha,
                ttl_millis: rmux_proto::MIN_SESSION_LEASE_TTL_MILLIS - 1,
            },
        ))
        .await;
    let Response::Error(error) = created_lease else {
        panic!("expected too-short lease ttl to be rejected");
    };
    assert!(
        error.error.to_string().contains("must be at least"),
        "unexpected lease ttl error: {}",
        error.error
    );
}

#[tokio::test]
async fn session_lease_reaper_kills_unrenewed_session() {
    let handler = RequestHandler::new();
    let alpha = session_name("leased");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let created_lease = handler
        .handle(Request::CreateSessionLease(
            rmux_proto::CreateSessionLeaseRequest {
                session_name: alpha.clone(),
                ttl_millis: 600,
            },
        ))
        .await;
    assert!(matches!(created_lease, Response::CreateSessionLease(_)));

    tokio::time::sleep(Duration::from_millis(800)).await;

    let exists = handler
        .handle(Request::HasSession(HasSessionRequest {
            target: alpha.clone(),
        }))
        .await;
    assert_eq!(
        exists,
        Response::HasSession(rmux_proto::HasSessionResponse { exists: false })
    );
}

#[tokio::test]
async fn session_lease_renew_and_release_preserves_session() {
    let handler = RequestHandler::new();
    let alpha = session_name("renewed");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let created_lease = handler
        .handle(Request::CreateSessionLease(
            rmux_proto::CreateSessionLeaseRequest {
                session_name: alpha.clone(),
                ttl_millis: 600,
            },
        ))
        .await;
    let Response::CreateSessionLease(created_lease) = created_lease else {
        panic!("expected lease create response");
    };

    let renewed = handler
        .handle(Request::RenewSessionLease(
            rmux_proto::RenewSessionLeaseRequest {
                session_name: alpha.clone(),
                token: created_lease.token,
                ttl_millis: 600,
            },
        ))
        .await;
    assert_eq!(
        renewed,
        Response::RenewSessionLease(rmux_proto::RenewSessionLeaseResponse { renewed: true })
    );

    let released = handler
        .handle(Request::ReleaseSessionLease(
            rmux_proto::ReleaseSessionLeaseRequest {
                session_name: alpha.clone(),
                token: created_lease.token,
            },
        ))
        .await;
    assert_eq!(
        released,
        Response::ReleaseSessionLease(rmux_proto::ReleaseSessionLeaseResponse { released: true })
    );

    tokio::time::sleep(Duration::from_millis(700)).await;

    let exists = handler
        .handle(Request::HasSession(HasSessionRequest {
            target: alpha.clone(),
        }))
        .await;
    assert_eq!(
        exists,
        Response::HasSession(rmux_proto::HasSessionResponse { exists: true })
    );
}

#[tokio::test]
async fn session_destroyed_by_last_pane_kill_clears_stale_lease() {
    let handler = RequestHandler::new();
    let alpha = session_name("lease-pane-kill");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let created_lease = handler
        .handle(Request::CreateSessionLease(
            rmux_proto::CreateSessionLeaseRequest {
                session_name: alpha.clone(),
                ttl_millis: 600,
            },
        ))
        .await;
    assert!(matches!(created_lease, Response::CreateSessionLease(_)));

    let killed = handler
        .handle(Request::KillPane(KillPaneRequest {
            target: PaneTarget::new(alpha.clone(), 0),
            kill_all_except: false,
        }))
        .await;
    assert_eq!(
        killed,
        Response::KillPane(rmux_proto::KillPaneResponse {
            target: PaneTarget::new(alpha.clone(), 0),
            window_destroyed: true,
        })
    );

    let recreated = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(
        matches!(recreated, Response::NewSession(_)),
        "same session name should be reusable after final-pane kill: {recreated:?}"
    );

    tokio::time::sleep(Duration::from_millis(800)).await;

    let exists = handler
        .handle(Request::HasSession(HasSessionRequest {
            target: alpha.clone(),
        }))
        .await;
    assert_eq!(
        exists,
        Response::HasSession(rmux_proto::HasSessionResponse { exists: true })
    );
}

#[tokio::test]
async fn session_destroyed_by_last_pane_exit_clears_stale_lease() {
    let handler = RequestHandler::new();
    let alpha = session_name("lease-pane-exit");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let created_lease = handler
        .handle(Request::CreateSessionLease(
            rmux_proto::CreateSessionLeaseRequest {
                session_name: alpha.clone(),
                ttl_millis: 600,
            },
        ))
        .await;
    assert!(matches!(created_lease, Response::CreateSessionLease(_)));

    let respawned = handler
        .handle(Request::RespawnPane(rmux_proto::RespawnPaneRequest {
            target: PaneTarget::new(alpha.clone(), 0),
            kill: true,
            start_directory: None,
            environment: None,
            command: None,
            process_command: Some(rmux_proto::ProcessCommand::Shell("exit 0".to_owned())),
        }))
        .await;
    assert!(matches!(respawned, Response::RespawnPane(_)));
    wait_for_session_state(&handler, alpha.clone(), false).await;

    let recreated = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: None,
            environment: None,
        }))
        .await;
    assert!(
        matches!(recreated, Response::NewSession(_)),
        "same session name should be reusable after final-pane exit: {recreated:?}"
    );

    tokio::time::sleep(Duration::from_millis(800)).await;
    wait_for_session_state(&handler, alpha, true).await;
}

async fn wait_for_session_state(
    handler: &RequestHandler,
    session_name: rmux_proto::SessionName,
    expected: bool,
) {
    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
    loop {
        let exists = handler
            .handle(Request::HasSession(HasSessionRequest {
                target: session_name.clone(),
            }))
            .await;
        if exists == Response::HasSession(rmux_proto::HasSessionResponse { exists: expected }) {
            return;
        }
        assert!(
            tokio::time::Instant::now() < deadline,
            "session {session_name} did not reach exists={expected}; last response: {exists:?}"
        );
        tokio::time::sleep(Duration::from_millis(20)).await;
    }
}