use std::fs;
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
use std::path::Path;
#[cfg(windows)]
use std::path::PathBuf;
mod common;
#[cfg(windows)]
fn steam_dry_run_json_output(
temp: &tempfile::TempDir,
case: &common::steam::SteamRuleCase,
) -> serde_json::Value {
let steam = temp.path().join("Steam");
case.write_fixture(&steam);
let mut command = common::isolated::isolated_rebecca(temp);
command.env("REBECCA_STEAM_DISCOVERY_PATH", &steam).args([
"clean",
"--dry-run",
"--format",
"json",
"--allow-warning",
"source-boundary",
"--rule",
case.rule_id,
]);
if case.allow_moderate {
command.args(["--allow-moderate"]);
}
let output = command.output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
common::support::api_data(&output.stdout)
}
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
fn write_fixture_file(path: impl AsRef<Path>, bytes: &[u8]) {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, bytes).unwrap();
}
#[cfg(windows)]
fn write_slack_cache_fixture(temp: &tempfile::TempDir) {
let slack = temp.path().join("roaming").join("Slack");
write_fixture_file(slack.join("Cache").join("cache.bin"), b"ab");
write_fixture_file(slack.join("Code Cache").join("code.bin"), b"cde");
write_fixture_file(slack.join("GPUCache").join("gpu.bin"), b"fghi");
write_fixture_file(
slack.join("Local Storage").join("leveldb").join("LOG"),
b"keep",
);
write_fixture_file(
slack
.join("IndexedDB")
.join("indexeddb.leveldb")
.join("LOG"),
b"keep",
);
write_fixture_file(
slack
.join("Service Worker")
.join("CacheStorage")
.join("index.bin"),
b"keep",
);
}
#[cfg(windows)]
fn write_wechat_cache_fixture(temp: &tempfile::TempDir) {
let wechat = temp.path().join("roaming").join("Tencent").join("WeChat");
write_fixture_file(wechat.join("radium").join("cache").join("cache.bin"), b"ab");
write_fixture_file(wechat.join("WmpfCache").join("wmpf.bin"), b"cde");
write_fixture_file(
wechat
.join("radium")
.join("web")
.join("profiles")
.join("multitab_abc")
.join("Cache")
.join("Cache_Data")
.join("multitab.bin"),
b"fghi",
);
write_fixture_file(
wechat
.join("radium")
.join("web")
.join("profiles")
.join("web_shell")
.join("Cache")
.join("Cache_Data")
.join("webshell.bin"),
b"jklmn",
);
write_fixture_file(wechat.join("bak").join("backup.bin"), b"keep");
}
#[test]
fn clean_dry_run_json_builds_plan_without_deleting() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
fs::create_dir_all(&temp_cache).unwrap();
let file = temp_cache.join("cache.tmp");
fs::write(&file, b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("REBECCA_STEAM_DISCOVERY", "none")
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.env("LOCALAPPDATA", temp.path().join("local"))
.env("APPDATA", temp.path().join("roaming"))
.args([
"clean",
"--dry-run",
"--format",
"json",
"--category",
"system",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
assert!(file.exists(), "dry-run must not delete files");
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["request"]["mode"], "dry-run");
assert!(value["summary"]["allowed_targets"].as_u64().unwrap() >= 1);
}
#[test]
fn clean_dry_run_accepts_windows_native_scan_backend() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
fs::create_dir_all(&temp_cache).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("REBECCA_STEAM_DISCOVERY", "none")
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--category",
"system",
"--scan-backend",
"windows-native",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["request"]["mode"], "dry-run");
assert!(value["summary"]["allowed_targets"].as_u64().unwrap() >= 1);
}
#[test]
fn clean_dry_run_accepts_windows_ntfs_mft_experimental_scan_backend() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
fs::create_dir_all(&temp_cache).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("REBECCA_STEAM_DISCOVERY", "none")
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--category",
"system",
"--scan-backend",
"windows-ntfs-mft-experimental",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["request"]["mode"], "dry-run");
assert!(value["summary"]["allowed_targets"].as_u64().unwrap() >= 1);
}
#[test]
fn clean_defaults_to_preview_without_deleting() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
fs::create_dir_all(&temp_cache).unwrap();
let file = temp_cache.join("cache.tmp");
fs::write(&file, b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("REBECCA_STEAM_DISCOVERY", "none")
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args(["clean", "--format", "json", "--category", "system"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
assert!(file.exists(), "clean should preview by default");
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["request"]["mode"], "dry-run");
assert!(value["summary"]["allowed_targets"].as_u64().unwrap() >= 1);
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_reports_slack_cache_rule() {
let temp = tempfile::tempdir().unwrap();
write_slack_cache_fixture(&temp);
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"windows.slack-cache",
"--allow-warning",
"active-process",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["allowed_targets"], 3);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 9);
let targets = value["targets"].as_array().unwrap();
let target_paths = targets
.iter()
.map(|target| PathBuf::from(target["path"].as_str().unwrap()))
.collect::<Vec<_>>();
for expected in [
Path::new("Slack").join("Cache"),
Path::new("Slack").join("Code Cache"),
Path::new("Slack").join("GPUCache"),
] {
assert!(
target_paths.iter().any(|path| path.ends_with(&expected)),
"missing Slack target {expected:?}"
);
}
assert!(targets.iter().all(|target| {
target["rule_id"] == "windows.slack-cache"
&& !target["path"].as_str().unwrap().contains("Local Storage")
&& !target["path"].as_str().unwrap().contains("IndexedDB")
&& !target["path"].as_str().unwrap().contains("Service Worker")
}));
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_skips_warning_rule_without_named_gate() {
let temp = tempfile::tempdir().unwrap();
write_slack_cache_fixture(&temp);
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"windows.slack-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["allowed_targets"], 0);
assert_eq!(value["summary"]["skipped_targets"], 3);
assert_eq!(
value["summary"]["issue_matrix"][0]["reason_code"],
"warning-gate-required"
);
assert_eq!(
value["summary"]["warning_matrix"][0]["warning"],
"active-process"
);
assert!(value["targets"].as_array().unwrap().iter().all(|target| {
target["reason_code"] == "warning-gate-required"
&& target["warnings"].as_array().unwrap()[0] == "active-process"
&& target["evidence"][0]["kind"] == "issue"
&& target["evidence"][0]["reason_code"] == "warning-gate-required"
&& target["evidence"][1]["kind"] == "warning"
&& target["evidence"][1]["warning"] == "active-process"
}));
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_reports_wechat_cache_rule() {
let temp = tempfile::tempdir().unwrap();
write_wechat_cache_fixture(&temp);
let output = common::isolated::isolated_rebecca(&temp)
.env("APPDATA", temp.path().join("roaming"))
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"windows.wechat-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 4);
assert_eq!(value["summary"]["allowed_targets"], 4);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 14);
let targets = value["targets"].as_array().unwrap();
assert_eq!(targets.len(), 4);
assert!(targets.iter().all(|target| {
target["rule_id"] == "windows.wechat-cache"
&& target["status"] == "allowed"
&& !target["path"].as_str().unwrap().contains("bak")
}));
for expected in [
Path::new("Tencent")
.join("WeChat")
.join("radium")
.join("cache"),
Path::new("Tencent").join("WeChat").join("WmpfCache"),
Path::new("Tencent")
.join("WeChat")
.join("radium")
.join("web")
.join("profiles")
.join("multitab_abc")
.join("Cache")
.join("Cache_Data"),
Path::new("Tencent")
.join("WeChat")
.join("radium")
.join("web")
.join("profiles")
.join("web_shell")
.join("Cache")
.join("Cache_Data"),
] {
assert!(
targets.iter().any(|target| {
PathBuf::from(target["path"].as_str().unwrap()).ends_with(&expected)
}),
"missing WeChat target {expected:?}"
);
}
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_honors_exclude_flag() {
let temp = tempfile::tempdir().unwrap();
write_slack_cache_fixture(&temp);
let protected_cache = temp.path().join("roaming").join("Slack").join("Cache");
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"windows.slack-cache",
"--allow-warning",
"active-process",
"--exclude",
protected_cache.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 3);
assert_eq!(value["summary"]["allowed_targets"], 2);
assert_eq!(value["summary"]["blocked_targets"], 1);
assert_eq!(value["summary"]["estimated_bytes"], 7);
let blocked = value["targets"]
.as_array()
.unwrap()
.iter()
.find(|target| target["status"] == "blocked")
.expect("expected excluded target to be blocked");
assert_eq!(blocked["rule_id"], "windows.slack-cache");
assert_eq!(blocked["reason_code"], "safety-policy-blocked");
assert!(
blocked["reason"]
.as_str()
.unwrap()
.contains("user-protected path")
);
assert!(
PathBuf::from(blocked["path"].as_str().unwrap())
.ends_with(Path::new("Slack").join("Cache"))
);
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_honors_config_protected_paths() {
let temp = tempfile::tempdir().unwrap();
write_slack_cache_fixture(&temp);
let config_dir = temp.path().join("rebecca-config");
fs::create_dir_all(&config_dir).unwrap();
let protected_cache = temp.path().join("roaming").join("Slack").join("GPUCache");
fs::write(
config_dir.join("config.toml"),
format!(
r#"
version = 1
[protection]
protected_paths = ['{}']
"#,
protected_cache.display()
),
)
.unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"windows.slack-cache",
"--allow-warning",
"active-process",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["allowed_targets"], 2);
assert_eq!(value["summary"]["blocked_targets"], 1);
assert!(value["targets"].as_array().unwrap().iter().any(|target| {
target["status"] == "blocked"
&& target["reason"]
.as_str()
.unwrap()
.contains("user-protected path")
&& PathBuf::from(target["path"].as_str().unwrap())
.ends_with(Path::new("Slack").join("GPUCache"))
}));
}
#[test]
fn clean_exclude_rejects_relative_paths() {
let temp = tempfile::tempdir().unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
common::support::current_platform_user_temp_rule_id(),
"--exclude",
"relative/cache",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = common::support::stderr(&output);
assert!(stderr.contains("invalid protected path"));
assert!(stderr.contains("path must be absolute"));
}
#[test]
fn clean_dry_run_json_deduplicates_overlapping_system_targets() {
let temp = tempfile::tempdir().unwrap();
let local = temp.path().join("local");
let temp_cache = local.join("Temp");
fs::create_dir_all(&temp_cache).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.env("LOCALAPPDATA", &local)
.env("APPDATA", temp.path().join("roaming"))
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
common::support::current_platform_user_temp_rule_id(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 2);
assert_eq!(value["summary"]["allowed_targets"], 1);
assert_eq!(value["summary"]["skipped_targets"], 1);
assert_eq!(value["summary"]["estimated_bytes"], 5);
assert_eq!(value["summary"]["issue_matrix"][0]["status"], "skipped");
assert_eq!(
value["summary"]["issue_matrix"][0]["reason_code"],
"duplicate-target-path"
);
assert_eq!(value["summary"]["issue_matrix"][0]["targets"], 1);
assert!(
value["targets"]
.as_array()
.unwrap()
.iter()
.any(|target| target["reason"] == "duplicate target path already covered")
);
assert!(
value["targets"]
.as_array()
.unwrap()
.iter()
.any(|target| target["reason_code"] == "duplicate-target-path")
);
}
#[test]
fn clean_dry_run_blocks_rebecca_storage_targets_from_config() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
let config_dir = temp.path().join("rebecca-config");
fs::create_dir_all(&temp_cache).unwrap();
fs::create_dir_all(&config_dir).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
fs::write(
config_dir.join("config.toml"),
format!(
r#"
version = 1
[app_paths]
cache_dir = '{}'
"#,
temp_cache.display()
),
)
.unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env_remove("REBECCA_CACHE_DIR")
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
common::support::current_platform_user_temp_rule_id(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["blocked_targets"], 1);
assert!(
value["summary"]["issue_matrix"]
.as_array()
.unwrap()
.iter()
.any(|issue| issue["status"] == "blocked"
&& issue["reason_code"] == "safety-policy-blocked"
&& issue["targets"] == 1)
);
let blocked = value["targets"]
.as_array()
.unwrap()
.iter()
.find(|target| target["status"] == "blocked")
.expect("expected a blocked Rebecca-owned storage target");
assert_eq!(blocked["reason_code"], "safety-policy-blocked");
assert!(
blocked["reason"]
.as_str()
.unwrap()
.contains("Rebecca-owned Cache dir")
);
assert!(temp_cache.join("cache.tmp").exists());
}
#[test]
fn clean_human_output_reports_issue_matrix_for_skipped_targets() {
let temp = tempfile::tempdir().unwrap();
let local = temp.path().join("local");
let temp_cache = local.join("Temp");
fs::create_dir_all(&temp_cache).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.env("LOCALAPPDATA", &local)
.env("APPDATA", temp.path().join("roaming"))
.args(["clean", "--dry-run", "--category", "system"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Decision: preview only; no files were deleted."));
assert!(stdout.contains("Next command: rebecca clean --yes --category system"));
assert!(stdout.contains("Issue matrix:"));
assert!(stdout.contains("- skipped duplicate-target-path: 1 target, 0 (0 B)"));
}
#[cfg(windows)]
#[test]
fn clean_human_output_reports_slack_cache_rule() {
let temp = tempfile::tempdir().unwrap();
write_slack_cache_fixture(&temp);
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-progress",
"--rule",
"windows.slack-cache",
"--allow-warning",
"active-process",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Reclaimable now: 9 (9 B)"));
assert!(stdout.contains("Execution: would move allowed targets to recoverable trash."));
assert!(stdout.contains(
"Next command: rebecca clean --yes --rule windows.slack-cache --allow-warning active-process"
));
assert!(stdout.contains("Required opt-ins in next command: --allow-warning active-process."));
assert!(stdout.contains("Warning gates in plan: active-process."));
assert!(stdout.contains("Doctor hint: rebecca doctor active-processes"));
assert!(stdout.contains("Targets: 3"));
assert!(stdout.contains("Allowed: 3"));
assert!(stdout.contains("Target details:"));
assert!(stdout.contains("allowed (3)"));
assert!(stdout.contains("windows.slack-cache"));
assert!(stdout.contains("Warning matrix:"));
assert!(stdout.contains("active-process: 3 targets, 9 (9 B)"));
assert!(stdout.contains("[warnings: active-process]"));
assert!(stdout.contains(&Path::new("Slack").join("Cache").display().to_string()));
assert!(!stdout.contains("Local Storage"));
assert!(!stdout.contains("IndexedDB"));
assert!(!stdout.contains("Service Worker"));
}
#[cfg(windows)]
#[test]
fn clean_human_output_explains_skipped_safety_opt_in() {
let temp = tempfile::tempdir().unwrap();
let local = temp.path().join("local");
let roaming = temp.path().join("roaming");
let local_npm_cache = local.join("npm-cache").join("_cacache");
let npm_cache = roaming.join("npm-cache").join("_cacache");
fs::create_dir_all(&local_npm_cache).unwrap();
fs::create_dir_all(&npm_cache).unwrap();
fs::write(local_npm_cache.join("index.bin"), b"abcd").unwrap();
fs::write(npm_cache.join("index.bin"), b"abcd").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("LOCALAPPDATA", &local)
.env("APPDATA", &roaming)
.args([
"clean",
"--dry-run",
"--no-progress",
"--rule",
"windows.npm-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Execution: no eligible target would be deleted."));
assert!(stdout.contains("Resolve before execution:"));
assert!(stdout.contains(
"- skipped safety-opt-in-required: add --allow-moderate or --allow-risky after reviewing the rule."
));
assert!(stdout.contains("Issue matrix:"));
assert!(stdout.contains("- skipped safety-opt-in-required: 2 targets, 0 (0 B)"));
assert!(!stdout.contains("Next command:"));
}
#[test]
fn clean_human_output_reports_blocked_target_details() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
let config_dir = temp.path().join("rebecca-config");
fs::create_dir_all(&temp_cache).unwrap();
fs::create_dir_all(&config_dir).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
fs::write(
config_dir.join("config.toml"),
format!(
r#"
version = 1
[app_paths]
cache_dir = '{}'
"#,
temp_cache.display()
),
)
.unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env_remove("REBECCA_CACHE_DIR")
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args([
"clean",
"--dry-run",
"--no-progress",
"--rule",
common::support::current_platform_user_temp_rule_id(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Issue matrix:"));
assert!(stdout.contains("- blocked safety-policy-blocked: 1 target, 0 (0 B)"));
assert!(stdout.contains("Target details:"));
assert!(stdout.contains("blocked (1)"));
assert!(stdout.contains(common::support::current_platform_user_temp_rule_id()));
assert!(stdout.contains(&temp_cache.display().to_string()));
assert!(stdout.contains("Rebecca-owned Cache dir"));
}
#[cfg(windows)]
#[test]
fn clean_human_output_highlights_largest_targets_by_size() {
let temp = tempfile::tempdir().unwrap();
let local = temp.path().join("local");
let edge_cache = local.join("Microsoft/Edge/User Data/Default/Cache");
let chrome_profile_code_cache = local.join("Google/Chrome/User Data/Profile 1/Code Cache");
let chrome_default_cache = local.join("Google/Chrome/User Data/Default/Cache");
fs::create_dir_all(&edge_cache).unwrap();
fs::create_dir_all(&chrome_profile_code_cache).unwrap();
fs::create_dir_all(&chrome_default_cache).unwrap();
fs::write(edge_cache.join("edge.bin"), b"1234567890").unwrap();
fs::write(chrome_profile_code_cache.join("code.bin"), b"123456").unwrap();
fs::write(chrome_default_cache.join("chrome.bin"), b"1234").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("LOCALAPPDATA", &local)
.args(["clean", "--dry-run", "--category", "browser"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Estimated bytes: 20 (20 B)"));
assert!(stdout.contains("Largest estimated targets:"));
assert!(stdout.contains("Target details:"));
assert!(stdout.contains("allowed (3)"));
assert!(stdout.contains("skipped ("));
let largest_section = stdout
.split("Largest estimated targets:")
.nth(1)
.expect("largest section should be present")
.split("Target details:")
.next()
.expect("target details section should follow largest section");
let edge_position = largest_section
.find("windows.edge-cache")
.expect("edge target should be in largest section");
let chrome_position = largest_section
.find("windows.chrome-cache")
.expect("chrome target should be in largest section");
assert!(
edge_position < chrome_position,
"largest section should sort targets by estimated bytes"
);
}
#[test]
fn clean_dry_run_accepts_no_progress_flag() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
fs::create_dir_all(&temp_cache).unwrap();
fs::write(temp_cache.join("cache.tmp"), b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args([
"clean",
"--dry-run",
"--no-progress",
"--rule",
common::support::current_platform_user_temp_rule_id(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Cleanup mode: dry-run"));
assert!(stdout.contains("Target details:"));
}
#[test]
fn clean_help_lists_scan_cache_opt_out() {
let output = common::command::rebecca()
.args(["clean", "--help"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("--scan-cache"));
assert!(stdout.contains("--no-scan-cache"));
assert!(stdout.contains("--scan-backend"));
assert!(stdout.contains("windows-native"));
assert!(stdout.contains("windows-ntfs-mft-experimental"));
}
#[test]
fn clean_scan_cache_flags_conflict() {
let output = common::command::rebecca()
.args(["clean", "--scan-cache", "--no-scan-cache"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = common::support::stderr(&output);
assert!(stderr.contains("--scan-cache"));
assert!(stderr.contains("--no-scan-cache"));
}
#[cfg(windows)]
#[test]
fn clean_dry_run_writes_scan_cache_by_default_for_file_targets() {
let temp = tempfile::tempdir().unwrap();
let explorer = temp
.path()
.join("local")
.join("Microsoft")
.join("Windows")
.join("Explorer");
fs::create_dir_all(&explorer).unwrap();
fs::write(explorer.join("thumbcache_96.db"), b"thumb").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"windows.thumbnail-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["estimated_bytes"], 5);
let scan_cache_dir = temp.path().join("rebecca-cache").join("scan");
let cache_entries = fs::read_dir(scan_cache_dir).unwrap().count();
assert_eq!(cache_entries, 1);
}
#[cfg(windows)]
#[test]
fn clean_dry_run_no_scan_cache_does_not_write_file_target_cache() {
let temp = tempfile::tempdir().unwrap();
let explorer = temp
.path()
.join("local")
.join("Microsoft")
.join("Windows")
.join("Explorer");
fs::create_dir_all(&explorer).unwrap();
fs::write(explorer.join("thumbcache_96.db"), b"thumb").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--no-scan-cache",
"--rule",
"windows.thumbnail-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["estimated_bytes"], 5);
assert!(!temp.path().join("rebecca-cache").join("scan").exists());
}
#[cfg(windows)]
#[test]
fn clean_yes_does_not_write_scan_cache_by_default() {
let temp = tempfile::tempdir().unwrap();
let explorer = temp
.path()
.join("local")
.join("Microsoft")
.join("Windows")
.join("Explorer");
fs::create_dir_all(&explorer).unwrap();
fs::write(explorer.join("thumbcache_96.db"), b"thumb").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--yes",
"--format",
"json",
"--rule",
"windows.thumbnail-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["request"]["mode"], "recoverable-delete");
assert_eq!(value["execution_report"]["summary"]["completed_actions"], 1);
assert_eq!(
value["execution_report"]["summary"]["pending_reclaim_bytes"],
5
);
assert!(!temp.path().join("rebecca-cache").join("scan").exists());
}
#[cfg(windows)]
#[test]
fn clean_dry_run_scan_cache_flag_writes_file_target_cache() {
let temp = tempfile::tempdir().unwrap();
let explorer = temp
.path()
.join("local")
.join("Microsoft")
.join("Windows")
.join("Explorer");
fs::create_dir_all(&explorer).unwrap();
fs::write(explorer.join("thumbcache_96.db"), b"thumb").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.thumbnail-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["estimated_bytes"], 5);
let scan_cache_dir = temp.path().join("rebecca-cache").join("scan");
let cache_entries = fs::read_dir(scan_cache_dir).unwrap().count();
assert_eq!(cache_entries, 1);
}
#[cfg(windows)]
#[test]
fn clean_dry_run_scan_cache_flag_reuses_directory_target_cache() {
let temp = tempfile::tempdir().unwrap();
let edge_cache = temp
.path()
.join("local")
.join("Microsoft")
.join("Edge")
.join("User Data")
.join("Default")
.join("Cache");
fs::create_dir_all(&edge_cache).unwrap();
fs::write(edge_cache.join("cache.bin"), b"edge").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["estimated_bytes"], 4);
assert_eq!(value["targets"][0]["estimate_source"], "fresh-scan");
assert_eq!(
value["targets"][0]["estimate_backend"],
"portable-recursive"
);
assert_eq!(value["targets"][0]["estimate_confidence"], "exact");
assert_eq!(
value["targets"][0]["estimate_backend_evidence"]["cache_events"][0]["cache"],
"scan-cache"
);
assert_eq!(
value["targets"][0]["estimate_backend_evidence"]["cache_events"][0]["outcome"],
"miss"
);
assert_eq!(
value["targets"][0]["estimate_backend_evidence"]["cache_events"][0]["reason"],
"missing"
);
assert!(
value["targets"][0]
.get("estimate_fallback_reason")
.is_none()
);
let scan_cache_dir = temp.path().join("rebecca-cache").join("scan");
let cache_files = fs::read_dir(scan_cache_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.collect::<Vec<_>>();
assert_eq!(cache_files.len(), 1);
let cache_file = &cache_files[0];
let mut record: serde_json::Value =
serde_json::from_slice(&fs::read(cache_file).unwrap()).unwrap();
record["report"]["bytes_scanned"] = serde_json::json!(99);
fs::write(cache_file, serde_json::to_vec_pretty(&record).unwrap()).unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["estimated_bytes"], 99);
assert_eq!(value["targets"][0]["estimate_source"], "scan-cache");
assert_eq!(
value["targets"][0]["estimate_backend"],
"portable-recursive"
);
assert_eq!(value["targets"][0]["estimate_confidence"], "exact");
assert!(
value["targets"][0]
.get("estimate_fallback_reason")
.is_none()
);
assert!(value.get("scan_cache").is_none());
assert!(value["summary"].get("scan_cache").is_none());
}
#[cfg(windows)]
#[test]
fn clean_dry_run_scan_cache_policy_expires_directory_records_from_config() {
let temp = tempfile::tempdir().unwrap();
let config_dir = temp.path().join("rebecca-config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("config.toml"),
r#"
version = 1
[scan_cache]
directory_record_max_age_seconds = 1
"#,
)
.unwrap();
let edge_cache = temp
.path()
.join("local")
.join("Microsoft")
.join("Edge")
.join("User Data")
.join("Default")
.join("Cache");
fs::create_dir_all(&edge_cache).unwrap();
fs::write(edge_cache.join("cache.bin"), b"edge").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let scan_cache_dir = temp.path().join("rebecca-cache").join("scan");
let cache_files = fs::read_dir(scan_cache_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.collect::<Vec<_>>();
assert_eq!(cache_files.len(), 1);
let cache_file = &cache_files[0];
let mut record: serde_json::Value =
serde_json::from_slice(&fs::read(cache_file).unwrap()).unwrap();
let stale_written_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.saturating_sub(2);
record["report"]["bytes_scanned"] = serde_json::json!(99);
record["written_at_unix_seconds"] = serde_json::json!(stale_written_at);
fs::write(cache_file, serde_json::to_vec_pretty(&record).unwrap()).unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["estimated_bytes"], 4);
assert_eq!(value["targets"][0]["estimate_source"], "fresh-scan");
}
#[cfg(windows)]
#[test]
fn clean_dry_run_scan_cache_reports_invalid_policy_config() {
let temp = tempfile::tempdir().unwrap();
let config_dir = temp.path().join("rebecca-config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("config.toml"),
r#"
[scan_cache]
directory_record_max_age_seconds = 0
"#,
)
.unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = common::support::stderr(&output);
assert!(stderr.contains("config parse failed"));
assert!(stderr.contains("scan_cache.directory_record_max_age_seconds must be at least 1"));
assert!(stderr.contains("config.toml"));
}
#[cfg(windows)]
#[test]
fn clean_human_output_summarizes_scan_cache_activity() {
let temp = tempfile::tempdir().unwrap();
let edge_cache = temp
.path()
.join("local")
.join("Microsoft")
.join("Edge")
.join("User Data")
.join("Default")
.join("Cache");
fs::create_dir_all(&edge_cache).unwrap();
fs::write(edge_cache.join("cache.bin"), b"edge").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-progress",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Scan cache summary: 1 hit, 0 misses, 0 skipped writes, 0 pruned records")
);
assert!(stdout.contains("[estimate: scan-cache, portable-recursive, exact]"));
}
#[cfg(windows)]
#[test]
fn clean_human_output_reports_scan_cache_prune_activity() {
let temp = tempfile::tempdir().unwrap();
let config_dir = temp.path().join("rebecca-config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("config.toml"),
r#"
version = 1
[scan_cache]
directory_record_max_age_seconds = 1
"#,
)
.unwrap();
let edge_cache = temp
.path()
.join("local")
.join("Microsoft")
.join("Edge")
.join("User Data")
.join("Default")
.join("Cache");
fs::create_dir_all(&edge_cache).unwrap();
fs::write(edge_cache.join("cache.bin"), b"edge").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let scan_cache_dir = temp.path().join("rebecca-cache").join("scan");
let cache_files = fs::read_dir(&scan_cache_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.collect::<Vec<_>>();
assert_eq!(cache_files.len(), 1);
let cache_file = &cache_files[0];
let mut record: serde_json::Value =
serde_json::from_slice(&fs::read(cache_file).unwrap()).unwrap();
record["written_at_unix_seconds"] = serde_json::json!(0);
fs::write(cache_file, serde_json::to_vec_pretty(&record).unwrap()).unwrap();
let stale_root = temp.path().join("stale-cache");
fs::create_dir_all(&stale_root).unwrap();
fs::write(stale_root.join("old.bin"), b"old").unwrap();
let stale_root_str = stale_root.display().to_string().replace('\\', "\\\\");
let stale_cache_file = scan_cache_dir.join("ffffffffffffffff.json");
fs::write(
&stale_cache_file,
format!(
r#"{{
"version": 1,
"root": "{stale_root_str}",
"fingerprint": {{
"file_type": "directory",
"len": 0,
"modified_unix_seconds": 0
}},
"report": {{
"bytes_scanned": 3,
"files_scanned": 1,
"directories_scanned": 1
}},
"written_at_unix_seconds": 0
}}"#
),
)
.unwrap();
fs::remove_dir_all(&stale_root).unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-progress",
"--scan-cache",
"--rule",
"windows.edge-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Scan cache summary: 0 hits, 1 miss, 0 skipped writes, 2 pruned records")
);
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_expands_steam_rules_with_discovery_override() {
for case in common::steam::STEAM_INSTALL_RULE_CASES {
let temp = tempfile::tempdir().unwrap();
let value = steam_dry_run_json_output(&temp, case);
assert_eq!(value["summary"]["total_targets"], 1, "{}", case.rule_id);
assert_eq!(value["summary"]["allowed_targets"], 1, "{}", case.rule_id);
assert_eq!(value["summary"]["skipped_targets"], 0, "{}", case.rule_id);
assert_eq!(
value["summary"]["estimated_bytes"],
case.bytes.len() as u64,
"{}",
case.rule_id
);
let targets = value["targets"].as_array().unwrap();
assert_eq!(targets.len(), 1, "{}", case.rule_id);
assert_eq!(targets[0]["rule_id"], case.rule_id);
assert_eq!(targets[0]["status"], "allowed");
assert_eq!(
targets[0]["restore_hint"].as_str().unwrap(),
case.expected_restore_hint.unwrap()
);
assert!(
targets[0]["path"]
.as_str()
.unwrap()
.ends_with(case.relative_path)
);
}
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_uses_install_root_when_libraryfolders_is_unreadable() {
let temp = tempfile::tempdir().unwrap();
let steam = temp.path().join("Steam");
let steamapps = steam.join("steamapps");
let httpcache = steam.join("appcache").join("httpcache");
std::fs::create_dir_all(steamapps.join("libraryfolders.vdf")).unwrap();
fs::create_dir_all(&httpcache).unwrap();
fs::write(httpcache.join("cache.bin"), b"abcd").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("REBECCA_STEAM_DISCOVERY_PATH", &steam)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--allow-warning",
"source-boundary",
"--rule",
"windows.steam-install-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 1);
assert_eq!(value["summary"]["allowed_targets"], 1);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 4);
let targets = value["targets"].as_array().unwrap();
assert_eq!(targets.len(), 1);
assert_eq!(targets[0]["rule_id"], "windows.steam-install-cache");
assert_eq!(targets[0]["status"], "allowed");
assert!(
targets[0]["path"]
.as_str()
.unwrap()
.ends_with("appcache/httpcache")
);
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_allows_moderate_rules_with_opt_in() {
let temp = tempfile::tempdir().unwrap();
let local = temp.path().join("local");
let roaming = temp.path().join("roaming");
let local_npm_cache = local.join("npm-cache").join("_cacache");
let npm_cache = roaming.join("npm-cache").join("_cacache");
fs::create_dir_all(&local_npm_cache).unwrap();
fs::create_dir_all(&npm_cache).unwrap();
fs::write(local_npm_cache.join("index.bin"), b"abcd").unwrap();
fs::write(npm_cache.join("index.bin"), b"abcd").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--allow-moderate",
"--rule",
"windows.npm-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 2);
assert_eq!(value["summary"]["allowed_targets"], 2);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 8);
let targets = value["targets"].as_array().unwrap();
assert_eq!(targets.len(), 2);
assert!(targets.iter().all(|target| {
target["rule_id"] == "windows.npm-cache" && target["status"] == "allowed"
}));
}
#[test]
fn clean_dry_run_json_gates_linux_developer_cache_rules() {
if !cfg!(target_os = "linux") {
return;
}
let temp = tempfile::tempdir().unwrap();
let pip_cache = temp.path().join("cache").join("pip");
fs::create_dir_all(&pip_cache).unwrap();
fs::write(pip_cache.join("http-cache.bin"), b"abcd").unwrap();
let skipped = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"linux.pip-cache",
])
.output()
.unwrap();
assert!(
skipped.status.success(),
"stderr: {}",
common::support::stderr(&skipped)
);
let skipped_value: serde_json::Value = common::support::api_data(&skipped.stdout);
assert_eq!(skipped_value["summary"]["total_targets"], 1);
assert_eq!(skipped_value["summary"]["allowed_targets"], 0);
assert_eq!(skipped_value["summary"]["skipped_targets"], 1);
assert_eq!(skipped_value["targets"][0]["rule_id"], "linux.pip-cache");
assert_eq!(skipped_value["targets"][0]["status"], "skipped");
let allowed = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-moderate",
"--rule",
"linux.pip-cache",
])
.output()
.unwrap();
assert!(
allowed.status.success(),
"stderr: {}",
common::support::stderr(&allowed)
);
let allowed_value: serde_json::Value = common::support::api_data(&allowed.stdout);
assert_eq!(allowed_value["summary"]["total_targets"], 1);
assert_eq!(allowed_value["summary"]["allowed_targets"], 1);
assert_eq!(allowed_value["summary"]["skipped_targets"], 0);
assert_eq!(allowed_value["summary"]["estimated_bytes"], 4);
assert_eq!(allowed_value["targets"][0]["rule_id"], "linux.pip-cache");
assert_eq!(allowed_value["targets"][0]["status"], "allowed");
}
#[test]
fn clean_dry_run_json_gates_linux_package_manager_cache_rules() {
if !cfg!(target_os = "linux") {
return;
}
let temp = tempfile::tempdir().unwrap();
let without_moderate = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"linux.apt-cache",
])
.output()
.unwrap();
assert!(
without_moderate.status.success(),
"stderr: {}",
common::support::stderr(&without_moderate)
);
let without_moderate_value: serde_json::Value =
common::support::api_data(&without_moderate.stdout);
assert_eq!(without_moderate_value["summary"]["total_targets"], 1);
assert_eq!(without_moderate_value["summary"]["allowed_targets"], 0);
assert_eq!(without_moderate_value["summary"]["skipped_targets"], 1);
assert_eq!(without_moderate_value["targets"][0]["status"], "skipped");
assert_eq!(
without_moderate_value["targets"][0]["reason_code"],
"safety-opt-in-required"
);
let without_warning = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-moderate",
"--rule",
"linux.apt-cache",
])
.output()
.unwrap();
assert!(
without_warning.status.success(),
"stderr: {}",
common::support::stderr(&without_warning)
);
let without_warning_value: serde_json::Value =
common::support::api_data(&without_warning.stdout);
assert_eq!(without_warning_value["summary"]["total_targets"], 1);
assert_eq!(without_warning_value["summary"]["allowed_targets"], 0);
assert_eq!(without_warning_value["summary"]["skipped_targets"], 1);
assert_eq!(
without_warning_value["targets"][0]["reason_code"],
"warning-gate-required"
);
}
#[test]
fn clean_dry_run_json_gates_linux_browser_cache_warnings() {
if !cfg!(target_os = "linux") {
return;
}
let temp = tempfile::tempdir().unwrap();
let chrome_cache = temp
.path()
.join("cache")
.join("google-chrome")
.join("Default")
.join("Cache");
let chrome_history = temp
.path()
.join("config")
.join("google-chrome")
.join("Default")
.join("History");
fs::create_dir_all(&chrome_cache).unwrap();
fs::create_dir_all(chrome_history.parent().unwrap()).unwrap();
fs::write(chrome_cache.join("data.bin"), b"abcd").unwrap();
fs::write(&chrome_history, b"keep").unwrap();
let skipped = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"linux.chrome-cache",
])
.output()
.unwrap();
assert!(
skipped.status.success(),
"stderr: {}",
common::support::stderr(&skipped)
);
let skipped_value: serde_json::Value = common::support::api_data(&skipped.stdout);
let skipped_targets = skipped_value["targets"].as_array().unwrap();
assert_eq!(
skipped_value["summary"]["total_targets"],
skipped_targets.len()
);
assert_eq!(skipped_value["summary"]["allowed_targets"], 0);
assert_eq!(
skipped_value["summary"]["skipped_targets"],
skipped_targets.len()
);
let skipped_cache_target = skipped_targets
.iter()
.find(|target| {
target["path"].as_str().unwrap() == "%XDG_CACHE_HOME%/google-chrome/Default/Cache"
})
.expect("Chrome cache template target should be present before warning opt-in");
assert_eq!(skipped_cache_target["status"], "skipped");
assert_eq!(skipped_cache_target["reason_code"], "warning-gate-required");
let allowed = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-warning",
"active-process",
"--rule",
"linux.chrome-cache",
])
.output()
.unwrap();
assert!(
allowed.status.success(),
"stderr: {}",
common::support::stderr(&allowed)
);
let allowed_value: serde_json::Value = common::support::api_data(&allowed.stdout);
let allowed_targets = allowed_value["targets"].as_array().unwrap();
assert_eq!(
allowed_value["summary"]["total_targets"],
allowed_targets.len()
);
assert_eq!(allowed_value["summary"]["allowed_targets"], 1);
assert_eq!(
allowed_value["summary"]["skipped_targets"],
allowed_targets.len() - 1
);
assert_eq!(allowed_value["summary"]["estimated_bytes"], 4);
let allowed_cache_target = allowed_targets
.iter()
.find(|target| {
target["path"]
.as_str()
.unwrap()
.ends_with("/cache/google-chrome/Default/Cache")
})
.expect("existing Chrome cache target should be allowed after warning opt-in");
assert_eq!(allowed_cache_target["rule_id"], "linux.chrome-cache");
assert_eq!(allowed_cache_target["status"], "allowed");
assert_eq!(allowed_cache_target["estimated_bytes"], 4);
assert!(
chrome_history.exists(),
"private browser data must not be targeted"
);
}
#[test]
fn clean_dry_run_json_gates_linux_desktop_app_cache_warnings() {
if !cfg!(target_os = "linux") {
return;
}
let temp = tempfile::tempdir().unwrap();
let native_cache = temp
.path()
.join("config")
.join("Slack")
.join("Cache")
.join("cache.bin");
let flatpak_cache = temp
.path()
.join(".var")
.join("app")
.join("com.slack.Slack")
.join("cache")
.join("flatpak.bin");
let snap_cache = temp
.path()
.join("snap")
.join("slack")
.join("common")
.join(".cache")
.join("snap.bin");
let flatpak_data = temp
.path()
.join(".var")
.join("app")
.join("com.slack.Slack")
.join("data")
.join("state.db");
let snap_config = temp
.path()
.join("snap")
.join("slack")
.join("common")
.join(".config")
.join("settings.json");
write_fixture_file(&native_cache, b"ab");
write_fixture_file(&flatpak_cache, b"cde");
write_fixture_file(&snap_cache, b"fghi");
write_fixture_file(&flatpak_data, b"keep");
write_fixture_file(&snap_config, b"keep");
let skipped = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"linux.slack-cache",
])
.output()
.unwrap();
assert!(
skipped.status.success(),
"stderr: {}",
common::support::stderr(&skipped)
);
let skipped_value: serde_json::Value = common::support::api_data(&skipped.stdout);
let skipped_targets = skipped_value["targets"].as_array().unwrap();
assert_eq!(
skipped_value["summary"]["total_targets"],
skipped_targets.len()
);
assert_eq!(skipped_value["summary"]["allowed_targets"], 0);
assert_eq!(
skipped_value["summary"]["skipped_targets"],
skipped_targets.len()
);
assert!(
skipped_targets
.iter()
.all(|target| target["status"] == "skipped")
);
for expected_path in [
"%XDG_CONFIG_HOME%/Slack/Cache",
"%HOME%/.var/app/com.slack.Slack/cache",
"%HOME%/snap/slack/common/.cache",
] {
let target = skipped_targets
.iter()
.find(|target| target["path"].as_str().unwrap() == expected_path)
.unwrap_or_else(|| {
panic!("Slack cache template target missing before warning opt-in: {expected_path}")
});
assert_eq!(target["reason_code"], "warning-gate-required");
}
let allowed = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-warning",
"active-process",
"--rule",
"linux.slack-cache",
])
.output()
.unwrap();
assert!(
allowed.status.success(),
"stderr: {}",
common::support::stderr(&allowed)
);
let allowed_value: serde_json::Value = common::support::api_data(&allowed.stdout);
let allowed_targets = allowed_value["targets"].as_array().unwrap();
assert_eq!(
allowed_value["summary"]["total_targets"],
allowed_targets.len()
);
assert_eq!(allowed_value["summary"]["allowed_targets"], 3);
assert_eq!(
allowed_value["summary"]["skipped_targets"],
allowed_targets.len() - 3
);
assert_eq!(allowed_value["summary"]["estimated_bytes"], 9);
for suffix in [
"/config/Slack/Cache",
"/.var/app/com.slack.Slack/cache",
"/snap/slack/common/.cache",
] {
let target = allowed_targets
.iter()
.find(|target| target["path"].as_str().unwrap().ends_with(suffix))
.unwrap_or_else(|| panic!("existing Slack cache target missing: {suffix}"));
assert_eq!(target["status"], "allowed");
}
let target_paths = allowed_targets
.iter()
.map(|target| target["path"].as_str().unwrap())
.collect::<Vec<_>>();
assert!(
target_paths.iter().all(|path| {
!path.contains("/.var/app/com.slack.Slack/data")
&& !path.contains("/snap/slack/common/.config")
}),
"durable Linux app state must not be targeted: {target_paths:?}"
);
assert!(
flatpak_data.exists(),
"Flatpak durable data must remain untouched"
);
assert!(
snap_config.exists(),
"Snap durable config must remain untouched"
);
}
#[test]
fn clean_dry_run_json_discovers_linux_steam_install_cache() {
if !cfg!(target_os = "linux") {
return;
}
let temp = tempfile::tempdir().unwrap();
let steam = temp.path().join(".steam").join("steam");
let httpcache = steam.join("appcache").join("httpcache").join("cache.bin");
let userdata = steam.join("userdata").join("123").join("config.vdf");
write_fixture_file(&httpcache, b"abcdef");
write_fixture_file(&userdata, b"keep");
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-warning",
"source-boundary",
"--rule",
"linux.steam-install-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 1);
assert_eq!(value["summary"]["allowed_targets"], 1);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 6);
assert_eq!(value["targets"][0]["rule_id"], "linux.steam-install-cache");
assert_eq!(value["targets"][0]["status"], "allowed");
assert!(
value["targets"][0]["path"]
.as_str()
.unwrap()
.contains("/.steam/steam/appcache/httpcache")
);
assert!(userdata.exists(), "Steam userdata must remain untouched");
}
#[test]
fn clean_dry_run_json_gates_macos_developer_cache_rules() {
if !cfg!(target_os = "macos") {
return;
}
let temp = tempfile::tempdir().unwrap();
let pip_cache = temp.path().join("Library").join("Caches").join("pip");
write_fixture_file(pip_cache.join("http-cache.bin"), b"abcd");
let skipped = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"macos.pip-cache",
])
.output()
.unwrap();
assert!(
skipped.status.success(),
"stderr: {}",
common::support::stderr(&skipped)
);
let skipped_value: serde_json::Value = common::support::api_data(&skipped.stdout);
assert_eq!(skipped_value["summary"]["total_targets"], 1);
assert_eq!(skipped_value["summary"]["allowed_targets"], 0);
assert_eq!(skipped_value["summary"]["skipped_targets"], 1);
assert_eq!(skipped_value["targets"][0]["rule_id"], "macos.pip-cache");
assert_eq!(skipped_value["targets"][0]["status"], "skipped");
let allowed = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-moderate",
"--rule",
"macos.pip-cache",
])
.output()
.unwrap();
assert!(
allowed.status.success(),
"stderr: {}",
common::support::stderr(&allowed)
);
let allowed_value: serde_json::Value = common::support::api_data(&allowed.stdout);
assert_eq!(allowed_value["summary"]["total_targets"], 1);
assert_eq!(allowed_value["summary"]["allowed_targets"], 1);
assert_eq!(allowed_value["summary"]["skipped_targets"], 0);
assert_eq!(allowed_value["summary"]["estimated_bytes"], 4);
assert_eq!(allowed_value["targets"][0]["rule_id"], "macos.pip-cache");
assert_eq!(allowed_value["targets"][0]["status"], "allowed");
}
#[test]
fn clean_dry_run_json_gates_macos_browser_cache_warnings() {
if !cfg!(target_os = "macos") {
return;
}
let temp = tempfile::tempdir().unwrap();
let chrome_profile = temp
.path()
.join("Library")
.join("Application Support")
.join("Google")
.join("Chrome")
.join("Default");
let chrome_cache = chrome_profile.join("Cache");
let chrome_history = chrome_profile.join("History");
write_fixture_file(chrome_cache.join("data.bin"), b"abcd");
write_fixture_file(&chrome_history, b"keep");
let skipped = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"macos.chrome-cache",
])
.output()
.unwrap();
assert!(
skipped.status.success(),
"stderr: {}",
common::support::stderr(&skipped)
);
let skipped_value: serde_json::Value = common::support::api_data(&skipped.stdout);
let skipped_targets = skipped_value["targets"].as_array().unwrap();
assert_eq!(
skipped_value["summary"]["total_targets"],
skipped_targets.len()
);
assert_eq!(skipped_value["summary"]["allowed_targets"], 0);
assert_eq!(
skipped_value["summary"]["skipped_targets"],
skipped_targets.len()
);
let skipped_cache_target = skipped_targets
.iter()
.find(|target| {
target["path"].as_str().unwrap()
== "%MACOS_APPLICATION_SUPPORT_HOME%/Google/Chrome/Default/Cache"
})
.expect("Chrome cache template target should be present before warning opt-in");
assert_eq!(skipped_cache_target["status"], "skipped");
assert_eq!(skipped_cache_target["reason_code"], "warning-gate-required");
let allowed = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-warning",
"active-process",
"--rule",
"macos.chrome-cache",
])
.output()
.unwrap();
assert!(
allowed.status.success(),
"stderr: {}",
common::support::stderr(&allowed)
);
let allowed_value: serde_json::Value = common::support::api_data(&allowed.stdout);
let allowed_targets = allowed_value["targets"].as_array().unwrap();
assert_eq!(
allowed_value["summary"]["total_targets"],
allowed_targets.len()
);
assert_eq!(allowed_value["summary"]["allowed_targets"], 1);
assert_eq!(
allowed_value["summary"]["skipped_targets"],
allowed_targets.len() - 1
);
assert_eq!(allowed_value["summary"]["estimated_bytes"], 4);
let allowed_cache_target = allowed_targets
.iter()
.find(|target| {
target["path"]
.as_str()
.unwrap()
.ends_with("/Library/Application Support/Google/Chrome/Default/Cache")
})
.expect("existing Chrome cache target should be allowed after warning opt-in");
assert_eq!(allowed_cache_target["rule_id"], "macos.chrome-cache");
assert_eq!(allowed_cache_target["status"], "allowed");
assert_eq!(allowed_cache_target["estimated_bytes"], 4);
assert!(
chrome_history.exists(),
"private browser data must not be targeted"
);
}
#[test]
fn clean_dry_run_json_gates_macos_desktop_app_cache_warnings() {
if !cfg!(target_os = "macos") {
return;
}
let temp = tempfile::tempdir().unwrap();
let slack = temp
.path()
.join("Library")
.join("Application Support")
.join("Slack");
write_fixture_file(slack.join("Cache").join("cache.bin"), b"ab");
write_fixture_file(slack.join("Code Cache").join("code.bin"), b"cde");
write_fixture_file(slack.join("GPUCache").join("gpu.bin"), b"fghi");
write_fixture_file(
slack.join("Local Storage").join("leveldb").join("LOG"),
b"keep",
);
let skipped = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--rule",
"macos.slack-cache",
])
.output()
.unwrap();
assert!(
skipped.status.success(),
"stderr: {}",
common::support::stderr(&skipped)
);
let skipped_value: serde_json::Value = common::support::api_data(&skipped.stdout);
let skipped_targets = skipped_value["targets"].as_array().unwrap();
assert_eq!(
skipped_value["summary"]["total_targets"],
skipped_targets.len()
);
assert_eq!(skipped_value["summary"]["allowed_targets"], 0);
assert_eq!(
skipped_value["summary"]["skipped_targets"],
skipped_targets.len()
);
for expected_path in [
"%MACOS_APPLICATION_SUPPORT_HOME%/Slack/Cache",
"%MACOS_APPLICATION_SUPPORT_HOME%/Slack/Code Cache",
"%MACOS_APPLICATION_SUPPORT_HOME%/Slack/GPUCache",
] {
let target = skipped_targets
.iter()
.find(|target| target["path"].as_str().unwrap() == expected_path)
.unwrap_or_else(|| {
panic!("Slack cache template target missing before warning opt-in: {expected_path}")
});
assert_eq!(target["reason_code"], "warning-gate-required");
}
let allowed = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-warning",
"active-process",
"--rule",
"macos.slack-cache",
])
.output()
.unwrap();
assert!(
allowed.status.success(),
"stderr: {}",
common::support::stderr(&allowed)
);
let allowed_value: serde_json::Value = common::support::api_data(&allowed.stdout);
let allowed_targets = allowed_value["targets"].as_array().unwrap();
assert_eq!(
allowed_value["summary"]["total_targets"],
allowed_targets.len()
);
assert_eq!(allowed_value["summary"]["allowed_targets"], 3);
assert_eq!(
allowed_value["summary"]["skipped_targets"],
allowed_targets.len() - 3
);
assert_eq!(allowed_value["summary"]["estimated_bytes"], 9);
for suffix in [
"/Library/Application Support/Slack/Cache",
"/Library/Application Support/Slack/Code Cache",
"/Library/Application Support/Slack/GPUCache",
] {
let target = allowed_targets
.iter()
.find(|target| target["path"].as_str().unwrap().ends_with(suffix))
.unwrap_or_else(|| panic!("existing Slack cache target missing: {suffix}"));
assert_eq!(target["status"], "allowed");
}
let target_paths = allowed_targets
.iter()
.map(|target| target["path"].as_str().unwrap())
.collect::<Vec<_>>();
assert!(
target_paths
.iter()
.all(|path| !path.contains("/Library/Application Support/Slack/Local Storage")),
"durable macOS app state must not be targeted: {target_paths:?}"
);
assert!(
slack
.join("Local Storage")
.join("leveldb")
.join("LOG")
.exists(),
"Slack durable data must remain untouched"
);
}
#[test]
fn clean_dry_run_json_discovers_macos_curated_developer_rules() {
if !cfg!(target_os = "macos") {
return;
}
let temp = tempfile::tempdir().unwrap();
let homebrew_cache = temp.path().join("Library").join("Caches").join("Homebrew");
write_fixture_file(
homebrew_cache.join("downloads").join("bottle.tar.gz"),
b"abcd",
);
write_fixture_file(homebrew_cache.join("api").join("formula.json"), b"ef");
write_fixture_file(homebrew_cache.join("Cask").join("app.dmg"), b"ghij");
let homebrew_tap = temp
.path()
.join("Library")
.join("Application Support")
.join("Homebrew")
.join("Library")
.join("Taps")
.join("keep.rb");
write_fixture_file(&homebrew_tap, b"keep");
let cocoapods_cache = temp.path().join("Library").join("Caches").join("CocoaPods");
write_fixture_file(cocoapods_cache.join("Pods").join("Alamofire.zip"), b"klm");
let xcode = temp.path().join("Library").join("Developer").join("Xcode");
write_fixture_file(
xcode
.join("DerivedData")
.join("Rebecca-abc123")
.join("Build")
.join("cache.bin"),
b"nopq",
);
write_fixture_file(
xcode
.join("Archives")
.join("2026-07-07")
.join("Rebecca.xcarchive")
.join("Info.plist"),
b"keep",
);
write_fixture_file(
xcode
.join("iOS DeviceSupport")
.join("18.0")
.join("Symbols")
.join("keep.bin"),
b"keep",
);
write_fixture_file(
temp.path()
.join("Library")
.join("MobileDevice")
.join("Provisioning Profiles")
.join("example.mobileprovision"),
b"keep",
);
let xcode_cache = temp
.path()
.join("Library")
.join("Caches")
.join("com.apple.dt.Xcode");
write_fixture_file(xcode_cache.join("cache.db"), b"rst");
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-moderate",
"--allow-warning",
"active-process",
"--allow-warning",
"permission-sensitive",
"--rule",
"macos.homebrew-cache",
"--rule",
"macos.cocoapods-cache",
"--rule",
"macos.xcode-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
let targets = value["targets"].as_array().unwrap();
assert_eq!(value["summary"]["allowed_targets"], 6);
let allowed_paths = targets
.iter()
.filter(|target| target["status"] == "allowed")
.map(|target| target["path"].as_str().unwrap())
.collect::<Vec<_>>();
let target_paths = targets
.iter()
.map(|target| target["path"].as_str().unwrap())
.collect::<Vec<_>>();
for expected in [
"/Library/Caches/Homebrew/downloads",
"/Library/Caches/Homebrew/api",
"/Library/Caches/Homebrew/Cask",
"/Library/Caches/CocoaPods",
"/Library/Developer/Xcode/DerivedData",
"/Library/Caches/com.apple.dt.Xcode",
] {
assert!(
allowed_paths.iter().any(|path| path.ends_with(expected)),
"missing allowed macOS developer cache target {expected}: {allowed_paths:?}"
);
}
for forbidden in [
"/Library/Application Support/Homebrew",
"/Library/Developer/Xcode/Archives",
"/Library/Developer/Xcode/iOS DeviceSupport",
"/Library/MobileDevice/Provisioning Profiles",
] {
assert!(
target_paths.iter().all(|path| !path.contains(forbidden)),
"durable macOS developer state must not be targeted: {target_paths:?}"
);
}
assert!(homebrew_tap.exists(), "Homebrew taps must remain untouched");
}
#[test]
fn clean_dry_run_json_discovers_macos_steam_install_cache() {
if !cfg!(target_os = "macos") {
return;
}
let temp = tempfile::tempdir().unwrap();
let steam = temp
.path()
.join("Library")
.join("Application Support")
.join("Steam");
let httpcache = steam.join("appcache").join("httpcache").join("cache.bin");
let userdata = steam.join("userdata").join("123").join("config.vdf");
write_fixture_file(&httpcache, b"abcdef");
write_fixture_file(&userdata, b"keep");
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--no-scan-cache",
"--format",
"json",
"--allow-warning",
"source-boundary",
"--rule",
"macos.steam-install-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 1);
assert_eq!(value["summary"]["allowed_targets"], 1);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 6);
assert_eq!(value["targets"][0]["rule_id"], "macos.steam-install-cache");
assert_eq!(value["targets"][0]["status"], "allowed");
assert!(
value["targets"][0]["path"]
.as_str()
.unwrap()
.contains("/Library/Application Support/Steam/appcache/httpcache")
);
assert!(userdata.exists(), "Steam userdata must remain untouched");
}
#[cfg(windows)]
#[test]
fn clean_dry_run_json_accepts_allow_risky_flag() {
let temp = tempfile::tempdir().unwrap();
let local = temp.path().join("local");
let roaming = temp.path().join("roaming");
let local_npm_cache = local.join("npm-cache").join("_cacache");
let npm_cache = roaming.join("npm-cache").join("_cacache");
fs::create_dir_all(&local_npm_cache).unwrap();
fs::create_dir_all(&npm_cache).unwrap();
fs::write(local_npm_cache.join("index.bin"), b"abcd").unwrap();
fs::write(npm_cache.join("index.bin"), b"abcd").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--allow-risky",
"--rule",
"windows.npm-cache",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["summary"]["total_targets"], 2);
assert_eq!(value["summary"]["allowed_targets"], 2);
assert_eq!(value["summary"]["skipped_targets"], 0);
assert_eq!(value["summary"]["estimated_bytes"], 8);
assert_eq!(value["request"]["allow_risky"], true);
let targets = value["targets"].as_array().unwrap();
assert_eq!(targets.len(), 2);
assert!(targets.iter().all(|target| {
target["rule_id"] == "windows.npm-cache" && target["status"] == "allowed"
}));
}
#[test]
fn clean_unknown_rule_returns_clear_error() {
let temp = tempfile::tempdir().unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--rule",
"missing.rule",
])
.output()
.unwrap();
assert!(!output.status.success());
assert!(common::support::stderr(&output).contains("invalid rule id"));
}
#[test]
fn clean_unknown_category_returns_clear_error() {
let temp = tempfile::tempdir().unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.args([
"clean",
"--dry-run",
"--format",
"json",
"--category",
"missing",
])
.output()
.unwrap();
assert!(!output.status.success());
assert!(common::support::stderr(&output).contains("invalid category"));
}
#[cfg(not(windows))]
#[test]
fn non_windows_execution_uses_recoverable_trash_backend() {
let temp = tempfile::tempdir().unwrap();
let temp_cache = temp.path().join("temp");
fs::create_dir_all(&temp_cache).unwrap();
let file = temp_cache.join("cache.tmp");
fs::write(&file, b"cache").unwrap();
let output = common::isolated::isolated_rebecca(&temp)
.env("TEMP", &temp_cache)
.env("TMPDIR", &temp_cache)
.args([
"clean",
"--yes",
"--format",
"json",
"--rule",
common::support::current_platform_user_temp_rule_id(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
common::support::stderr(&output)
);
assert!(temp_cache.exists(), "clean should preserve the temp root");
assert!(!file.exists(), "clean should move temp contents to trash");
let value: serde_json::Value = common::support::api_data(&output.stdout);
assert_eq!(value["request"]["mode"], "recoverable-delete");
assert_eq!(value["execution_report"]["summary"]["completed_actions"], 1);
assert_eq!(
value["execution_report"]["summary"]["pending_reclaim_bytes"],
5
);
}