#![cfg(all(feature = "shells", unix))]
use std::time::{Duration, Instant};
use basemind::shells::ShellRuntime;
use basemind::shells::session::{self, ShellCommand};
use tempfile::TempDir;
fn runtime_in(dir: &TempDir) -> ShellRuntime {
let daemon = std::path::PathBuf::from(env!("CARGO_BIN_EXE_basemind"));
unsafe { basemind::shells::daemon::point_sdk_daemon_at(&daemon) }
let socket = dir.path().join("shells.sock");
ShellRuntime::with_socket_path(socket)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_capture_kill_roundtrip() {
let dir = tempfile::tempdir().expect("tempdir");
let runtime = runtime_in(&dir);
let (keepalive_id, keepalive_name) = runtime
.spawn(
runtime.mint_session_id(),
ShellCommand::Shell("sleep 60".to_string()),
None,
Vec::new(),
200,
50,
)
.await
.expect("spawn keepalive session");
let (session_id, name) = runtime
.spawn(
runtime.mint_session_id(),
ShellCommand::Shell("echo basemind-hi; sleep 5".to_string()),
None,
Vec::new(),
200,
50,
)
.await
.expect("spawn session");
assert_ne!(name, keepalive_name, "sessions get distinct names");
assert_eq!(
runtime.resolve(&session_id).await.as_ref(),
Some(&name),
"minted session_id should resolve to the rmux session name"
);
let rmux = runtime.rmux().await.expect("rmux handle");
let deadline = Instant::now() + Duration::from_secs(15);
loop {
let live = rmux.session(name.clone()).await.expect("open live session");
let captured = session::capture(&live, None).await.expect("capture");
if captured.contains("basemind-hi") {
break;
}
assert!(
Instant::now() < deadline,
"timed out waiting for sentinel; last capture was {captured:?}"
);
tokio::time::sleep(Duration::from_millis(200)).await;
}
let listed = session::list_sessions(rmux).await.expect("list sessions");
assert!(
listed.iter().any(|n| n == &name),
"live session {name:?} should appear in list_sessions: {listed:?}"
);
let live = rmux.session(name.clone()).await.expect("open for kill");
let killed = session::kill_session(&live).await.expect("kill");
assert!(killed, "killing a live session returns true");
runtime.forget(&session_id).await;
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let listed = session::list_sessions(rmux).await.expect("list after kill");
if !listed.iter().any(|n| n == &name) {
break;
}
assert!(
Instant::now() < deadline,
"session {name:?} still listed after kill: {listed:?}"
);
tokio::time::sleep(Duration::from_millis(200)).await;
}
assert!(
runtime.resolve(&session_id).await.is_none(),
"forgotten session_id should no longer resolve"
);
let keepalive = rmux
.session(keepalive_name)
.await
.expect("open keepalive for kill");
let _ = session::kill_session(&keepalive).await;
runtime.forget(&keepalive_id).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn broadcast_reaches_every_session_and_list_reports_alive() {
const MARKER: &str = "S2-BROADCAST";
let dir = tempfile::tempdir().expect("tempdir");
let runtime = runtime_in(&dir);
let (id_a, name_a) = runtime
.spawn(
runtime.mint_session_id(),
ShellCommand::Argv(vec!["bash".to_string()]),
None,
Vec::new(),
200,
50,
)
.await
.expect("spawn session A");
let (id_b, name_b) = runtime
.spawn(
runtime.mint_session_id(),
ShellCommand::Argv(vec!["bash".to_string()]),
None,
Vec::new(),
200,
50,
)
.await
.expect("spawn session B");
assert_ne!(name_a, name_b, "sessions get distinct names");
let rmux = runtime.rmux().await.expect("rmux handle");
let listed = runtime.list().await.expect("list sessions");
assert_eq!(
listed.len(),
2,
"both spawned sessions are listed: {listed:?}"
);
for id in [&id_a, &id_b] {
let entry = listed
.iter()
.find(|info| &info.session_id == id)
.unwrap_or_else(|| panic!("session {id} missing from list: {listed:?}"));
assert!(entry.alive, "freshly spawned session {id} should be alive");
}
let delivered = runtime
.broadcast(
&[id_a.clone(), id_b.clone()],
&format!("echo {MARKER}"),
true,
)
.await
.expect("broadcast to both sessions");
assert_eq!(delivered, 2, "broadcast delivered to both panes");
for name in [&name_a, &name_b] {
let deadline = Instant::now() + Duration::from_secs(15);
loop {
let live = rmux.session(name.clone()).await.expect("open live session");
let captured = session::capture(&live, None).await.expect("capture");
if captured.contains(MARKER) {
break;
}
assert!(
Instant::now() < deadline,
"timed out waiting for {MARKER} in {name:?}; last capture was {captured:?}"
);
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
for (id, name) in [(&id_a, &name_a), (&id_b, &name_b)] {
let live = rmux.session(name.clone()).await.expect("open for kill");
let _ = session::kill_session(&live).await;
runtime.forget(id).await;
}
}