use std::path::{Path, PathBuf};
use chrono::Utc;
use tempfile::TempDir;
use crate::session_manager::manager::SessionManager;
use crate::session_manager::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
use crate::session_manager::tests::FakeTmuxDriver;
use crate::session_manager::worktree_reclaim_claim::ClaimLiveness;
fn tombstoned(tmux_name: &str, workspace: &Path) -> SessionRecord {
SessionRecord {
id: ManagedSessionId::for_adopted_tmux_name(tmux_name),
tmux_name: tmux_name.to_owned(),
cwd: workspace.to_path_buf(),
task: "adopted pane".into(),
state: ManagedSessionState::Deleted,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(workspace.to_path_buf()),
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
}
}
async fn seeded(
dir: &TempDir,
fake: std::sync::Arc<FakeTmuxDriver>,
workspace: &Path,
) -> SessionManager {
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
let mut store = mgr.store.write().await;
store
.upsert(tombstoned("tm-gone", workspace))
.await
.unwrap();
store
.upsert(tombstoned("tm-still-here", workspace))
.await
.unwrap();
drop(store);
mgr
}
#[tokio::test]
async fn dead_sessions_claims_are_discarded_and_live_ones_are_not() {
let dir = TempDir::new().unwrap();
let workspace = PathBuf::from("/srv/projects/acme");
let fake = FakeTmuxDriver::new();
fake.seeded_names
.lock()
.unwrap()
.push("tm-still-here".into());
let mgr = seeded(&dir, fake, &workspace).await;
let claims = mgr.workspace_claims(None).await;
let gone = claims
.claims
.iter()
.find(|c| c.session == ManagedSessionId::for_adopted_tmux_name("tm-gone").to_string())
.expect("the dead session's claim is still IN the set, just not live");
assert_eq!(gone.liveness, ClaimLiveness::SessionGone);
let live = claims
.claims
.iter()
.find(|c| c.session == ManagedSessionId::for_adopted_tmux_name("tm-still-here").to_string())
.expect("the live session's claim");
assert_eq!(
live.liveness,
ClaimLiveness::Live,
"a session tmux still lists is live whatever its record's state says (#2919)"
);
}
struct SleepingTmux;
impl crate::session_manager::ManagedTmuxDriver for SleepingTmux {
fn create_session(
&self,
_n: &str,
_w: &str,
) -> Result<(), crate::session_manager::ManagedError> {
Ok(())
}
fn kill_session(&self, _n: &str) -> Result<(), crate::session_manager::ManagedError> {
Ok(())
}
fn send_line(&self, _n: &str, _t: &str) -> Result<(), crate::session_manager::ManagedError> {
Ok(())
}
fn capture(&self, _n: &str, _l: usize) -> Result<String, crate::session_manager::ManagedError> {
Ok(String::new())
}
fn list_sessions(&self) -> Result<Vec<String>, crate::session_manager::ManagedError> {
std::thread::sleep(std::time::Duration::from_secs(10));
Ok(Vec::new())
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_slow_tmux_probe_is_bounded_and_leaves_every_claim_live() {
let dir = TempDir::new().unwrap();
let workspace = PathBuf::from("/srv/projects/acme");
let mgr = SessionManager::new(dir.path(), std::sync::Arc::new(SleepingTmux))
.await
.unwrap();
{
let mut store = mgr.store.write().await;
store
.upsert(tombstoned("tm-gone", &workspace))
.await
.unwrap();
}
let started = std::time::Instant::now();
let claims = mgr.workspace_claims(None).await;
let elapsed = started.elapsed();
assert!(
elapsed < super::TMUX_PROBE_TIMEOUT + std::time::Duration::from_secs(3),
"the probe must bound itself; it took {elapsed:?} (#7965)"
);
assert!(
!claims.claims.is_empty(),
"the claim set must not empty out when tmux does not answer"
);
for claim in &claims.claims {
assert_eq!(
claim.liveness,
ClaimLiveness::Live,
"a probe that timed out must leave {} live",
claim.session
);
}
}
#[tokio::test]
async fn an_unobservable_tmux_leaves_every_claim_live() {
let dir = TempDir::new().unwrap();
let workspace = PathBuf::from("/srv/projects/acme");
let fake = FakeTmuxDriver::new();
fake.seeded_names
.lock()
.unwrap()
.push("tm-still-here".into());
let mgr = seeded(&dir, fake.clone(), &workspace).await;
*fake.list_sessions_should_fail.lock().unwrap() = true;
let claims = mgr.workspace_claims(None).await;
assert!(
!claims.claims.is_empty(),
"the claim set must not empty out when tmux cannot be read"
);
for claim in &claims.claims {
assert_eq!(
claim.liveness,
ClaimLiveness::Live,
"a probe that could not answer must leave {} live",
claim.session
);
}
}
#[tokio::test]
async fn an_unmanaged_tmux_name_is_never_read_as_dead() {
let dir = TempDir::new().unwrap();
let workspace = PathBuf::from("/srv/projects/acme");
let fake = FakeTmuxDriver::new();
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
{
let mut store = mgr.store.write().await;
store
.upsert(tombstoned("someone-elses-pane", &workspace))
.await
.unwrap();
}
let claims = mgr.workspace_claims(None).await;
assert_eq!(claims.claims.len(), 1);
assert_eq!(claims.claims[0].liveness, ClaimLiveness::Live);
}