use super::*;
use super::switch_frame_geometry::{
register_declared_attach, set_window_size_policy, window_content_size,
};
use rmux_core::SessionRecency;
const SIZELESS_PID: u32 = 94_301;
const STALE_PID: u32 = 94_311;
const RIVAL_PID: u32 = 94_312;
const FIRST_REHOME_PID: u32 = 94_321;
const SECOND_REHOME_PID: u32 = 94_322;
const PINNED_SECOND: i64 = 1_785_500_000;
const ANCHOR_SIZE: TerminalSize = TerminalSize { cols: 80, rows: 24 };
const LOW_SEQUENCE_SIZE: TerminalSize = TerminalSize {
cols: 100,
rows: 40,
};
const HIGH_SEQUENCE_SIZE: TerminalSize = TerminalSize {
cols: 120,
rows: 50,
};
async fn session_recency(handler: &RequestHandler, session: &SessionName) -> SessionRecency {
handler
.state
.lock()
.await
.sessions
.session(session)
.expect("session exists")
.recency()
}
async fn attached_size_sequence(handler: &RequestHandler, attach_pid: u32) -> u64 {
handler
.active_attach
.lock()
.await
.by_pid
.get(&attach_pid)
.expect("the attached client is registered")
.size_sequence
}
async fn attached_size_is_inferred(handler: &RequestHandler, attach_pid: u32) -> bool {
handler
.active_attach
.lock()
.await
.by_pid
.get(&attach_pid)
.expect("the attached client is registered")
.client_size_is_inferred()
}
async fn attached_generation_id(handler: &RequestHandler, attach_pid: u32) -> u64 {
handler
.active_attach
.lock()
.await
.by_pid
.get(&attach_pid)
.expect("the attached client is registered")
.id
}
async fn create_detached_session(handler: &RequestHandler, session: &SessionName) {
let created = handler
.handle(Request::NewSession(NewSessionRequest {
session_name: session.clone(),
detached: true,
size: Some(ANCHOR_SIZE),
environment: None,
}))
.await;
assert!(matches!(created, Response::NewSession(_)), "{created:?}");
}
async fn set_session_status(handler: &RequestHandler, session: &SessionName, value: &str) {
let response = handler
.handle(Request::SetOption(SetOptionRequest {
scope: ScopeSelector::Session(session.clone()),
option: OptionName::Status,
value: value.to_owned(),
mode: SetOptionMode::Replace,
}))
.await;
assert!(matches!(response, Response::SetOption(_)), "{response:?}");
}
async fn pin_public_times(handler: &RequestHandler, sessions: &[&SessionName]) {
let mut state = handler.state.lock().await;
for session in sessions {
state
.sessions
.session_mut(session)
.expect("session exists")
.pin_public_times_for_tests(PINNED_SECOND);
}
}
#[tokio::test]
async fn promoting_an_inferred_client_size_does_not_touch_session_recency() {
let handler = RequestHandler::new();
let session = session_name("combined-promotion");
create_detached_session(&handler, &session).await;
set_session_status(&handler, &session, "2").await;
let (control_tx, _control_rx) = mpsc::unbounded_channel();
let uid = current_owner_uid();
handler
.register_attach_with_access(
SIZELESS_PID,
session.clone(),
None,
AttachRegistration {
control_tx,
control_backlog: Arc::new(AtomicUsize::new(0)),
closing: Arc::new(AtomicBool::new(false)),
persistent_overlay_epoch: Arc::new(AtomicU64::new(0)),
terminal_context: OuterTerminalContext::default(),
client_title: None,
flags: super::super::attach_support::ClientFlags::default(),
render_stream: false,
uid,
user: rmux_os::identity::UserIdentity::Uid(uid),
can_write: true,
client_size: None,
},
)
.await
.expect("sizeless attach registration succeeds");
assert!(
attached_size_is_inferred(&handler, SIZELESS_PID).await,
"a client that declared no size must be anchored, not trusted"
);
let sequence_before = attached_size_sequence(&handler, SIZELESS_PID).await;
let recency_before = session_recency(&handler, &session).await;
handler
.handle_attached_resize(SIZELESS_PID, ANCHOR_SIZE)
.await
.expect("promoting resize succeeds");
assert!(
!attached_size_is_inferred(&handler, SIZELESS_PID).await,
"a real resize leaves no inferred provenance behind"
);
assert!(
attached_size_sequence(&handler, SIZELESS_PID).await > sequence_before,
"promotion must mint a fresh sizing order even at identical dimensions"
);
assert_eq!(
session_recency(&handler, &session).await,
recency_before,
"promotion is a sizing event: it is neither an attach nor an interaction, \
so the session's recency order must not move"
);
}
#[tokio::test]
async fn registration_credits_recency_once_across_a_concurrent_same_pid_replacement() {
let handler = Arc::new(RequestHandler::new());
let used = session_name("combined-used");
let rival = session_name("combined-rival");
create_detached_session(&handler, &used).await;
create_detached_session(&handler, &rival).await;
let pause = handler.install_attach_registration_activity_pause();
let (stale_tx, _stale_rx) = mpsc::unbounded_channel();
let registering_handler = Arc::clone(&handler);
let stale_session = used.clone();
let registering = tokio::spawn(async move {
registering_handler
.register_attach(STALE_PID, stale_session, stale_tx)
.await
});
tokio::time::timeout(ATTACH_LIFECYCLE_TIMEOUT, pause.reached.notified())
.await
.expect("attach registration reaches its activity commit");
let _replacement_rx =
register_declared_attach(&handler, STALE_PID, &used, HIGH_SEQUENCE_SIZE).await;
let replacement_generation = attached_generation_id(&handler, STALE_PID).await;
let replacement_sequence = attached_size_sequence(&handler, STALE_PID).await;
let _rival_rx = register_declared_attach(&handler, RIVAL_PID, &rival, LOW_SEQUENCE_SIZE).await;
pin_public_times(&handler, &[&used, &rival]).await;
pause.release.notify_one();
tokio::time::timeout(ATTACH_LIFECYCLE_TIMEOUT, registering)
.await
.expect("the stale registration finishes")
.expect("the registration task does not panic");
assert_eq!(
handler
.preferred_session_name()
.await
.expect("preferred session resolves"),
rival,
"the stale registration no longer owns this pid, so its credit must not \
fire and push its session back to the front"
);
assert_eq!(
attached_generation_id(&handler, STALE_PID).await,
replacement_generation,
"the replacement must still own the pid"
);
assert_eq!(
attached_size_sequence(&handler, STALE_PID).await,
replacement_sequence,
"a stale generation must not displace the replacement's sizing order"
);
}
#[tokio::test]
async fn destroy_rehome_orders_clients_by_captured_size_sequence_and_sessions_by_recency() {
let handler = RequestHandler::new();
let doomed = session_name("combined-doomed");
let older = session_name("combined-a-older");
let newer = session_name("combined-z-newer");
for session in [&doomed, &older, &newer] {
create_detached_session(&handler, session).await;
set_session_status(&handler, session, "off").await;
}
set_window_size_policy(&handler, &newer, 0, "latest").await;
set_detach_on_destroy(&handler, &doomed, "off").await;
{
let mut state = handler.state.lock().await;
for session in [&older, &newer] {
state
.sessions
.session_mut(session)
.expect("survivor exists")
.touch_attached();
}
}
pin_public_times(&handler, &[&doomed, &older, &newer]).await;
let mut first_rx =
register_declared_attach(&handler, FIRST_REHOME_PID, &doomed, HIGH_SEQUENCE_SIZE).await;
let mut second_rx =
register_declared_attach(&handler, SECOND_REHOME_PID, &doomed, LOW_SEQUENCE_SIZE).await;
for pid in [SECOND_REHOME_PID, FIRST_REHOME_PID] {
let switched = handler
.dispatch(
pid,
Request::SwitchClient(SwitchClientRequest {
target: doomed.clone(),
}),
)
.await;
assert!(
matches!(switched.response, Response::SwitchClient(_)),
"the current-session switch must succeed, got {:?}",
switched.response
);
}
let first_sequence = attached_size_sequence(&handler, FIRST_REHOME_PID).await;
let second_sequence = attached_size_sequence(&handler, SECOND_REHOME_PID).await;
assert!(
second_sequence < first_sequence,
"the fixture must invert the two orders: the lower attach id has to hold \
the higher sizing order, got {second_sequence} and {first_sequence}"
);
drain_attach_controls(&mut first_rx);
drain_attach_controls(&mut second_rx);
let killed = handler
.handle(Request::KillSession(KillSessionRequest {
target: doomed.clone(),
kill_all_except_target: false,
clear_alerts: false,
kill_group: false,
}))
.await;
assert!(matches!(killed, Response::KillSession(_)), "{killed:?}");
for (label, control_rx) in [("first", &mut first_rx), ("second", &mut second_rx)] {
let target = recv_switch_target(control_rx, "destroy rehome").await;
assert_eq!(
target.session_name, newer,
"the {label} rehomed client must follow the recency order"
);
}
assert_eq!(
window_content_size(&handler, &newer, 0).await,
HIGH_SEQUENCE_SIZE,
"the rehome order must follow the captured sizing order, not the attach ids"
);
}
async fn set_detach_on_destroy(handler: &RequestHandler, session: &SessionName, value: &str) {
handler
.state
.lock()
.await
.options
.set(
ScopeSelector::Session(session.clone()),
OptionName::DetachOnDestroy,
value.to_owned(),
SetOptionMode::Replace,
)
.expect("detach-on-destroy policy is valid");
}