use super::*;
use crate::test_env::CargoTargetDirGuard;
use crate::store::Store;
use rusqlite::params;
use std::fs;
use std::path::Path;
fn insert_task(
store: &Store,
id: &str,
status: &str,
repo_path: Option<&Path>,
worktree_path: Option<&Path>,
branch: Option<&str>,
) {
store
.db()
.execute(
"INSERT INTO tasks (id, agent, prompt, status, repo_path, worktree_path, worktree_branch, created_at)
VALUES (?1, 'codex', 'cleanup test', ?2, ?3, ?4, ?5, '2026-01-01T00:00:00Z')",
params![
id,
status,
repo_path.map(|path| path.to_string_lossy().into_owned()),
worktree_path.map(|path| path.to_string_lossy().into_owned()),
branch,
],
)
.unwrap();
}
fn init_repo(repo: &Path) {
std::process::Command::new("git")
.args(["init", "-b", "main", &repo.to_string_lossy()])
.status()
.unwrap();
std::process::Command::new("git")
.args(["-C", &repo.to_string_lossy(), "config", "user.email", "test@example.com"])
.status()
.unwrap();
std::process::Command::new("git")
.args(["-C", &repo.to_string_lossy(), "config", "user.name", "Test User"])
.status()
.unwrap();
fs::write(repo.join("file.txt"), "base\n").unwrap();
std::process::Command::new("git")
.args(["-C", &repo.to_string_lossy(), "add", "file.txt"])
.status()
.unwrap();
std::process::Command::new("git")
.args(["-C", &repo.to_string_lossy(), "commit", "-m", "base"])
.status()
.unwrap();
}
#[test]
fn cleanup_removes_only_terminal_task_owned_targets() {
let _permit = crate::test_subprocess::acquire();
let aid_home = tempfile::tempdir().unwrap();
let _aid_guard = crate::paths::AidHomeGuard::set(aid_home.path());
let target_root = aid_home.path().join("cargo-target");
let _target_guard = CargoTargetDirGuard::set(&target_root);
let fallback_root = tempfile::tempdir().unwrap();
let store = Store::open_memory().unwrap();
let repo = tempfile::tempdir().unwrap();
init_repo(repo.path());
let live_worktree = tempfile::tempdir().unwrap();
let live_branch = "feat/live-clean-target";
let live_path = live_worktree.path().join("live");
std::process::Command::new("git")
.args([
"-C",
&repo.path().to_string_lossy(),
"worktree",
"add",
&live_path.to_string_lossy(),
"-b",
live_branch,
])
.status()
.unwrap();
let stale_branch = "feat/stale-clean-target";
let stale_worktree = repo.path().join("missing-worktree");
insert_task(
&store,
"t-stale",
"done",
Some(repo.path()),
Some(&stale_worktree),
Some(stale_branch),
);
insert_task(
&store,
"t-live",
"done",
Some(repo.path()),
Some(&live_path),
Some(live_branch),
);
insert_task(&store, "t-blank-branch", "done", None, None, Some(""));
let stale_target = target_root.join(crate::agent::env::branch_target_name(stale_branch));
let live_target = target_root.join(crate::agent::env::branch_target_name(live_branch));
let stale_fallback = fallback_root
.path()
.join(crate::cmd::build::build_fallback::cwd_key(&stale_worktree));
let unattributed = target_root.join("not-in-task-record");
for path in [&stale_target, &live_target, &stale_fallback, &unattributed] {
fs::create_dir_all(path).unwrap();
fs::write(path.join("artifact"), vec![b'x'; 8]).unwrap();
}
fs::write(target_root.join("root-artifact"), b"root").unwrap();
let mut sizes = crate::cmd::clean_size::SizeTracker::new();
clean_orphaned_branch_targets(&store, false, Some(fallback_root.path()), &mut sizes).unwrap();
assert!(!stale_target.exists());
assert!(!stale_fallback.exists());
assert!(live_target.exists());
assert!(unattributed.exists());
assert!(target_root.exists());
}
#[test]
fn cleanup_preserves_root_when_branch_name_matches_root_basename() {
let _permit = crate::test_subprocess::acquire();
let aid_home = tempfile::tempdir().unwrap();
let _aid_guard = crate::paths::AidHomeGuard::set(aid_home.path());
let target_root = aid_home.path().join("cargo-target");
let _target_guard = CargoTargetDirGuard::set(&target_root);
let fallback_root = tempfile::tempdir().unwrap();
let store = Store::open_memory().unwrap();
let live_worktree = tempfile::tempdir().unwrap();
insert_task(
&store,
"t-root-name",
"done",
None,
None,
Some("cargo-target"),
);
insert_task(
&store,
"t-live-leaf",
"done",
None,
Some(live_worktree.path()),
Some("live-leaf"),
);
let live_target = target_root.join("live-leaf");
fs::create_dir_all(&live_target).unwrap();
fs::write(target_root.join("root-artifact"), b"root").unwrap();
let mut sizes = crate::cmd::clean_size::SizeTracker::new();
clean_orphaned_branch_targets(&store, false, Some(fallback_root.path()), &mut sizes).unwrap();
assert!(target_root.exists());
assert!(live_target.exists());
}
#[test]
fn hint_scan_reports_lower_bound_when_entry_limit_truncates_scan() {
let temp = tempfile::tempdir().unwrap();
fs::write(temp.path().join("first"), vec![b'a'; 7]).unwrap();
fs::write(temp.path().join("second"), vec![b'b'; 11]).unwrap();
fs::write(temp.path().join("third"), vec![b'c'; 13]).unwrap();
let expected = crate::cmd::clean_size::get_dir_size(temp.path()).unwrap();
let mut sizes = crate::cmd::clean_size::SizeTracker::new();
let measured = scan_hint_path(&mut sizes, temp.path(), 10_000, 2).unwrap();
assert!(measured.truncated);
assert_eq!(measured.entries, 2);
assert!(measured.bytes < expected);
}
#[test]
fn hint_scan_marks_complete_scan_when_limits_are_not_reached() {
let temp = tempfile::tempdir().unwrap();
fs::write(temp.path().join("first"), vec![b'a'; 7]).unwrap();
fs::write(temp.path().join("second"), vec![b'b'; 11]).unwrap();
let expected = crate::cmd::clean_size::get_dir_size(temp.path()).unwrap();
let mut sizes = crate::cmd::clean_size::SizeTracker::new();
let measured = scan_hint_path(&mut sizes, temp.path(), 10_000, 3).unwrap();
assert!(!measured.truncated);
assert_eq!(measured.bytes, expected);
}
#[test]
fn hint_estimate_survives_entry_limit_before_byte_threshold() {
let _permit = crate::test_subprocess::acquire();
let aid_home = tempfile::tempdir().unwrap();
let _aid_guard = crate::paths::AidHomeGuard::set(aid_home.path());
let target_root = aid_home.path().join("cargo-target");
let _target_guard = CargoTargetDirGuard::set(&target_root);
let store = Store::open_memory().unwrap();
let branch = "feat/truncated-hint";
insert_task(&store, "t-truncated-hint", "done", None, None, Some(branch));
let target = target_root.join(crate::agent::env::branch_target_name(branch));
fs::create_dir_all(&target).unwrap();
for index in 0..=crate::cmd::clean_size::CLEANUP_HINT_ENTRY_LIMIT {
fs::File::create(target.join(format!("artifact-{index}"))).unwrap();
}
let estimate = has_reclaimable_space_above_threshold(&store)
.unwrap()
.expect("entry-budget truncation must remain visible");
assert!(estimate.truncated);
assert!(estimate.bytes < crate::cmd::clean_size::CLEANUP_HINT_THRESHOLD_BYTES);
}