use super::*;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
#[test]
fn worktree_7885_an_audit_names_the_path_branch_session_and_reason() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("agent-a9826013b");
GitWorktreeFixture::stamp_reclaimable_sentinel(&wt);
let audit = RemovalAudit::resolve(&wt, "merged-PR reclaim, every gate passed");
let line = audit.line();
assert!(
line.contains(
&wt.canonicalize()
.expect("canonicalize")
.display()
.to_string()
),
"names the resolved path: {line}"
);
assert!(
line.contains("session/agent-a9826013b"),
"names the branch: {line}"
);
assert!(
line.contains("merged-PR reclaim"),
"names the reason: {line}"
);
assert!(
!audit.session.is_empty() && audit.session != "(unowned)",
"names the sentinel's owner: {:?}",
audit.session
);
}
fn captured<T>(body: impl FnOnce() -> T) -> (T, Vec<String>) {
use tracing_subscriber::layer::SubscriberExt;
crate::test_support::enable_event_capture();
let buffer = trusty_common::log_buffer::LogBuffer::new(64);
let subscriber = tracing_subscriber::registry().with(
trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
);
let out = tracing::subscriber::with_default(subscriber, body);
(out, buffer.tail(64))
}
#[test]
#[serial_test::serial]
fn worktree_7885_the_agent_reaper_is_audited() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree_at(&fx.repo.join(".claude").join("worktrees"), "agent-7885reap");
std::fs::write(wt.join("landed.rs"), "// landed\n").expect("write landed file");
GitWorktreeFixture::commit_all_and_push(&wt, "landed");
GitWorktreeFixture::stamp_agent_sentinel(&wt, "agent-7885reap");
let (outcome, lines) = captured(|| {
crate::daemon::services::agent_worktree_reap::reap_worktree(&wt, "agent-7885reap", &[])
});
assert!(
matches!(
outcome,
crate::daemon::services::agent_worktree_reap::ReapOutcome::Removed
),
"the fixture must be reapable. Captured: {lines:#?}"
);
assert!(
lines
.iter()
.any(|l| l.contains("worktree-removal: attempting removal of")
&& l.contains("agent-7885reap")
&& l.contains("agent-worktree reap")),
"Captured: {lines:#?}"
);
assert!(
lines
.iter()
.any(|l| l.contains("worktree-removal: removed") && l.contains("agent-7885reap")),
"Captured: {lines:#?}"
);
}
#[test]
#[serial_test::serial]
fn worktree_7885_a_removal_emits_one_audit_line_before_deleting() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("agent-a1afc1489");
GitWorktreeFixture::stamp_reclaimable_sentinel(&wt);
let (outcome, lines) = captured(|| {
crate::session_manager::decommission::remove_session_worktree(
&wt,
"merged-PR reclaim, every gate passed",
crate::session_manager::DirtyWorktreePolicy::Skip,
)
});
assert!(outcome.removed(), "the fixture worktree must be removable");
assert!(!wt.exists(), "and actually gone");
let audit = lines
.iter()
.find(|l| l.contains("worktree-removal:"))
.unwrap_or_else(|| panic!("no audit line for a real removal. Captured: {lines:#?}"));
assert!(audit.contains("agent-a1afc1489"), "names the path: {audit}");
assert!(
audit.contains("session/agent-a1afc1489"),
"names the branch: {audit}"
);
assert!(
audit.contains("merged-PR reclaim"),
"names the reason: {audit}"
);
assert!(audit.contains("INFO"), "at info level: {audit}");
let locked = fx.add_worktree("agent-locked");
GitWorktreeFixture::stamp_reclaimable_sentinel(&locked);
fx.lock_worktree(&locked);
let (outcome, lines) = captured(|| {
crate::session_manager::decommission::remove_session_worktree(
&locked,
"orphan sweep, no live session claims it",
crate::session_manager::DirtyWorktreePolicy::Skip,
)
});
assert!(!outcome.removed(), "a git-locked worktree must be refused");
assert!(locked.exists(), "and left on disk");
assert!(
lines.iter().any(|l| l.contains("worktree-removal:")
&& l.contains("agent-locked")
&& l.contains("orphan sweep")),
"a refused removal must still be audited. Captured: {lines:#?}"
);
}
fn reads_as_a_deletion(line: &str) -> bool {
line.contains("worktree-removal: removing") || line.contains("worktree-removal: removed")
}
#[test]
#[serial_test::serial]
fn worktree_7885_a_refused_removal_is_never_audited_as_a_deletion() {
let fx = GitWorktreeFixture::new();
let locked = fx.add_worktree("agent-refused-7885");
GitWorktreeFixture::stamp_reclaimable_sentinel(&locked);
fx.lock_worktree(&locked);
let (outcome, lines) = captured(|| {
crate::session_manager::decommission::remove_session_worktree(
&locked,
"merged-PR reclaim, every gate passed",
crate::session_manager::DirtyWorktreePolicy::Skip,
)
});
assert!(!outcome.removed(), "a git-locked worktree must be refused");
assert!(locked.exists(), "and left on disk");
let audit: Vec<&String> = lines
.iter()
.filter(|l| l.contains("worktree-removal:"))
.collect();
assert!(!audit.is_empty(), "still audited. Captured: {lines:#?}");
assert!(
!audit.iter().any(|l| reads_as_a_deletion(l)),
"nothing was deleted, so no line may read as a deletion: {audit:#?}"
);
assert!(
audit
.iter()
.any(|l| l.contains("worktree-removal: kept") && l.contains("agent-refused-7885")),
"the attempt needs a kept outcome naming the path: {audit:#?}"
);
}
#[test]
#[serial_test::serial]
fn worktree_7885_a_completed_removal_is_audited_as_removed() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("agent-removed-7885");
GitWorktreeFixture::stamp_reclaimable_sentinel(&wt);
let (outcome, lines) = captured(|| {
crate::session_manager::decommission::remove_session_worktree(
&wt,
"merged-PR reclaim, every gate passed",
crate::session_manager::DirtyWorktreePolicy::Skip,
)
});
assert!(
outcome.removed() && !wt.exists(),
"the fixture must be removed"
);
assert!(
lines
.iter()
.any(|l| l.contains("worktree-removal: removed") && l.contains("agent-removed-7885")),
"Captured: {lines:#?}"
);
assert!(
!lines.iter().any(|l| l.contains("worktree-removal: kept")),
"Captured: {lines:#?}"
);
}
#[test]
fn an_audit_for_an_unreadable_path_still_names_the_path_and_reason() {
let tmp = tempfile::tempdir().expect("tempdir");
let missing = tmp.path().join("gone");
let audit = RemovalAudit::resolve(&missing, "leftover directory, no repository claims it");
assert_eq!(audit.branch, "(none)", "no branch is knowable here");
assert_eq!(audit.session, "(unowned)", "no sentinel is knowable here");
let line = audit.line();
assert!(line.contains("gone"), "still names the path: {line}");
assert!(
line.contains("leftover directory"),
"still names the reason: {line}"
);
}