use super::decommission::{
WORKTREE_SENTINEL_FILE, is_session_worktree, remove_session_worktree, worktrees_dirname,
};
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 worktrees_dirname_delegates_to_the_shared_resolver() {
let _guard = crate::core::trusty_tools_config::env_test_lock();
unsafe { std::env::set_var("TRUSTY_MPM_WORKTREES_DIRNAME", ".sessions") };
let configured = worktrees_dirname();
unsafe { std::env::remove_var("TRUSTY_MPM_WORKTREES_DIRNAME") };
assert_eq!(
configured, ".sessions",
"the loaded-config delegation must carry the resolved base through"
);
}
#[test]
fn configured_base_can_never_claim_a_claude_agent_worktree() {
let _guard = crate::core::trusty_tools_config::env_test_lock();
unsafe { std::env::set_var("TRUSTY_MPM_WORKTREES_DIRNAME", "worktrees") };
let agent_wt = std::path::Path::new(
"/Users/dev/trusty-mpm-projects/owner/repo/.claude/worktrees/agent-abc123",
);
let observed_session = is_session_worktree(agent_wt);
let observed_provisioned = crate::session_manager::worktree_reclaim::tm_provisioned(agent_wt);
unsafe { std::env::remove_var("TRUSTY_MPM_WORKTREES_DIRNAME") };
assert!(
!observed_session,
"`.claude/worktrees/<agent>` must never be a tm session worktree, whatever \
TRUSTY_MPM_WORKTREES_DIRNAME says"
);
assert!(
!observed_provisioned,
"the remover's own predicate must refuse a Claude Code agent worktree \
(ADR-0020 puts that store out of scope)"
);
}
#[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.removed(),
"absent path should report Removed (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.removed(),
"remove_session_worktree must refuse a non-worktrees dir without a sentinel"
);
assert!(
result.reason().is_some_and(|r| r.contains("sentinel")),
"#4732: and must say WHY, so the caller can report it: {result:?}"
);
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),
terminal_at: None,
stop_cause: None,
}
}
#[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))
.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))
.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))
.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)
.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)
.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());
}