use super::decommission::{
WORKTREE_SENTINEL_FILE, is_session_worktree, removal_permitted, remove_session_worktree,
worktrees_dirname,
};
use super::manager::{ManagedError, SessionManager};
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
#[test]
fn removal_permitted_admits_all_three_tiers() {
let tmp = crate::test_support::hermetic_temp_dir();
let parked = tmp.path().join("parked");
std::fs::create_dir_all(&parked).expect("mkdir");
assert!(!removal_permitted(&parked));
std::fs::write(parked.join(WORKTREE_SENTINEL_FILE), b"").expect("write sentinel");
assert!(removal_permitted(&parked));
let convention = tmp.path().join(".worktrees").join("some-session");
std::fs::create_dir_all(&convention).expect("mkdir");
assert!(removal_permitted(&convention));
let agent_store = tmp
.path()
.join(".claude")
.join("worktrees")
.join("agent-aa049179cd3e4e1bb");
std::fs::create_dir_all(&agent_store).expect("mkdir");
assert!(
removal_permitted(&agent_store),
"a `.claude/worktrees/<name>` leaf is the harness store and is trusty-mpm's to reclaim"
);
}
#[test]
fn removal_permitted_refuses_a_user_directory() {
let tmp = crate::test_support::hermetic_temp_dir();
for rel in [
"src",
"worktrees/not-under-dot-claude",
".claude/worktrees/agent-x/crates",
".claude/settings",
] {
let path = tmp.path().join(rel);
std::fs::create_dir_all(&path).expect("mkdir");
assert!(
!removal_permitted(&path),
"{rel} carries none of the three ownership marks and must be refused"
);
}
}
#[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_harness = super::worktree_ownership::is_harness_agent_worktree(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 — the session paths (decommission, \
search-index GC, --reset-agents-workspaces) all key on that predicate"
);
assert!(
observed_harness,
"it is the harness agent store, and that classification must come from the \
path shape rather than from a config value a rename could flip (#6561)"
);
}
#[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, "test");
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, "test");
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, "test");
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());
}
fn record_claiming(
id: ManagedSessionId,
state: ManagedSessionState,
workspace_path: std::path::PathBuf,
) -> SessionRecord {
let mut record = owned_record(id, state);
record.workspace_path = Some(workspace_path);
record.worktree_owner = None;
record
}
#[tokio::test]
async fn decommission_refuses_a_foreign_active_workspace_collision() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let shared = std::path::PathBuf::from("/tmp/shared-collision-workspace");
let session_a = ManagedSessionId::new();
let session_b = ManagedSessionId::new();
mgr.store
.write()
.await
.upsert(record_claiming(
session_a,
ManagedSessionState::Active,
shared.clone(),
))
.await
.expect("upsert A");
mgr.store
.write()
.await
.upsert(record_claiming(
session_b,
ManagedSessionState::Active,
shared,
))
.await
.expect("upsert B");
let managed_root = crate::test_support::hermetic_temp_dir();
let err = mgr
.decommission_with_root(&session_b, managed_root.path(), None)
.await
.expect_err("a second Active record naming the same workspace_path must refuse");
match err {
ManagedError::ForeignActiveWorkspaceClaim(target, other) => {
assert_eq!(target, session_b);
assert_eq!(other, session_a);
}
other => panic!("expected ForeignActiveWorkspaceClaim, got {other:?}"),
}
let record = mgr.get(&session_b).await.expect("get B");
assert_eq!(record.state, ManagedSessionState::Active);
}
#[tokio::test]
async fn decommission_refuses_a_foreign_active_cwd_collision() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let shared = std::path::PathBuf::from("/tmp/shared-collision-cwd");
let session_a = ManagedSessionId::new();
let session_b = ManagedSessionId::new();
let mut record_a = owned_record(session_a, ManagedSessionState::Active);
record_a.cwd = shared.clone();
mgr.store
.write()
.await
.upsert(record_a)
.await
.expect("upsert A");
mgr.store
.write()
.await
.upsert(record_claiming(
session_b,
ManagedSessionState::Active,
shared,
))
.await
.expect("upsert B");
let managed_root = crate::test_support::hermetic_temp_dir();
let err = mgr
.decommission_with_root(&session_b, managed_root.path(), None)
.await
.expect_err("a sibling's cwd matching this workspace_path must refuse");
assert!(matches!(
err,
ManagedError::ForeignActiveWorkspaceClaim(_, _)
));
}
#[tokio::test]
async fn decommission_allows_a_terminal_sibling_sharing_the_path() {
let dir = crate::test_support::hermetic_temp_dir();
let mgr = SessionManager::new(
dir.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
let shared = std::path::PathBuf::from("/tmp/shared-terminal-path");
let session_a = ManagedSessionId::new();
let session_b = ManagedSessionId::new();
mgr.store
.write()
.await
.upsert(record_claiming(
session_a,
ManagedSessionState::Decommissioned,
shared.clone(),
))
.await
.expect("upsert A");
mgr.store
.write()
.await
.upsert(record_claiming(
session_b,
ManagedSessionState::Active,
shared,
))
.await
.expect("upsert B");
let managed_root = crate::test_support::hermetic_temp_dir();
let (record, _workspace_removed) = mgr
.decommission_with_root(&session_b, managed_root.path(), None)
.await
.expect("a terminal sibling's stale claim must never block decommission");
assert_eq!(record.state, ManagedSessionState::Decommissioned);
}
#[tokio::test]
async fn decommission_allows_an_uncontested_workspace() {
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(record_claiming(
session_id,
ManagedSessionState::Active,
std::path::PathBuf::from("/tmp/nobody-else-here"),
))
.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("an uncontested workspace must decommission normally");
assert_eq!(record.state, ManagedSessionState::Decommissioned);
}