use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use super::{
DEFAULT_INTERVAL_SECS, SweepReport, parse_enabled, parse_interval_secs, reclaim, reclaim_loop,
};
use crate::daemon::state::DaemonState;
use crate::session_manager::worktree_reclaim::{ReclaimMode, ReclaimOutcome, ReclaimSurvey};
fn scratch_root_state() -> (Arc<DaemonState>, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("scratch framework root");
let state = Arc::new(DaemonState::with_root(dir.path().to_path_buf()));
(state, dir)
}
#[test]
fn parse_enabled_defaults_on_and_honours_the_off_switch() {
assert!(parse_enabled(None), "unset must enable the sweep");
assert!(parse_enabled(Some("1")));
assert!(parse_enabled(Some("true")));
assert!(
parse_enabled(Some("")),
"an empty value is not an off switch"
);
for off in ["0", "false", "off", "no", " OFF ", "False"] {
assert!(!parse_enabled(Some(off)), "`{off}` must disable the sweep");
}
}
#[test]
fn parse_interval_falls_back_on_junk_and_zero() {
assert_eq!(parse_interval_secs(None), DEFAULT_INTERVAL_SECS);
assert_eq!(parse_interval_secs(Some("nonsense")), DEFAULT_INTERVAL_SECS);
assert_eq!(parse_interval_secs(Some("0")), DEFAULT_INTERVAL_SECS);
assert_eq!(parse_interval_secs(Some("-5")), DEFAULT_INTERVAL_SECS);
assert_eq!(parse_interval_secs(Some(" 120 ")), 120);
}
fn outcome_with(removal_failed: Vec<String>, removed: Vec<PathBuf>) -> ReclaimOutcome {
ReclaimOutcome {
survey: ReclaimSurvey::from_candidates(Vec::new()),
removed,
removed_bytes: 0,
refused_at_recheck: Vec::new(),
removal_failed,
partially_removed: Vec::new(),
}
}
#[test]
fn a_tick_with_a_failed_removal_is_reported_as_a_failure() {
let outcome = outcome_with(
vec!["/tmp/wt: git worktree remove exited 128".to_string()],
vec![PathBuf::from("/tmp/other")],
);
let report = SweepReport::from_outcome(&outcome);
assert!(report.failed(), "report: {report:?}");
assert_eq!(report.failed, 1);
assert_eq!(
report.reclaimed, 1,
"a partially successful tick still reports what it did reclaim"
);
}
#[test]
fn a_clean_tick_is_not_a_failure() {
let outcome = outcome_with(Vec::new(), vec![PathBuf::from("/tmp/gone")]);
let report = SweepReport::from_outcome(&outcome);
assert!(!report.failed(), "report: {report:?}");
}
#[test]
fn a_spared_candidate_is_not_a_failure() {
let mut outcome = outcome_with(Vec::new(), Vec::new());
outcome
.refused_at_recheck
.push("/tmp/wt: holds unsaved work".to_string());
let report = SweepReport::from_outcome(&outcome);
assert!(!report.failed(), "report: {report:?}");
assert_eq!(report.refused, 1);
}
#[tokio::test]
#[serial_test::serial]
async fn reclaim_loop_reclaims_nothing_on_a_scratch_framework_root() {
let (state, dir) = scratch_root_state();
super::run_one_tick(&state).await;
assert!(
dir.path().exists(),
"the tick must not disturb the scratch root itself"
);
}
#[tokio::test]
#[serial_test::serial]
async fn reclaim_loop_exits_on_cancel() {
let (state, _dir) = scratch_root_state();
let cancel = tokio_util::sync::CancellationToken::new();
let handle = tokio::spawn(reclaim_loop(
state,
Duration::from_secs(3600),
cancel.child_token(),
));
cancel.cancel();
tokio::time::timeout(Duration::from_secs(30), handle)
.await
.expect("the loop must exit on cancel rather than hang")
.expect("the loop must not panic");
}
#[tokio::test]
#[serial_test::serial]
async fn reclaim_on_an_empty_root_reclaims_nothing() {
let (state, _dir) = scratch_root_state();
let empty_root = tempfile::tempdir().expect("empty workspace root");
let out = reclaim(
&state,
empty_root.path(),
ReclaimMode::Remove,
None,
crate::session_manager::worktree_scope::WorktreeScope::all(),
)
.await
.expect("the probe assembly must not panic");
assert!(
out.removed.is_empty() && out.removal_failed.is_empty(),
"a workspace root with no registered worktree has nothing to reclaim: {out:?}"
);
assert_eq!(out.survey.candidates.len(), 0, "outcome: {out:?}");
}