use std::path::PathBuf;
use tempfile::TempDir;
use super::manager::{ManagedTmuxDriver, SessionManager};
use super::record::ManagedSessionState;
use super::tests::FakeTmuxDriver;
#[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]
async fn manager_reconcile_adopts_new_prefix_session() {
let dir = TempDir::new().unwrap();
let fake = FakeTmuxDriver::new();
fake.seeded_names
.lock()
.unwrap()
.push("tm-trusty-tools-01".into());
let mgr = SessionManager::new(dir.path(), fake.clone()).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]
async fn reconcile_resolves_adopted_session_cwd_from_pane() {
let dir = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
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");
}
#[tokio::test]
async fn reconcile_flags_unresolvable_adopted_session_as_unmanaged() {
let dir = TempDir::new().unwrap();
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();
mgr.reconcile_on_boot(false).await.expect("reconcile");
let listed = mgr.list().await;
let adopted = listed
.iter()
.find(|r| r.tmux_name == "tm-unresolvable-01")
.expect("adopted record present");
assert_eq!(adopted.cwd, PathBuf::from("/unknown"));
assert!(adopted.workspace_path.is_none());
assert!(
adopted.task.contains("unmanaged"),
"task must clearly flag the record as unmanaged, got: {}",
adopted.task
);
}