use super::*;
fn tree() -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = std::fs::canonicalize(tmp.path()).expect("canonicalize");
(tmp, root)
}
#[test]
fn claim_state_matches_exact_ancestor_and_descendant_paths() {
let (_tmp, root) = tree();
let candidate = root.join("wt");
let inside = candidate.join("nested");
std::fs::create_dir_all(&inside).expect("mkdir");
let exact = LiveClaims::foreign(vec![WorkspaceClaim::new("s1", &candidate)]);
assert!(
exact.claim_state(&candidate).refusal(false).is_some(),
"exact match must block"
);
let claim_inside = LiveClaims::foreign(vec![WorkspaceClaim::new("s1", &inside)]);
assert!(
claim_inside
.claim_state(&candidate)
.refusal(false)
.is_some(),
"a session sitting INSIDE the candidate must protect it"
);
let claim_outer = LiveClaims::foreign(vec![WorkspaceClaim::new("s1", &candidate)]);
assert!(
claim_outer.claim_state(&inside).refusal(false).is_none(),
"a project-level claim must not veto a worktree nested under it (#7652)"
);
let unrelated = LiveClaims::foreign(vec![WorkspaceClaim::new("s1", root.join("unrelated"))]);
assert_eq!(
unrelated.claim_state(&candidate),
ClaimState::Unclaimed,
"an unrelated sibling must not protect it"
);
assert_eq!(
LiveClaims::default().claim_state(&candidate),
ClaimState::Unclaimed,
"nothing claimed means not live"
);
}
#[test]
fn claims_from_the_caller_alone_do_not_block() {
let (_tmp, root) = tree();
let workspace = root.join("client");
let worktree = workspace.join(".worktrees").join("rb-1");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims {
claims: vec![WorkspaceClaim::new("tm-client-03", &workspace)],
caller: Some("tm-client-03".to_string()),
owners: Default::default(),
};
assert_eq!(
claims.claim_state(&worktree),
ClaimState::CallerNested {
session: "tm-client-03".to_string()
}
);
assert!(
claims.claim_state(&worktree).refusal(false).is_none(),
"the caller's own claim must not block its own nested worktree"
);
}
#[test]
fn a_caller_may_not_reclaim_its_own_workspace() {
let (_tmp, root) = tree();
let workspace = root.join("client");
std::fs::create_dir_all(&workspace).expect("mkdir");
let claims = LiveClaims {
claims: vec![WorkspaceClaim::new("tm-client-03", &workspace)],
caller: Some("tm-client-03".to_string()),
owners: Default::default(),
};
let reason = claims
.claim_state(&workspace)
.refusal(false)
.expect("a caller's own workspace must still refuse");
assert!(reason.contains("tm-client-03"), "{reason}");
assert!(reason.contains("IS the caller"), "{reason}");
let reason = claims
.claim_state(&root)
.refusal(false)
.expect("a candidate containing the caller's workspace must refuse");
assert!(reason.contains("IS the caller"), "{reason}");
}
#[test]
fn a_foreign_sessions_claim_still_blocks() {
let (_tmp, root) = tree();
let worktree = root.join("client").join(".worktrees").join("rb-1");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims {
claims: vec![WorkspaceClaim::new("tm-other-01", &worktree)],
caller: Some("tm-client-03".to_string()),
owners: Default::default(),
};
assert!(
claims.claim_state(&worktree).refusal(false).is_some(),
"a live session that is NOT the caller must still block"
);
}
#[test]
fn a_foreign_refusal_names_the_claimant_and_denies_it_is_the_caller() {
let (_tmp, root) = tree();
let worktree = root.join("wt");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims {
claims: vec![WorkspaceClaim::new("tm-other-01", &worktree)],
caller: Some("tm-client-03".to_string()),
owners: Default::default(),
};
let reason = claims
.claim_state(&worktree)
.refusal(false)
.expect("a foreign claim must refuse");
assert!(
reason.contains("tm-other-01"),
"names the claimant: {reason}"
);
assert!(
reason.contains("tm-client-03"),
"names the caller: {reason}"
);
assert!(
reason.contains("not the calling session"),
"says it is not the caller: {reason}"
);
}
#[test]
fn a_foreign_refusal_says_when_the_caller_named_no_session() {
let (_tmp, root) = tree();
let worktree = root.join("wt");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims::foreign(vec![WorkspaceClaim::new("tm-other-01", &worktree)]);
let reason = claims
.claim_state(&worktree)
.refusal(false)
.expect("a foreign claim must refuse");
assert!(reason.contains("tm-other-01"), "{reason}");
assert!(reason.contains("named no session of its own"), "{reason}");
}
#[test]
fn a_foreign_claim_outranks_the_callers_own() {
let (_tmp, root) = tree();
let workspace = root.join("client");
let worktree = workspace.join(".worktrees").join("rb-1");
std::fs::create_dir_all(&worktree).expect("mkdir");
for order in [0usize, 1] {
let mut claims = vec![
WorkspaceClaim::new("tm-client-03", &workspace),
WorkspaceClaim::new("tm-other-01", &worktree),
];
if order == 1 {
claims.reverse();
}
let live = LiveClaims {
claims,
caller: Some("tm-client-03".to_string()),
owners: Default::default(),
};
let state = live.claim_state(&worktree);
assert!(
matches!(state, ClaimState::Foreign { .. }),
"order {order}: foreign must win, got {state:?}"
);
}
}
#[test]
fn a_dead_sessions_claim_no_longer_blocks() {
let (_tmp, root) = tree();
let worktree = root.join("client").join(".worktrees").join("agent-1");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims::foreign(vec![WorkspaceClaim::with_liveness(
"51786c9c-478f-5b3f-83a7-cbe63e3d5958",
&root,
ClaimLiveness::SessionGone,
)]);
let state = claims.claim_state(&worktree);
assert!(
!matches!(state, ClaimState::Foreign { .. }),
"a dead session's claim must not be foreign: {state:?}"
);
assert_eq!(state.refusal(false), None, "and must not refuse: {state:?}");
let note = state
.note()
.expect("the decision must name what it discarded");
assert!(
note.contains("51786c9c-478f-5b3f-83a7-cbe63e3d5958"),
"{note}"
);
assert!(note.contains("#7232"), "{note}");
}
#[test]
fn a_live_foreign_sessions_claim_still_blocks() {
let (_tmp, root) = tree();
let worktree = root.join("client").join(".worktrees").join("agent-1");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims::foreign(vec![WorkspaceClaim::with_liveness(
"tm-other-01",
&worktree,
ClaimLiveness::Live,
)]);
let state = claims.claim_state(&worktree);
assert!(
matches!(state, ClaimState::Foreign { .. }),
"a live foreign claim must still refuse: {state:?}"
);
assert!(state.refusal(false).is_some());
assert_eq!(state.note(), None, "nothing was discarded");
}
#[test]
fn a_live_claim_beside_a_dead_one_still_blocks() {
let (_tmp, root) = tree();
let worktree = root.join("client").join(".worktrees").join("agent-1");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims::foreign(vec![
WorkspaceClaim::with_liveness("tm-dead-01", &root, ClaimLiveness::SessionGone),
WorkspaceClaim::with_liveness("tm-alive-02", &worktree, ClaimLiveness::Live),
]);
let reason = claims
.claim_state(&worktree)
.refusal(false)
.expect("the live claim must still refuse");
assert!(reason.contains("tm-alive-02"), "{reason}");
}
#[test]
fn the_recheck_wording_is_present_tense() {
let (_tmp, root) = tree();
let worktree = root.join("wt");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims::foreign(vec![WorkspaceClaim::new("tm-other-01", &worktree)]);
let state = claims.claim_state(&worktree);
assert!(
state
.refusal(true)
.expect("refuses")
.contains("claims this workspace now"),
"{:?}",
state.refusal(true)
);
assert!(
state
.refusal(false)
.expect("refuses")
.contains("still claims this workspace"),
"{:?}",
state.refusal(false)
);
}
#[test]
fn worktree_7652_a_foreign_project_root_claim_no_longer_blocks_a_nested_worktree() {
let (_tmp, root) = tree();
let project = root.join("bobmatnyc").join("trusty-tools");
let worktree = project.join(".claude").join("worktrees").join("agent-ae59");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims {
claims: vec![WorkspaceClaim::new(
"0b318c84-bae9-4a50-8832-65ed61f8ab22",
&project,
)],
caller: Some("b175bb88-af7f-5ba5-b75d-85795d60b234".to_string()),
owners: Default::default(),
};
let state = claims.claim_state(&worktree);
assert_eq!(
state,
ClaimState::ForeignNested {
session: "0b318c84-bae9-4a50-8832-65ed61f8ab22".to_string(),
caller: Some("b175bb88-af7f-5ba5-b75d-85795d60b234".to_string()),
},
"a project-level foreign claim is not an attribution of this worktree"
);
assert_eq!(
state.refusal(false),
None,
"gate 2 must let the later gates decide it"
);
assert_eq!(
state.refusal(true),
None,
"and the pre-delete re-check must agree, or the two gates disagree"
);
let note = state
.note()
.expect("a claim that stopped vetoing must say so");
assert!(
note.contains("0b318c84-bae9-4a50-8832-65ed61f8ab22"),
"{note}"
);
assert!(note.contains("#7652"), "{note}");
let on_the_worktree = LiveClaims {
claims: vec![WorkspaceClaim::new(
"0b318c84-bae9-4a50-8832-65ed61f8ab22",
&worktree,
)],
caller: Some("b175bb88-af7f-5ba5-b75d-85795d60b234".to_string()),
owners: Default::default(),
};
let reason = on_the_worktree
.claim_state(&worktree)
.refusal(false)
.expect("a foreign claim ON this worktree must still refuse");
assert!(
reason.contains("0b318c84-bae9-4a50-8832-65ed61f8ab22"),
"{reason}"
);
}
#[test]
fn worktree_7652_a_foreign_nested_claim_outranks_the_callers_own() {
let (_tmp, root) = tree();
let project = root.join("bobmatnyc").join("trusty-tools");
let worktree = project.join(".claude").join("worktrees").join("agent-ae59");
std::fs::create_dir_all(&worktree).expect("mkdir");
let claims = LiveClaims {
claims: vec![
WorkspaceClaim::new("tm-caller-7652", &project),
WorkspaceClaim::new("tm-foreign-7652", &project),
],
caller: Some("tm-caller-7652".to_string()),
owners: Default::default(),
};
assert_eq!(
claims.claim_state(&worktree),
ClaimState::ForeignNested {
session: "tm-foreign-7652".to_string(),
caller: Some("tm-caller-7652".to_string()),
},
"the caller's own ancestor claim must not hide a live foreign one"
);
let reversed = LiveClaims {
claims: vec![
WorkspaceClaim::new("tm-foreign-7652", &project),
WorkspaceClaim::new("tm-caller-7652", &project),
],
caller: Some("tm-caller-7652".to_string()),
owners: Default::default(),
};
assert_eq!(
reversed.claim_state(&worktree),
ClaimState::ForeignNested {
session: "tm-foreign-7652".to_string(),
caller: Some("tm-caller-7652".to_string()),
},
"precedence, not iteration order, must decide this"
);
let caller_only = LiveClaims {
claims: vec![WorkspaceClaim::new("tm-caller-7652", &project)],
caller: Some("tm-caller-7652".to_string()),
owners: Default::default(),
};
assert_eq!(
caller_only.claim_state(&worktree),
ClaimState::CallerNested {
session: "tm-caller-7652".to_string()
}
);
let over_the_callers_workspace = LiveClaims {
claims: vec![
WorkspaceClaim::new("tm-foreign-7652", &project),
WorkspaceClaim::new("tm-caller-7652", &worktree),
],
caller: Some("tm-caller-7652".to_string()),
owners: Default::default(),
};
let containing = worktree.parent().expect("the worktrees directory");
let state = over_the_callers_workspace.claim_state(containing);
assert_eq!(
state,
ClaimState::CallerWorkspace {
session: "tm-caller-7652".to_string()
},
"a candidate containing the caller's own workspace must not be downgraded \
to a permitting state by a foreign ancestor claim"
);
assert!(
state.refusal(false).is_some(),
"and that state must still refuse"
);
}