use super::*;
use super::switch_frame_geometry::{
frame_geometry, linked_alias_sessions, pane_pty_size, register_declared_attach,
set_window_size_policy, window_content_size, CLIENT_SIZE, SOURCE_WINDOW_INDEX, STATUS_OFF,
TARGET_WINDOW_INDEX,
};
use rmux_core::SessionRecency;
const MOVER_SIZE: TerminalSize = TerminalSize {
cols: 120,
rows: 50,
};
const RESIDENT_SIZE: TerminalSize = CLIENT_SIZE;
const REQUESTED_SIZE: TerminalSize = TerminalSize { cols: 80, rows: 24 };
const MOVER_PID: u32 = 94_201;
const RESIDENT_PID: u32 = 94_202;
async fn session_recency(handler: &RequestHandler, session: &SessionName) -> SessionRecency {
handler
.state
.lock()
.await
.sessions
.session(session)
.expect("session exists")
.recency()
}
async fn create_witness_session(handler: &RequestHandler, session: &SessionName) {
let created = handler
.handle(Request::NewSession(NewSessionRequest {
session_name: session.clone(),
detached: true,
size: Some(CLIENT_SIZE),
environment: None,
}))
.await;
assert!(matches!(created, Response::NewSession(_)), "{created:?}");
}
async fn linked_family(handler: &RequestHandler, policy: &str) -> (SessionName, SessionName) {
let (alpha, beta) = linked_alias_sessions(handler, STATUS_OFF, STATUS_OFF).await;
set_window_size_policy(handler, &alpha, SOURCE_WINDOW_INDEX, policy).await;
set_window_size_policy(handler, &beta, TARGET_WINDOW_INDEX, policy).await;
(alpha, beta)
}
#[tokio::test]
async fn attached_switch_credits_the_destination_recency_exactly_once() {
let handler = RequestHandler::new();
let (alpha, beta) = linked_family(&handler, "latest").await;
let mut mover_rx = register_declared_attach(&handler, MOVER_PID, &alpha, MOVER_SIZE).await;
let _resident_rx = register_declared_attach(&handler, RESIDENT_PID, &beta, RESIDENT_SIZE).await;
drain_attach_controls(&mut mover_rx);
assert_eq!(
window_content_size(&handler, &beta, TARGET_WINDOW_INDEX).await,
RESIDENT_SIZE,
"the resident client must own the shared window before the switch"
);
let witness = session_name("combined-switch-witness");
create_witness_session(&handler, &witness).await;
let alpha_before = session_recency(&handler, &alpha).await;
let beta_before = session_recency(&handler, &beta).await;
let witness_before = session_recency(&handler, &witness).await;
assert!(
witness_before > beta_before,
"the fixture must leave the witness ranked ahead of the destination"
);
let switched = handler
.dispatch(
MOVER_PID,
Request::SwitchClient(SwitchClientRequest {
target: beta.clone(),
}),
)
.await;
assert!(
matches!(switched.response, Response::SwitchClient(_)),
"the switch must succeed, got {:?}",
switched.response
);
let beta_after = session_recency(&handler, &beta).await;
assert!(
beta_after > witness_before,
"the commit must credit the destination, and the credit must be newer \
than a session used just before the command"
);
assert_eq!(
session_recency(&handler, &alpha).await,
alpha_before,
"the source keeps the order it had before the client left"
);
assert_eq!(
session_recency(&handler, &witness).await,
witness_before,
"a switch credits its destination and nothing else"
);
let framed = frame_geometry(recv_switch_target(&mut mover_rx, "combined switch frame").await);
assert_eq!(framed, MOVER_SIZE, "the switch frame carries the mover");
for (alias, window_index) in [(&beta, TARGET_WINDOW_INDEX), (&alpha, SOURCE_WINDOW_INDEX)] {
assert_eq!(
window_content_size(&handler, alias, window_index).await,
MOVER_SIZE,
"alias {alias}:{window_index} must settle on the mover's geometry"
);
assert_eq!(
pane_pty_size(&handler, alias, window_index).await,
MOVER_SIZE,
"the PTY behind {alias}:{window_index} must agree with the model"
);
}
}
#[derive(Clone, Copy, Debug)]
enum LostDelivery {
Closing,
ClosedReceiver,
FullBacklog,
}
#[tokio::test]
async fn a_failed_attached_switch_leaves_the_destination_recency_unchanged() {
for lost in [
LostDelivery::Closing,
LostDelivery::ClosedReceiver,
LostDelivery::FullBacklog,
] {
assert_failed_switch_credits_nothing(lost).await;
}
}
async fn assert_failed_switch_credits_nothing(lost: LostDelivery) {
let handler = RequestHandler::new();
let (alpha, beta) = linked_family(&handler, "largest").await;
let mut mover_rx = register_declared_attach(&handler, MOVER_PID, &alpha, MOVER_SIZE).await;
drain_attach_controls(&mut mover_rx);
let identity = handler.active_attach_identity_for_test(MOVER_PID).await;
let alpha_before = session_recency(&handler, &alpha).await;
let beta_before = session_recency(&handler, &beta).await;
let pause = handler.install_attached_size_selection_pause();
let sized_attach = super::super::with_expected_attach_and_session_identity(
identity,
alpha.clone(),
identity.session_id(),
handler.dispatch(
MOVER_PID,
Request::AttachSessionExt2(Box::new(AttachSessionExt2Request {
target: Some(beta.clone()),
target_spec: Some(beta.to_string()),
detach_other_clients: false,
kill_other_clients: false,
read_only: false,
skip_environment_update: false,
flags: None,
working_directory: None,
client_terminal: rmux_proto::ClientTerminalContext::default(),
client_size: Some(REQUESTED_SIZE),
})),
),
);
let lose_the_delivery = async {
tokio::time::timeout(ATTACH_LIFECYCLE_TIMEOUT, pause.reached.notified())
.await
.expect("the sized attach-session reaches its selection pause");
lose_delivery(&handler, lost, identity.attach_id(), &mut mover_rx).await;
pause.release.notify_one();
};
let (attached, ()) = tokio::join!(sized_attach, lose_the_delivery);
assert!(
matches!(attached.response, Response::Error(_)),
"{lost:?}: the sized attach-session must fail, got {:?}",
attached.response
);
assert_eq!(
session_recency(&handler, &beta).await,
beta_before,
"{lost:?}: a switch that never committed must not credit its destination"
);
assert_eq!(
session_recency(&handler, &alpha).await,
alpha_before,
"{lost:?}: a failed switch must not credit the session it tried to leave"
);
}
async fn lose_delivery(
handler: &RequestHandler,
lost: LostDelivery,
expected_attach_id: u64,
control_rx: &mut mpsc::UnboundedReceiver<AttachControl>,
) {
match lost {
LostDelivery::Closing => {
let response = handler
.dispatch(MOVER_PID, Request::DetachClient(DetachClientRequest))
.await
.response;
assert!(
matches!(response, Response::DetachClient(_)),
"detach-client must succeed, got {response:?}"
);
}
LostDelivery::ClosedReceiver => control_rx.close(),
LostDelivery::FullBacklog => {
let mut active_attach = handler.active_attach.lock().await;
let active = active_attach
.by_pid
.get_mut(&MOVER_PID)
.expect("the attached client is registered");
let payload = vec![
0_u8;
(super::super::attach_support::ATTACH_CONTROL_BACKLOG_LIMIT - 1)
* AttachControl::BACKLOG_UNIT_BYTES
];
active
.control_tx
.send(AttachControl::Write(payload))
.expect("the last control that fits the budget is accepted");
}
}
let active_attach = handler.active_attach.lock().await;
let active = active_attach
.by_pid
.get(&MOVER_PID)
.expect("every one of these states keeps the registration under its pid");
assert_eq!(
active.id, expected_attach_id,
"{lost:?} must not replace the captured generation"
);
}
#[tokio::test]
async fn rename_between_switch_selection_and_commit_preserves_both_orders() {
let handler = RequestHandler::new();
let (alpha, beta) = linked_family(&handler, "latest").await;
let renamed = session_name("combined-renamed-source");
let mut mover_rx = register_declared_attach(&handler, MOVER_PID, &alpha, MOVER_SIZE).await;
let _resident_rx = register_declared_attach(&handler, RESIDENT_PID, &beta, RESIDENT_SIZE).await;
drain_attach_controls(&mut mover_rx);
let alpha_before = session_recency(&handler, &alpha).await;
let beta_before = session_recency(&handler, &beta).await;
let pause = handler.install_attached_size_selection_pause();
let switching = handler.dispatch(
MOVER_PID,
Request::SwitchClient(SwitchClientRequest {
target: beta.clone(),
}),
);
let rename_the_source = async {
tokio::time::timeout(ATTACH_LIFECYCLE_TIMEOUT, pause.reached.notified())
.await
.expect("the switch reaches its selection pause");
let response = handler
.handle(Request::RenameSession(RenameSessionRequest {
target: alpha.clone(),
new_name: renamed.clone(),
}))
.await;
assert!(
matches!(response, Response::RenameSession(_)),
"renaming the source must succeed, got {response:?}"
);
drain_attach_controls(&mut mover_rx);
pause.release.notify_one();
};
let (switched, ()) = tokio::join!(switching, rename_the_source);
assert!(
matches!(switched.response, Response::SwitchClient(_)),
"a rename of the source must not fail the switch, got {:?}",
switched.response
);
assert!(
session_recency(&handler, &beta).await > beta_before,
"the destination is still credited by the commit"
);
assert_eq!(
session_recency(&handler, &renamed).await,
alpha_before,
"a rename moves a store key, it is not use of the session"
);
let framed = frame_geometry(recv_switch_target(&mut mover_rx, "renamed switch frame").await);
assert_eq!(framed, MOVER_SIZE, "the switch frame carries the mover");
for (alias, window_index) in [
(&beta, TARGET_WINDOW_INDEX),
(&renamed, SOURCE_WINDOW_INDEX),
] {
assert_eq!(
window_content_size(&handler, alias, window_index).await,
MOVER_SIZE,
"alias {alias}:{window_index} must follow the mover's sizing order"
);
}
}
#[tokio::test]
async fn switch_latest_recency_holds_with_a_popup_open() {
let handler = RequestHandler::new();
let (alpha, beta) = linked_family(&handler, "latest").await;
let mut mover_rx = register_declared_attach(&handler, MOVER_PID, &alpha, MOVER_SIZE).await;
let _resident_rx = register_declared_attach(&handler, RESIDENT_PID, &beta, RESIDENT_SIZE).await;
drain_attach_controls(&mut mover_rx);
open_popup(&handler, MOVER_PID).await;
drain_attach_controls(&mut mover_rx);
assert!(
client_has_overlay(&handler, MOVER_PID).await,
"the popup must still be open when the switch runs"
);
let beta_before = session_recency(&handler, &beta).await;
let switched = handler
.dispatch(
MOVER_PID,
Request::SwitchClient(SwitchClientRequest {
target: beta.clone(),
}),
)
.await;
assert!(
matches!(switched.response, Response::SwitchClient(_)),
"an open popup must not fail the switch, got {:?}",
switched.response
);
let framed = frame_geometry(recv_switch_target(&mut mover_rx, "popup switch frame").await);
assert_eq!(
framed, MOVER_SIZE,
"an open popup must not change which client the switch frame is sized for"
);
for (alias, window_index) in [(&beta, TARGET_WINDOW_INDEX), (&alpha, SOURCE_WINDOW_INDEX)] {
assert_eq!(
window_content_size(&handler, alias, window_index).await,
MOVER_SIZE,
"alias {alias}:{window_index} must settle on the mover's geometry"
);
assert_eq!(
pane_pty_size(&handler, alias, window_index).await,
MOVER_SIZE,
"the PTY behind {alias}:{window_index} must agree with the model"
);
}
assert!(
session_recency(&handler, &beta).await > beta_before,
"the destination is credited once whether or not a popup is open"
);
}
async fn open_popup(handler: &RequestHandler, requester_pid: u32) {
let parsed = handler
.parse_control_commands("display-popup -N -T Combined -w 30 -h 8 -x C -y C")
.await
.expect("display-popup parses");
let result = handler
.execute_parsed_commands_for_test(requester_pid, parsed)
.await
.expect("display-popup executes");
assert!(
result.stdout().is_empty(),
"display-popup writes nothing to stdout"
);
}
async fn client_has_overlay(handler: &RequestHandler, requester_pid: u32) -> bool {
handler
.active_attach
.lock()
.await
.by_pid
.get(&requester_pid)
.expect("the attached client is registered")
.overlay
.is_some()
}