use std::path::PathBuf;
use chrono::Utc;
use tempfile::TempDir;
use super::manager::ManagedError;
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
use super::tests::make_manager;
#[tokio::test]
async fn manager_adopt_existing_registers_active() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
fake.seeded_names
.lock()
.unwrap()
.push("tmpm-hand-started".into());
let record = mgr
.adopt_existing(
"tmpm-hand-started",
PathBuf::from("/Users/op/work/proj"),
"drive my hand-started session".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("adopt existing");
assert_eq!(record.tmux_name, "tmpm-hand-started");
assert_eq!(record.state, ManagedSessionState::Active);
assert_eq!(record.cwd, PathBuf::from("/Users/op/work/proj"));
assert_eq!(record.task, "drive my hand-started session");
assert!(
fake.create_cwd_calls.lock().unwrap().is_empty(),
"adopt_existing must NOT call create_session; calls: {:?}",
fake.create_cwd_calls.lock().unwrap()
);
let got = mgr.get(&record.id).await.expect("get adopted");
assert_eq!(got.id, record.id);
assert_eq!(got.state, ManagedSessionState::Active);
}
#[tokio::test]
async fn manager_adopt_existing_missing_tmux_errors() {
let dir = TempDir::new().unwrap();
let (mgr, _fake) = make_manager(&dir).await;
let err = mgr
.adopt_existing(
"tmpm-not-here",
PathBuf::from("/tmp/x"),
String::new(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect_err("adopting a nonexistent pane must fail");
assert!(
matches!(err, ManagedError::TmuxSessionMissing(ref n) if n == "tmpm-not-here"),
"expected TmuxSessionMissing, got {err:?}"
);
}
#[tokio::test]
async fn manager_adopt_existing_double_adopt_errors() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
fake.seeded_names.lock().unwrap().push("tmpm-once".into());
mgr.adopt_existing(
"tmpm-once",
PathBuf::from("/tmp/once"),
String::new(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("first adopt succeeds");
let err = mgr
.adopt_existing(
"tmpm-once",
PathBuf::from("/tmp/once"),
String::new(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect_err("second adopt of the same pane must fail");
assert!(
matches!(err, ManagedError::AlreadyAdopted(ref n) if n == "tmpm-once"),
"expected AlreadyAdopted, got {err:?}"
);
}
#[tokio::test]
async fn manager_adopt_existing_allows_non_tmpm_name() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
fake.seeded_names
.lock()
.unwrap()
.push("my-cli-session".into());
let record = mgr
.adopt_existing(
"my-cli-session",
PathBuf::from("/Users/op/repo"),
"adopt non-prefixed".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("non-tmpm names are adoptable on the explicit path");
assert_eq!(record.tmux_name, "my-cli-session");
assert_eq!(record.state, ManagedSessionState::Active);
}
#[tokio::test]
async fn manager_adopt_existing_persists_ephemeral_flag() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
fake.seeded_names
.lock()
.unwrap()
.push("tmpm-eph-adopt".into());
let record = mgr
.adopt_existing(
"tmpm-eph-adopt",
PathBuf::from("/tmp/adopt"),
"throwaway adopt".into(),
crate::runtime::RuntimeKind::default(),
true,
)
.await
.expect("adopt ephemeral");
assert!(
mgr.get(&record.id).await.unwrap().ephemeral,
"adopted ephemeral flag persisted"
);
}
fn bare_record(
tmux_name: &str,
state: ManagedSessionState,
pane_id: Option<&str>,
) -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: tmux_name.into(),
cwd: PathBuf::from("/tmp/bare"),
task: "seed".into(),
state,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
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: pane_id.map(str::to_string),
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
}
}
#[tokio::test]
async fn manager_adopt_existing_reuses_name_freed_by_decommissioned_record() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
let gone = bare_record(
"tm-freed",
ManagedSessionState::Decommissioned,
Some("old-pane"),
);
mgr.store
.write()
.await
.upsert(gone)
.await
.expect("seed decommissioned tombstone");
fake.seeded_names.lock().unwrap().push("tm-freed".into());
let record = mgr
.adopt_existing(
"tm-freed",
PathBuf::from("/tmp/new"),
"fresh adoption".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("a decommissioned tombstone's name must be reusable, unsuffixed");
assert_eq!(
record.tmux_name, "tm-freed",
"a terminal record's name must not block reuse or force a suffix"
);
}
#[tokio::test]
async fn manager_adopt_existing_suffixes_recycled_name() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
*fake.pane_id_override.lock().unwrap() = Some("pane-old".into());
fake.seeded_names.lock().unwrap().push("tm-recycled".into());
let first = mgr
.adopt_existing(
"tm-recycled",
PathBuf::from("/tmp/first"),
"first pane".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("first adopt succeeds");
assert_eq!(first.tmux_name, "tm-recycled");
*fake.pane_id_override.lock().unwrap() = Some("pane-new".into());
let second = mgr
.adopt_existing(
"tm-recycled",
PathBuf::from("/tmp/second"),
"recycled pane".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("a recycled name must auto-suffix, never reject");
assert_eq!(second.tmux_name, "tm-recycled-2");
assert_ne!(first.id, second.id);
assert!(
fake.rename_calls
.lock()
.unwrap()
.iter()
.any(|(o, n)| o == "tm-recycled" && n == "tm-recycled-2"),
"expected rename_session(tm-recycled -> tm-recycled-2), got {:?}",
fake.rename_calls.lock().unwrap()
);
assert_eq!(
mgr.get(&first.id).await.expect("get stale").state,
ManagedSessionState::Stopped,
"the provably-dead Active collider must be converged to Stopped"
);
}
#[tokio::test]
async fn manager_adopt_existing_recycled_name_skips_to_next_free_ordinal() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
*fake.pane_id_override.lock().unwrap() = Some("pane-1".into());
fake.seeded_names.lock().unwrap().push("tm-recycled".into());
mgr.adopt_existing(
"tm-recycled",
PathBuf::from("/tmp/1"),
"pane 1".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("first adopt succeeds");
*fake.pane_id_override.lock().unwrap() = Some("pane-2".into());
let second = mgr
.adopt_existing(
"tm-recycled",
PathBuf::from("/tmp/2"),
"pane 2".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("second adopt auto-suffixes to -2");
assert_eq!(second.tmux_name, "tm-recycled-2");
*fake.pane_id_override.lock().unwrap() = Some("pane-3".into());
let third = mgr
.adopt_existing(
"tm-recycled",
PathBuf::from("/tmp/3"),
"pane 3".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("third adopt must skip the taken -2 and land on -3");
assert_eq!(third.tmux_name, "tm-recycled-3");
}
#[cfg(unix)]
#[tokio::test]
async fn manager_adopt_existing_rolls_back_tmux_rename_when_store_write_fails() {
use std::os::unix::fs::PermissionsExt;
use super::manager::ManagedError;
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
*fake.pane_id_override.lock().unwrap() = Some("pane-old".into());
fake.seeded_names.lock().unwrap().push("tm-roll".into());
mgr.adopt_existing(
"tm-roll",
PathBuf::from("/tmp/first"),
"first pane".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect("first adopt succeeds");
*fake.pane_id_override.lock().unwrap() = Some("pane-new".into());
let dir_perms = std::fs::metadata(dir.path()).expect("meta").permissions();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o555))
.expect("make store dir read-only");
let err = mgr
.adopt_existing(
"tm-roll",
PathBuf::from("/tmp/second"),
"recycled pane".into(),
crate::runtime::RuntimeKind::default(),
false,
)
.await
.expect_err("adopt must fail when the store write fails");
std::fs::set_permissions(dir.path(), dir_perms).expect("restore perms");
assert!(
matches!(err, ManagedError::InvalidState(_, _)),
"got {err:?}"
);
let renames = fake.rename_calls.lock().unwrap();
assert!(
renames
.iter()
.any(|(o, n)| o == "tm-roll" && n == "tm-roll-2"),
"the forward rename must have happened: {renames:?}"
);
assert!(
renames
.iter()
.any(|(o, n)| o == "tm-roll-2" && n == "tm-roll"),
"the compensating rollback rename must have happened: {renames:?}"
);
}