use super::decommission::{WORKTREE_SENTINEL_FILE, is_session_worktree, remove_session_worktree};
use super::manager::{ManagedError, SessionManager};
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
#[test]
fn is_session_worktree_detects_dot_worktrees_component() {
assert!(is_session_worktree(std::path::Path::new(
"/home/user/repo/.worktrees/session-abc"
)));
assert!(is_session_worktree(std::path::Path::new(
"/some/base/.worktrees/session-id"
)));
assert!(!is_session_worktree(std::path::Path::new(
"/home/user/repo/session-id"
)));
assert!(!is_session_worktree(std::path::Path::new(
"/base/.worktrees/deep/path"
)));
assert!(!is_session_worktree(std::path::Path::new(
"/base/worktrees/session"
)));
}
#[test]
fn is_session_worktree_absent_path_is_noop() {
let absent = std::path::Path::new("/nonexistent/.worktrees/session-abc");
assert!(is_session_worktree(absent));
let result = remove_session_worktree(absent);
assert!(
result,
"absent path should return true (idempotently removed)"
);
}
#[test]
fn sentinel_gates_worktree_removal_refuses_non_worktrees_dir_without_sentinel() {
let dir = tempfile::tempdir().expect("tempdir");
let wt_path = dir.path().to_path_buf();
assert!(!wt_path.join(WORKTREE_SENTINEL_FILE).exists());
assert!(
!is_session_worktree(&wt_path),
"test invariant: parent must NOT be .worktrees for this branch"
);
let result = remove_session_worktree(&wt_path);
assert!(
!result,
"remove_session_worktree must return false for non-worktrees dir without sentinel"
);
assert!(
wt_path.exists(),
"non-SM directory must remain on disk after refused removal"
);
}
#[test]
fn sentinel_present_passes_safety_gate() {
let dir = tempfile::tempdir().expect("tempdir");
let wt_path = dir.keep();
std::fs::write(wt_path.join(WORKTREE_SENTINEL_FILE), b"").expect("write sentinel");
remove_session_worktree(&wt_path);
assert!(
!wt_path.exists(),
"sentinel present: safety gate must pass and directory must be removed"
);
}
fn owned_record(id: ManagedSessionId, state: ManagedSessionState) -> SessionRecord {
SessionRecord {
id,
tmux_name: format!("tm-owner-gate-{id}"),
cwd: std::path::PathBuf::from("/tmp"),
task: "task".into(),
state,
created_at: chrono::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: None,
injection_status: Default::default(),
worktree_owner: Some(id),
}
}
#[tokio::test]
async fn decommission_owner_gate_refuses_foreign_caller() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let session_b = ManagedSessionId::new();
mgr.store
.write()
.await
.upsert(owned_record(session_b, ManagedSessionState::Active))
.await
.expect("upsert B");
let session_a = ManagedSessionId::new();
let managed_root = crate::test_support::hermetic_temp_dir();
let err = mgr
.decommission_with_root(&session_b, managed_root.path(), Some(session_a), false)
.await
.expect_err("session A must be refused decommissioning session B's live worktree");
match err {
ManagedError::WorktreeOwnerMismatch(caller, owner, target) => {
assert_eq!(caller, session_a);
assert_eq!(owner, session_b);
assert_eq!(target, session_b);
}
other => panic!("expected WorktreeOwnerMismatch, got {other:?}"),
}
let record = mgr.get(&session_b).await.expect("get B");
assert_eq!(record.state, ManagedSessionState::Active);
}
#[tokio::test]
async fn decommission_owner_gate_allows_self() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let session_id = ManagedSessionId::new();
mgr.store
.write()
.await
.upsert(owned_record(session_id, ManagedSessionState::Active))
.await
.expect("upsert");
let managed_root = crate::test_support::hermetic_temp_dir();
let (record, _workspace_removed) = mgr
.decommission_with_root(&session_id, managed_root.path(), Some(session_id), false)
.await
.expect("self-decommission must be allowed");
assert_eq!(record.state, ManagedSessionState::Decommissioned);
}
#[tokio::test]
async fn decommission_owner_gate_allows_terminal_owner() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let target = ManagedSessionId::new();
mgr.store
.write()
.await
.upsert(owned_record(target, ManagedSessionState::Decommissioned))
.await
.expect("upsert target");
let caller = ManagedSessionId::new();
let managed_root = crate::test_support::hermetic_temp_dir();
mgr.decommission_with_root(&target, managed_root.path(), Some(caller), false)
.await
.expect("decommissioning an already-terminal, provably-ownerless target must be allowed");
}
#[tokio::test]
async fn decommission_owner_gate_bypassed_for_none_caller() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let session_id = ManagedSessionId::new();
mgr.store
.write()
.await
.upsert(owned_record(session_id, ManagedSessionState::Active))
.await
.expect("upsert");
let managed_root = crate::test_support::hermetic_temp_dir();
let (record, _workspace_removed) = mgr
.decommission_with_root(&session_id, managed_root.path(), None, false)
.await
.expect("operator (caller=None) decommission must never be gated");
assert_eq!(record.state, ManagedSessionState::Decommissioned);
}
#[tokio::test]
async fn decommission_clears_pending_decision() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let session_id = ManagedSessionId::new();
let mut record = owned_record(session_id, ManagedSessionState::Active);
record.pending_decision =
Some("T4: irreversible operation; human confirmation required".into());
record.proposed_default = Some("use cursor".into());
mgr.store
.write()
.await
.upsert(record)
.await
.expect("upsert");
let managed_root = crate::test_support::hermetic_temp_dir();
let (after, _workspace_removed) = mgr
.decommission_with_root(&session_id, managed_root.path(), None, false)
.await
.expect("decommission");
assert_eq!(after.state, ManagedSessionState::Decommissioned);
assert!(
after.pending_decision.is_none(),
"pending_decision must be cleared on decommission"
);
assert!(
after.proposed_default.is_none(),
"proposed_default must be cleared on decommission"
);
let stored = mgr.get(&session_id).await.expect("get");
assert!(stored.pending_decision.is_none());
assert!(stored.proposed_default.is_none());
}