use std::path::{Path, PathBuf};
use crate::session_manager::decommission_force::{ProvisioningDirt, remove_in_project_worktree};
use crate::session_manager::record::ManagedSessionId;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
use crate::session_manager::worktree_ownership::{AgentDelegationState, AgentWorktreeOwner};
use crate::session_manager::worktree_reclaim::{
KeepList, LiveClaims, PrIndex, ReclaimMode, ReclaimOutcome,
};
use crate::session_manager::worktree_reclaim_sweep::{FreshProbes, reclaim_scoped};
use crate::session_manager::{DirtyWorktreePolicy, SessionManager};
#[derive(Debug, Clone, Copy)]
enum Seed {
RunOutput,
Target,
NestedNodeModules,
Harness,
}
const REGENERABLE: [Seed; 3] = [Seed::Target, Seed::NestedNodeModules, Seed::Harness];
fn ignore_seeds(fx: &GitWorktreeFixture) {
std::fs::write(
fx.repo.join(".git/info/exclude"),
"results/\ntarget/\nnode_modules/\n/.claude/settings.json\n/.trusty-mpm-worktree\n",
)
.expect("write info/exclude");
}
fn seed(wt: &Path, seed: Seed) {
let (dir, file) = match seed {
Seed::RunOutput => ("results", "results/out.json"),
Seed::Target => ("target/debug", "target/debug/app"),
Seed::NestedNodeModules => ("web/node_modules/pkg", "web/node_modules/pkg/index.js"),
Seed::Harness => (".claude", ".claude/settings.json"),
};
std::fs::create_dir_all(wt.join(dir)).expect("mkdir seed dir");
std::fs::write(wt.join(file), "{}").expect("write seed file");
if matches!(seed, Seed::Harness) {
GitWorktreeFixture::stamp_reclaimable_sentinel(wt);
}
}
fn seeded_tree(fx: &GitWorktreeFixture, name: &str, s: Seed) -> PathBuf {
let wt = fx.add_worktree(name);
GitWorktreeFixture::stamp_reclaimable_sentinel(&wt);
seed(&wt, s);
wt
}
async fn captured<F: std::future::Future>(body: F) -> (F::Output, Vec<String>) {
use tracing_subscriber::layer::SubscriberExt;
crate::test_support::enable_event_capture();
let buffer = trusty_common::log_buffer::LogBuffer::new(256);
let subscriber = tracing_subscriber::registry().with(
trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
);
let guard = tracing::subscriber::set_default(subscriber);
let out = body.await;
drop(guard);
(out, buffer.tail(256))
}
#[tokio::test]
async fn decommission_keeps_gitignored_run_output() {
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let wt = seeded_tree(&fx, "decom-8534", Seed::RunOutput);
let verdict = remove_in_project_worktree(
&ManagedSessionId::new(),
None,
&wt,
ProvisioningDirt::Refuse,
)
.await;
assert!(!verdict.removed, "{verdict:?}");
assert!(
wt.join("results/out.json").exists(),
"the run output survives"
);
let reason = verdict.kept_reason.expect("a kept tree names why");
assert!(
reason.contains("#8534") && reason.contains("results/"),
"{reason}"
);
}
#[tokio::test]
async fn force_decommission_keeps_gitignored_run_output() {
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let wt = seeded_tree(&fx, "decom-force-8534", Seed::RunOutput);
let verdict = remove_in_project_worktree(
&ManagedSessionId::new(),
None,
&wt,
ProvisioningDirt::Discard,
)
.await;
assert!(!verdict.removed && wt.join("results/out.json").exists());
}
#[tokio::test]
async fn decommission_removes_build_and_harness_output() {
for (i, s) in REGENERABLE.into_iter().enumerate() {
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let wt = seeded_tree(&fx, &format!("decom-regen-8534-{i}"), s);
let verdict = remove_in_project_worktree(
&ManagedSessionId::new(),
None,
&wt,
ProvisioningDirt::Refuse,
)
.await;
assert!(verdict.removed, "{s:?}: {:?}", verdict.kept_reason);
assert!(!wt.exists(), "{s:?}: the tree is gone");
}
}
fn no_agents(_: &AgentWorktreeOwner) -> AgentDelegationState {
AgentDelegationState::Unknown
}
fn reclaim(fx: &GitWorktreeFixture, name: &str, s: Seed) -> (PathBuf, ReclaimOutcome) {
let wt = fx.add_worktree(name);
std::fs::write(wt.join("landed.rs"), "// landed\n").expect("write landed file");
GitWorktreeFixture::commit_all_and_push(&wt, "landed");
seed(&wt, s);
let branch = format!("session/{name}");
let index = move |_: &Path| {
PrIndex::from_json(
&format!(r#"[{{"number": 37, "headRefName": "{branch}", "state": "MERGED"}}]"#),
400,
)
};
let out = reclaim_scoped(
&fx.repos_root,
&FreshProbes {
prove: &crate::session_manager::worktree_reclaim_landed::reclaim_landed_proof,
launched_from: &[],
cwd_holder: &|_| None,
keep_list: &KeepList::default,
agent_state: &no_agents,
in_use_now: &|| {
Some(LiveClaims {
owners: GitWorktreeFixture::reclaimable_owner_gone(),
..LiveClaims::default()
})
},
index_for: &index,
},
ReclaimMode::Remove,
&[],
&crate::session_manager::worktree_scope::WorktreeScope::all(),
);
(wt, out)
}
#[test]
fn reclaim_keeps_gitignored_run_output() {
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let (wt, out) = reclaim(&fx, "reclaim-8534", Seed::RunOutput);
assert!(out.removed.is_empty(), "{out:?}");
assert!(
wt.join("results/out.json").exists(),
"the run output survives"
);
assert!(
out.removal_failed
.iter()
.any(|r| r.contains("#8534") && r.contains("results/")),
"{out:?}"
);
}
#[test]
fn reclaim_removes_build_and_harness_output() {
for (i, s) in REGENERABLE.into_iter().enumerate() {
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let (wt, out) = reclaim(&fx, &format!("reclaim-regen-8534-{i}"), s);
assert_eq!(out.removed, vec![wt.clone()], "{s:?}: {out:?}");
assert!(!wt.exists(), "{s:?}: the tree is gone");
}
}
async fn manager() -> (SessionManager, tempfile::TempDir) {
let store = tempfile::tempdir().expect("tempdir");
let mgr = SessionManager::new(
store.path(),
crate::session_manager::tests::FakeTmuxDriver::new(),
)
.await
.expect("manager");
(mgr, store)
}
#[tokio::test]
#[serial_test::serial]
async fn prune_keeps_gitignored_run_output() {
let (mgr, _store) = manager().await;
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let wt = seeded_tree(&fx, "prune-8534", Seed::RunOutput);
let (outcome, lines) = captured(mgr.prune_orphaned_worktrees(
&fx.repos_root,
&[],
false,
DirtyWorktreePolicy::Skip,
&[],
))
.await;
let outcome = outcome.expect("prune must not error");
assert!(!outcome.removed.contains(&wt), "{:?}", outcome.removed);
assert!(
wt.join("results/out.json").exists(),
"the run output survives"
);
assert!(
lines
.iter()
.any(|l| l.contains("worktree kept") && l.contains("#8534")),
"{lines:#?}"
);
}
#[tokio::test]
async fn force_discard_prune_removes_gitignored_output() {
let (mgr, _store) = manager().await;
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let wt = seeded_tree(&fx, "prune-discard-8534", Seed::RunOutput);
let outcome = mgr
.prune_orphaned_worktrees(
&fx.repos_root,
&[],
false,
DirtyWorktreePolicy::ForceDiscard,
&[],
)
.await
.expect("prune must not error");
assert!(outcome.removed.contains(&wt), "{:?}", outcome.removed);
assert!(!wt.exists());
}
#[tokio::test]
async fn prune_removes_build_and_harness_output() {
for (i, s) in REGENERABLE.into_iter().enumerate() {
let (mgr, _store) = manager().await;
let fx = GitWorktreeFixture::new();
ignore_seeds(&fx);
let wt = seeded_tree(&fx, &format!("prune-regen-8534-{i}"), s);
let outcome = mgr
.prune_orphaned_worktrees(&fx.repos_root, &[], false, DirtyWorktreePolicy::Skip, &[])
.await
.expect("prune must not error");
assert!(
outcome.removed.contains(&wt),
"{s:?}: {:?}",
outcome.removed
);
assert!(!wt.exists(), "{s:?}: the tree is gone");
}
}