use std::path::PathBuf;
use tempfile::TempDir;
use super::manager::{ManagedTmuxDriver, SessionManager};
use super::record::ManagedSessionState;
use super::tests::FakeTmuxDriver;
struct HomeGuard(Option<String>);
impl Drop for HomeGuard {
fn drop(&mut self) {
match self.0 {
Some(ref p) => unsafe { std::env::set_var("HOME", p) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
fn set_home(home: &std::path::Path) -> HomeGuard {
let prior = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", home) };
HomeGuard(prior)
}
#[tokio::test]
async fn manager_naming_convention() {
let dir = TempDir::new().unwrap();
let fake = FakeTmuxDriver::new();
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let record = mgr
.create(
"task".into(),
Some(PathBuf::from("/tmp/wt1")),
None,
None,
None,
None,
)
.await
.expect("create");
assert!(record.tmux_name.starts_with("tm-"), "has tm- prefix");
let suffix = record
.tmux_name
.rsplit('-')
.next()
.expect("name has at least one dash-delimited segment");
assert_eq!(
suffix.len(),
2,
"serial suffix is exactly 2 digits: {}",
record.tmux_name
);
assert!(
suffix.bytes().all(|b| b.is_ascii_digit()),
"serial suffix is numeric: {}",
record.tmux_name
);
let serial: u8 = suffix.parse().expect("suffix is numeric (checked above)");
assert!(
(1..=99).contains(&serial),
"serial is in 01-99: {}",
record.tmux_name
);
}
#[tokio::test]
async fn manager_serial_reuses_decommissioned_gap() {
let dir = TempDir::new().unwrap();
let fake = FakeTmuxDriver::new();
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let repo_url = Some("https://github.com/acme/gap-test".to_string());
let r1 = mgr
.create(
"t1".into(),
Some(PathBuf::from("/tmp/g1")),
None,
None,
repo_url.clone(),
None,
)
.await
.expect("create r1");
let r2 = mgr
.create(
"t2".into(),
Some(PathBuf::from("/tmp/g2")),
None,
None,
repo_url.clone(),
None,
)
.await
.expect("create r2");
let r3 = mgr
.create(
"t3".into(),
Some(PathBuf::from("/tmp/g3")),
None,
None,
repo_url.clone(),
None,
)
.await
.expect("create r3");
assert_eq!(r1.tmux_name, "tm-gap-test-01");
assert_eq!(r2.tmux_name, "tm-gap-test-02");
assert_eq!(r3.tmux_name, "tm-gap-test-03");
{
let mut store = mgr.store.write().await;
let mut rec = store.get(&r2.id).await.expect("get r2");
rec.state = ManagedSessionState::Decommissioned;
store.upsert(rec).await.expect("upsert decommissioned r2");
}
fake.kill_session(&r2.tmux_name).expect("kill r2 tmux");
let r4 = mgr
.create(
"t4".into(),
Some(PathBuf::from("/tmp/g4")),
None,
None,
repo_url.clone(),
None,
)
.await
.expect("create r4");
assert_eq!(
r4.tmux_name, "tm-gap-test-02",
"decommissioned serial 02 must be reused, got {}",
r4.tmux_name
);
}
#[tokio::test]
#[serial_test::serial]
async fn manager_reconcile_adopts_new_prefix_session() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-trusty-tools-01".into()],
pane_cwd: Some(workspace.path().to_path_buf()),
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert!(
report
.external_adopted
.contains(&"tm-trusty-tools-01".to_string()),
"a live tm-prefixed session must be externally adopted; report: {report:?}"
);
let listed = mgr.list().await;
let adopted = listed
.iter()
.find(|r| r.tmux_name == "tm-trusty-tools-01")
.expect("adopted record present");
assert_eq!(adopted.state, ManagedSessionState::Active);
}
struct PaneCwdTmux {
alive: Vec<String>,
pane_cwd: Option<PathBuf>,
}
impl ManagedTmuxDriver for PaneCwdTmux {
fn create_session(
&self,
_name: &str,
_workdir: &str,
) -> Result<(), super::manager::ManagedError> {
unimplemented!("not exercised by PaneCwdTmux tests")
}
fn kill_session(&self, _name: &str) -> Result<(), super::manager::ManagedError> {
unimplemented!("not exercised by PaneCwdTmux tests")
}
fn send_line(&self, _name: &str, _text: &str) -> Result<(), super::manager::ManagedError> {
unimplemented!("not exercised by PaneCwdTmux tests")
}
fn capture(&self, _name: &str, _lines: usize) -> Result<String, super::manager::ManagedError> {
unimplemented!("not exercised by PaneCwdTmux tests")
}
fn list_sessions(&self) -> Result<Vec<String>, super::manager::ManagedError> {
Ok(self.alive.clone())
}
fn get_pane_cwd(&self, _name: &str) -> Option<PathBuf> {
self.pane_cwd.clone()
}
}
#[tokio::test]
#[serial_test::serial]
async fn reconcile_resolves_adopted_session_cwd_from_pane() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-resolvable-01".into()],
pane_cwd: Some(workspace.path().to_path_buf()),
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
mgr.reconcile_on_boot(false).await.expect("reconcile");
let listed = mgr.list().await;
let adopted = listed
.iter()
.find(|r| r.tmux_name == "tm-resolvable-01")
.expect("adopted record present");
assert_eq!(adopted.cwd, workspace.path());
assert_eq!(adopted.workspace_path.as_deref(), Some(workspace.path()));
assert_eq!(adopted.task, "adopted session");
}
struct FlakyPaneCwdTmux {
alive: Vec<String>,
pane_cwd: Option<PathBuf>,
flaky_for: usize,
calls: std::sync::atomic::AtomicUsize,
pane_id: Option<String>,
}
impl ManagedTmuxDriver for FlakyPaneCwdTmux {
fn create_session(
&self,
_name: &str,
_workdir: &str,
) -> Result<(), super::manager::ManagedError> {
unimplemented!("not exercised by FlakyPaneCwdTmux tests")
}
fn kill_session(&self, _name: &str) -> Result<(), super::manager::ManagedError> {
unimplemented!("not exercised by FlakyPaneCwdTmux tests")
}
fn send_line(&self, _name: &str, _text: &str) -> Result<(), super::manager::ManagedError> {
unimplemented!("not exercised by FlakyPaneCwdTmux tests")
}
fn capture(&self, _name: &str, _lines: usize) -> Result<String, super::manager::ManagedError> {
unimplemented!("not exercised by FlakyPaneCwdTmux tests")
}
fn list_sessions(&self) -> Result<Vec<String>, super::manager::ManagedError> {
Ok(self.alive.clone())
}
fn get_pane_cwd(&self, _name: &str) -> Option<PathBuf> {
let n = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if n < self.flaky_for {
return None;
}
self.pane_cwd.clone()
}
fn get_pane_id(&self, _name: &str) -> Option<String> {
self.pane_id.clone()
}
}
#[tokio::test]
#[serial_test::serial]
async fn flaky_cwd_probe_still_adopts_within_one_reconcile() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(FlakyPaneCwdTmux {
alive: vec!["tm-flaky-01".into()],
pane_cwd: Some(workspace.path().to_path_buf()),
flaky_for: 1,
calls: std::sync::atomic::AtomicUsize::new(0),
pane_id: None,
});
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert!(
report.adoption_declined.is_empty(),
"a probe that recovers on retry must not be declined; report: {report:?}"
);
let listed = mgr.list().await;
assert_eq!(
listed.len(),
1,
"the pane must be adopted within one reconcile pass: {listed:?}"
);
assert_eq!(listed[0].tmux_name, "tm-flaky-01");
assert_eq!(listed[0].cwd, workspace.path());
assert_eq!(listed[0].workspace_path.as_deref(), Some(workspace.path()));
assert!(
fake.calls.load(std::sync::atomic::Ordering::SeqCst) >= 2,
"the first probe must have been retried"
);
}
#[tokio::test]
#[serial_test::serial]
async fn unresolvable_cwd_probe_is_retried_a_bounded_number_of_times() {
let dir = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(FlakyPaneCwdTmux {
alive: vec!["tm-never-01".into()],
pane_cwd: None,
flaky_for: usize::MAX,
calls: std::sync::atomic::AtomicUsize::new(0),
pane_id: None,
});
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert_eq!(report.adoption_declined, vec!["tm-never-01".to_string()]);
assert_eq!(mgr.list().await.len(), 0);
assert_eq!(
fake.calls.load(std::sync::atomic::Ordering::SeqCst),
3,
"the probe must be attempted exactly 3 times, then give up"
);
}
#[tokio::test]
#[serial_test::serial]
async fn a_stale_pane_path_is_declined_on_the_first_probe() {
let dir = TempDir::new().unwrap();
let _home = set_home(dir.path());
let reaped = dir.path().join("reaped-worktree");
assert!(!reaped.exists(), "the fixture path must not exist");
let fake = std::sync::Arc::new(FlakyPaneCwdTmux {
alive: vec!["tm-stale-01".into()],
pane_cwd: Some(reaped),
flaky_for: 0,
calls: std::sync::atomic::AtomicUsize::new(0),
pane_id: None,
});
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert_eq!(
fake.calls.load(std::sync::atomic::Ordering::SeqCst),
1,
"a path tmux keeps reporting cannot become a directory by being read again"
);
assert_eq!(report.adoption_declined, vec!["tm-stale-01".to_string()]);
assert!(report.external_adopted.is_empty());
assert_eq!(mgr.list().await.len(), 0);
}
#[tokio::test]
#[serial_test::serial]
async fn reconcile_declines_to_adopt_a_pane_whose_cwd_cannot_be_resolved() {
let dir = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-unresolvable-01".into()],
pane_cwd: None,
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert_eq!(
mgr.list().await.len(),
0,
"a pane with no resolvable cwd must leave no record behind"
);
assert!(
report
.adoption_declined
.contains(&"tm-unresolvable-01".to_string()),
"the declined pane must be named in the report; report: {report:?}"
);
assert!(
report.external_adopted.is_empty(),
"nothing was adopted; report: {report:?}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn repeated_reconcile_of_one_unresolvable_pane_stays_at_zero_records() {
let dir = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-ghosty-01".into()],
pane_cwd: None,
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
for pass in 1..=3 {
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert_eq!(
report.adoption_declined,
vec!["tm-ghosty-01".to_string()],
"pass {pass} must decline the pane; report: {report:?}"
);
assert_eq!(
mgr.list().await.len(),
0,
"pass {pass} must leave the store empty"
);
}
}
#[cfg(feature = "daemon")]
#[tokio::test]
#[serial_test::serial]
async fn a_declined_pane_is_reapable_by_the_orphan_gc() {
use crate::daemon::orphan_gc::{
AlwaysIdleProbe, GcDecision, PaneInfo, TrackedNames, classify_session,
};
let dir = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-leaked-01".into()],
pane_cwd: None,
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
mgr.reconcile_on_boot(false).await.expect("reconcile");
let managed = mgr.known_tmux_names().await.expect("known names");
assert!(
!managed.contains("tm-leaked-01"),
"a declined pane must not be in the orphan-GC's protected set: {managed:?}"
);
let decision = classify_session(
&PaneInfo {
session_name: "tm-leaked-01".into(),
pane_current_command: "sh".into(),
pane_pid: None,
pane_id: None,
pane_current_path: None,
},
&TrackedNames {
managed,
..Default::default()
},
&AlwaysIdleProbe,
&crate::daemon::orphan_gc::FsCwdProbe,
);
assert_eq!(
decision,
GcDecision::ReapCandidate,
"an idle leaked pane the daemon declined to adopt must be reapable"
);
}
#[tokio::test]
#[serial_test::serial]
async fn reconcile_skips_external_adopt_when_workspace_already_tracked() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-crossed-01".into()],
pane_cwd: Some(workspace.path().to_path_buf()),
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
let existing = super::record::SessionRecord {
id: super::record::ManagedSessionId::new(),
tmux_name: "tm-known-01".into(),
cwd: workspace.path().to_path_buf(),
task: "existing task".into(),
state: ManagedSessionState::Stopped,
created_at: chrono::Utc::now(),
last_activity_at: None,
workspace_path: Some(workspace.path().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(),
};
let existing_id = existing.id;
mgr.store.write().await.upsert(existing).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert!(
!report
.external_adopted
.contains(&"tm-crossed-01".to_string()),
"a live session resolving to an already-tracked workspace must never be \
externally adopted; report: {report:?}"
);
let listed = mgr.list().await;
assert_eq!(
listed.len(),
1,
"no second record must be minted for the same workspace; records: {listed:?}"
);
assert_eq!(
listed[0].id, existing_id,
"the original record must be the only one"
);
assert!(
!listed.iter().any(|r| r.tmux_name == "tm-crossed-01"),
"no record must carry the crossed tmux_name"
);
}
#[test]
fn adopted_id_is_stable_for_one_tmux_name() {
let a = super::record::ManagedSessionId::for_adopted_tmux_name("tm-stable-01");
let b = super::record::ManagedSessionId::for_adopted_tmux_name("tm-stable-01");
assert_eq!(a, b, "the same tmux name must derive the same id");
}
#[test]
fn adopted_id_differs_per_tmux_name() {
let a = super::record::ManagedSessionId::for_adopted_tmux_name("tm-one-01");
let b = super::record::ManagedSessionId::for_adopted_tmux_name("tm-two-01");
assert_ne!(a, b, "distinct tmux names must derive distinct ids");
}
#[tokio::test]
#[serial_test::serial]
async fn adopting_the_same_pane_twice_writes_one_record() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-raced-01".into()],
pane_cwd: Some(workspace.path().to_path_buf()),
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
mgr.reconcile_on_boot(false).await.expect("first reconcile");
let first = mgr.list().await;
assert_eq!(first.len(), 1, "first adoption wrote one record");
mgr.store
.write()
.await
.remove(&first[0].id)
.await
.expect("drop the record the second adopter never saw");
mgr.reconcile_on_boot(false)
.await
.expect("second reconcile");
let after = mgr.list().await;
assert_eq!(after.len(), 1, "one pane, one record: {after:?}");
assert_eq!(
after[0].id, first[0].id,
"re-adopting one tmux name must land on the same store key, so two \
concurrent adopters overwrite each other instead of both surviving"
);
}
#[tokio::test]
#[serial_test::serial]
async fn a_reused_adopted_id_carries_no_residual_state() {
let dir = TempDir::new().unwrap();
let first_ws = TempDir::new().unwrap();
let second_ws = TempDir::new().unwrap();
let _home = set_home(dir.path());
let first_pane = std::sync::Arc::new(FlakyPaneCwdTmux {
alive: vec!["tm-recycled-01".into()],
pane_cwd: Some(first_ws.path().to_path_buf()),
flaky_for: 0,
calls: std::sync::atomic::AtomicUsize::new(0),
pane_id: Some("%11".into()),
});
let mgr = SessionManager::new(dir.path(), first_pane).await.unwrap();
mgr.reconcile_on_boot(false).await.expect("first reconcile");
let listed = mgr.list().await;
assert_eq!(listed.len(), 1);
let reused_id = listed[0].id;
assert_eq!(
reused_id,
super::record::ManagedSessionId::for_adopted_tmux_name("tm-recycled-01")
);
{
let mut store = mgr.store.write().await;
let mut rec = store.get(&reused_id).await.expect("get adopted");
rec.deliverable_id = Some(crate::deliverable::record::DeliverableId::new());
rec.claude_session_id = Some("claude-abc".into());
rec.scrollback_path = Some(dir.path().join("scrollback.log"));
rec.last_activity_at = Some(chrono::Utc::now());
rec.correlation.branch = Some("feature/old".into());
store.upsert(rec).await.expect("seed residue");
}
let pids = crate::core::pid_registry::PidRegistry::new(dir.path().join("pids"));
pids.register(&reused_id.to_string(), 4242)
.expect("register pid");
mgr.store
.write()
.await
.remove(&reused_id)
.await
.expect("remove record");
let second_pane = std::sync::Arc::new(FlakyPaneCwdTmux {
alive: vec!["tm-recycled-01".into()],
pane_cwd: Some(second_ws.path().to_path_buf()),
flaky_for: 0,
calls: std::sync::atomic::AtomicUsize::new(0),
pane_id: Some("%22".into()),
});
let mgr2 = SessionManager::new(dir.path(), second_pane).await.unwrap();
mgr2.reconcile_on_boot(false)
.await
.expect("second reconcile");
let after = mgr2.list().await;
assert_eq!(after.len(), 1, "one record for one pane: {after:?}");
let fresh = &after[0];
assert_eq!(fresh.id, reused_id, "the derived id is reused by design");
assert_eq!(
fresh.pane_id.as_deref(),
Some("%22"),
"the record must track the NEW pane, not the one that held the name"
);
assert_eq!(fresh.workspace_path.as_deref(), Some(second_ws.path()));
assert_eq!(
fresh.deliverable_id, None,
"deliverable link must not carry over"
);
assert_eq!(
fresh.claude_session_id, None,
"claude session must not carry over"
);
assert_eq!(
fresh.scrollback_path, None,
"scrollback must not carry over"
);
assert_eq!(fresh.last_activity_at, None, "activity must not carry over");
assert_eq!(
fresh.correlation,
Default::default(),
"correlation must not carry over: {:?}",
fresh.correlation
);
let entries = pids.entries().expect("read pid registry");
assert_eq!(entries.len(), 1, "adoption must not write PID entries");
assert_eq!(entries[0].session_id, reused_id.to_string());
assert_eq!(entries[0].pid, 4242);
}
#[tokio::test]
#[serial_test::serial]
async fn repeated_reconcile_of_one_resolvable_pane_keeps_one_record() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec!["tm-steady-01".into()],
pane_cwd: Some(workspace.path().to_path_buf()),
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
for _ in 0..3 {
mgr.reconcile_on_boot(false).await.expect("reconcile");
}
let listed = mgr.list().await;
assert_eq!(listed.len(), 1, "three passes, one record: {listed:?}");
assert_eq!(listed[0].tmux_name, "tm-steady-01");
assert_eq!(listed[0].state, ManagedSessionState::Active);
assert_eq!(listed[0].cwd, workspace.path());
assert_eq!(listed[0].workspace_path.as_deref(), Some(workspace.path()));
assert_eq!(listed[0].task, "adopted session");
}
#[tokio::test]
async fn create_with_reserved_name_suffixes_stale_reservation() {
let dir = TempDir::new().unwrap();
let fake = FakeTmuxDriver::new();
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let first = mgr
.create(
"squatter".into(),
Some(PathBuf::from("/tmp/wt1")),
Some("stale".into()),
None,
None,
None,
)
.await
.expect("first create");
assert_eq!(first.tmux_name, "tm-stale-01");
let second = mgr
.create_with_reserved_name(
super::record::ManagedSessionId::new(),
"tm-stale-01".into(),
"stale reservation".into(),
Some(PathBuf::from("/tmp/wt2")),
None,
None,
None,
Default::default(),
false,
false,
)
.await
.expect("a stale reservation must auto-suffix, never collide");
assert_eq!(
second.tmux_name, "tm-stale-02",
"the stale reserved name must be suffixed to the next free ordinal"
);
assert!(
fake.session_exists("tm-stale-02"),
"the tmux session must be created under the SUFFIXED name"
);
assert_eq!(
mgr.get(&first.id).await.expect("get first").tmux_name,
"tm-stale-01"
);
}
#[tokio::test]
async fn dedupe_name_against_caps_suffixed_length_at_64() {
let long = "x".repeat(64); let taken: std::collections::HashSet<String> = [long.clone()].into();
let result = SessionManager::dedupe_name_against(&long, &taken);
assert!(
result.chars().count() <= 64,
"suffixed result must stay within the 64-char cap, got {} chars: {result}",
result.chars().count()
);
assert_ne!(result, long, "the taken candidate must still be suffixed");
}
#[test]
fn dedupe_suffixes_an_unnumbered_name_with_two_digits() {
let taken = |names: &[&str]| -> std::collections::HashSet<String> {
names.iter().map(|n| n.to_string()).collect()
};
let dedupe = SessionManager::dedupe_name_against;
assert_eq!(dedupe("tm-localizer", &taken(&[])), "tm-localizer");
assert_eq!(
dedupe("tm-localizer", &taken(&["tm-localizer"])),
"tm-localizer-02"
);
assert_eq!(
dedupe("tm-localizer", &taken(&["tm-localizer", "tm-localizer-02"])),
"tm-localizer-03"
);
assert_eq!(dedupe("tm-i8n-01", &taken(&["tm-i8n-01"])), "tm-i8n-02");
assert_eq!(dedupe("tm-old-2", &taken(&["tm-old-2"])), "tm-old-3");
}
#[tokio::test]
#[serial_test::serial]
async fn reconcile_refuses_to_adopt_a_reserved_test_session() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let _home = set_home(dir.path());
let leaked = format!(
"{}deadrt4242-01",
trusty_common::session_naming::RESERVED_TEST_PREFIX
);
let fake = std::sync::Arc::new(PaneCwdTmux {
alive: vec![leaked.clone()],
pane_cwd: Some(workspace.path().to_path_buf()),
});
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert_eq!(
mgr.list().await.len(),
0,
"a leaked test session must leave no record for the picker to show"
);
assert!(
report.reserved_test_refused.contains(&leaked),
"the refused session must be named in the report; report: {report:?}"
);
assert!(
report.external_adopted.is_empty() && report.adoption_declined.is_empty(),
"refusal is by name, not a cwd-resolution decline; report: {report:?}"
);
}
#[tokio::test]
async fn reconcile_tombstones_an_adopted_reserved_record_whose_tmux_is_gone() {
let dir = crate::test_support::hermetic_temp_dir();
let fake = FakeTmuxDriver::new();
let mgr = SessionManager::new(dir.path(), fake).await.unwrap();
let leaked_name = reserved_name("deadrt4242-01");
let leaked = super::tests::make_active_test_record(
&leaked_name,
super::record::ADOPTED_TASK,
"/tmp/leak-ws",
);
let ordinary = super::tests::make_active_test_record("tm-trusty-tools-01", "real", "/tmp/ws");
let created =
super::tests::make_active_test_record(&reserved_name("foo-01"), "real work", "/tmp/ws2");
{
let mut store = mgr.store.write().await;
store.upsert(leaked.clone()).await.unwrap();
store.upsert(ordinary.clone()).await.unwrap();
store.upsert(created.clone()).await.unwrap();
}
let leak_root = std::path::PathBuf::from("/tmp/leak-ws");
mgr.residency_cache_store(leaked.id, leak_root.clone(), None, Vec::new())
.await;
let generation_before = mgr.residency_generation();
let report = mgr.reconcile_on_boot(false).await.expect("reconcile");
assert!(
mgr.residency_generation() > generation_before,
"the sweep leaves the active-project set, so it must bump the \
residency generation: before={generation_before} after={}",
mgr.residency_generation()
);
assert_eq!(
mgr.residency_cache_lookup(&leaked.id, &leak_root).await,
None,
"a swept record is terminal — its derivation cache entry must go with it"
);
let after = mgr.get(&leaked.id).await.unwrap();
assert_eq!(
after.state,
ManagedSessionState::Deleted,
"a leaked test record with no tmux session must be tombstoned, not left resumable"
);
assert!(
after.terminal_at.is_some(),
"the retention clock must start when the record goes terminal"
);
assert!(
report.reserved_test_swept.contains(&leaked.id.to_string())
&& !report.stopped.contains(&leaked.id.to_string()),
"the swept record belongs in exactly one list; report: {report:?}"
);
for (record, why) in [
(&ordinary, "an ordinary gone session stays Stopped"),
(&created, "a CREATED session in the namespace stays Stopped"),
] {
assert_eq!(
mgr.get(&record.id).await.unwrap().state,
ManagedSessionState::Stopped,
"{why}"
);
assert!(report.stopped.contains(&record.id.to_string()));
assert!(!report.reserved_test_swept.contains(&record.id.to_string()));
}
}
#[tokio::test]
async fn a_live_leaked_test_pane_is_never_readopted_or_recreated() {
let dir = crate::test_support::hermetic_temp_dir();
let fake = FakeTmuxDriver::new();
let leaked_name = reserved_name("deadrt4242-01");
fake.seeded_names.lock().unwrap().push(leaked_name.clone());
let mgr = SessionManager::new(dir.path(), fake.clone()).await.unwrap();
let leaked = super::tests::make_active_test_record(
&leaked_name,
super::record::ADOPTED_TASK,
"/tmp/leak-ws",
);
{
let mut store = mgr.store.write().await;
store.upsert(leaked.clone()).await.unwrap();
}
let creates_before = fake.create_cwd_calls.lock().unwrap().len();
let report = mgr.reconcile_on_boot(true).await.expect("reconcile");
let after = mgr.get(&leaked.id).await.unwrap();
assert_eq!(
after.state,
ManagedSessionState::Deleted,
"a live leaked pane must not keep its record Active — that is what sustained the loop"
);
assert!(
!after.is_auto_resumable(),
"no automatic path may relaunch a leaked test session"
);
assert!(
!report.adopted.contains(&leaked_name),
"the live pane must not be re-adopted; report: {report:?}"
);
assert_eq!(
fake.create_cwd_calls.lock().unwrap().len(),
creates_before,
"nothing may recreate the pane"
);
assert!(
fake.kill_calls.lock().unwrap().is_empty(),
"reconcile drops the RECORD; killing the pane is the orphan-GC's decision, \
and it spares a pane that is not an idle shell"
);
}
fn reserved_name(tag: &str) -> String {
format!(
"{}{tag}",
trusty_common::session_naming::RESERVED_TEST_PREFIX
)
}