use super::*;
use crate::core::agent::DelegationId;
use crate::core::session::SessionId;
use crate::session_manager::decommission::WORKTREE_SENTINEL_FILE;
use crate::session_manager::worktree_ownership::AgentWorktreeOwner;
use chrono::Utc;
fn agent_owner(agent_id: &str) -> SentinelOwner {
SentinelOwner::Agent(
AgentWorktreeOwner {
agent_id: agent_id.to_string(),
delegation_id: DelegationId::new(),
parent_session_id: SessionId::new(),
},
Utc::now(),
)
}
#[test]
fn adoption_refuses_a_live_owner() {
let dir = tempfile::tempdir().unwrap();
let verdict = evaluate_adoption(
dir.path(),
&agent_owner("agent-still-working"),
OwnerLiveness::Alive,
&[],
);
let AdoptionVerdict::Refuse(reason) = verdict else {
panic!("a live owner's tree must never be adoptable");
};
assert!(reason.contains("agent-still-working"), "{reason}");
assert!(reason.contains("still running"), "{reason}");
}
#[test]
fn adoption_refuses_an_owner_the_registry_never_heard_of() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
OwnerLiveness::from_agent_state(AgentDelegationState::Unknown),
OwnerLiveness::Undeterminable,
);
let verdict = evaluate_adoption(
dir.path(),
&agent_owner("agent-unheard-of"),
OwnerLiveness::Undeterminable,
&[],
);
assert!(
!matches!(verdict, AdoptionVerdict::Adopt),
"an unanswerable claim must refuse"
);
}
#[test]
fn adoption_refuses_an_unreadable_claim() {
let dir = tempfile::tempdir().unwrap();
let verdict = evaluate_adoption(
dir.path(),
&SentinelOwner::Unknown,
OwnerLiveness::Dead,
&[],
);
assert!(
!matches!(verdict, AdoptionVerdict::Adopt),
"an unreadable sentinel must refuse even when the caller says the owner is dead"
);
}
#[test]
fn adoption_refuses_a_tree_something_still_works_in() {
let dir = tempfile::tempdir().unwrap();
let verdict = evaluate_adoption(
dir.path(),
&agent_owner("agent-that-ended"),
OwnerLiveness::Dead,
&["rust-engineer".to_string()],
);
let AdoptionVerdict::Refuse(reason) = verdict else {
panic!("a tree with a live claimant must not be adoptable");
};
assert!(reason.contains("rust-engineer"), "{reason}");
}
#[test]
fn adoption_takes_a_dead_owners_tree() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
OwnerLiveness::from_agent_state(AgentDelegationState::Ended),
OwnerLiveness::Dead,
);
assert_eq!(
evaluate_adoption(
dir.path(),
&agent_owner("agent-that-ended"),
OwnerLiveness::Dead,
&[],
),
AdoptionVerdict::Adopt,
);
}
#[test]
fn adoption_handles_a_session_owned_tree_by_the_same_rule() {
let dir = tempfile::tempdir().unwrap();
let owner = SentinelOwner::Known(ManagedSessionId::new(), Utc::now());
assert_eq!(
evaluate_adoption(dir.path(), &owner, OwnerLiveness::Dead, &[]),
AdoptionVerdict::Adopt
);
assert!(!matches!(
evaluate_adoption(dir.path(), &owner, OwnerLiveness::Alive, &[]),
AdoptionVerdict::Adopt
));
}
#[test]
fn lock_fallback_takes_a_dead_pids_tree() {
let (liveness, evidence) = liveness_with_lock_fallback(
OwnerLiveness::Undeterminable,
LockEvidence::HolderPidGone(17167),
);
assert_eq!(liveness, OwnerLiveness::Dead);
let reason = evidence.expect("an adoption on this path must carry its evidence");
assert!(reason.contains("17167"), "{reason}");
assert!(reason.contains("#7974"), "{reason}");
}
#[test]
fn lock_fallback_refuses_a_running_pid() {
let (liveness, evidence) = liveness_with_lock_fallback(
OwnerLiveness::Undeterminable,
LockEvidence::HolderPidRunning(std::process::id()),
);
assert_eq!(liveness, OwnerLiveness::Undeterminable);
assert_eq!(evidence, None);
}
#[test]
fn lock_fallback_refuses_when_the_lock_says_nothing() {
let (liveness, evidence) =
liveness_with_lock_fallback(OwnerLiveness::Undeterminable, LockEvidence::Silent);
assert_eq!(liveness, OwnerLiveness::Undeterminable);
assert_eq!(evidence, None);
}
#[test]
fn lock_fallback_never_revives_a_live_owner() {
for lock in [
LockEvidence::HolderPidGone(17167),
LockEvidence::HolderPidRunning(std::process::id()),
LockEvidence::Silent,
] {
assert_eq!(
liveness_with_lock_fallback(OwnerLiveness::Alive, lock),
(OwnerLiveness::Alive, None),
"a live owner must survive {lock:?}"
);
}
}
#[test]
fn lock_fallback_leaves_a_dead_owner_dead() {
assert_eq!(
liveness_with_lock_fallback(OwnerLiveness::Dead, LockEvidence::HolderPidRunning(1)),
(OwnerLiveness::Dead, None)
);
}
#[test]
fn adopt_worktree_rewrites_the_sentinel() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(WORKTREE_SENTINEL_FILE), b"{}").unwrap();
let new_owner = ManagedSessionId::new();
adopt_worktree(dir.path(), new_owner).expect("adoption must write the sentinel");
match crate::session_manager::worktree_ownership::read_sentinel_owner(dir.path()) {
SentinelOwner::Known(id, _) => assert_eq!(id, new_owner),
other => panic!("the sentinel must name the adopting session; got {other:?}"),
}
}
#[test]
fn adopt_worktree_refuses_a_path_that_is_not_a_directory() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("no-such-worktree");
assert!(adopt_worktree(&missing, ManagedSessionId::new()).is_err());
assert!(!missing.join(WORKTREE_SENTINEL_FILE).exists());
}