rebecca 0.3.0

Cross-platform cleanup CLI and Rust library surface for Rebecca cleanup planning, rules, and platform adapters.
Documentation
use std::fs;

mod common;
use rebecca::core::history::HistoryStore;
use rebecca::core::plan::{CleanupPlan, CleanupTarget};
use rebecca::core::{DeleteMode, PlanRequest, Platform};

#[test]
fn config_paths_json_is_parseable() {
    let temp = tempfile::tempdir().unwrap();
    let output = common::isolated::isolated_rebecca(&temp)
        .args(["config", "paths", "--format", "json"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    let value: serde_json::Value = common::support::api_data(&output.stdout);

    assert!(
        value["config_file"]
            .as_str()
            .unwrap()
            .contains("rebecca-config")
    );
    assert!(
        value["history_file"]
            .as_str()
            .unwrap()
            .contains("history.jsonl")
    );
    let storage = value["storage"].as_array().unwrap();
    assert_eq!(storage.len(), 5);
    assert_eq!(
        storage
            .iter()
            .find(|entry| entry["id"].as_str().unwrap() == "cache-dir")
            .unwrap()["lifecycle"]
            .as_str()
            .unwrap(),
        "rebuildable-cache"
    );
    assert_eq!(
        storage
            .iter()
            .find(|entry| entry["id"].as_str().unwrap() == "history-file")
            .unwrap()["retention"]
            .as_str()
            .unwrap(),
        "preserve"
    );
}

#[test]
fn config_paths_json_respects_config_file_overrides() {
    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

[app_paths]
state_dir = "C:\\Rebecca\\State"
cache_dir = "C:\\Rebecca\\Cache"
history_file = "C:\\Rebecca\\State\\audit.jsonl"
"#,
    )
    .unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .env_remove("REBECCA_STATE_DIR")
        .env_remove("REBECCA_CACHE_DIR")
        .env_remove("REBECCA_HISTORY_FILE")
        .args(["config", "paths", "--format", "json"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );

    let value: serde_json::Value = common::support::api_data(&output.stdout);
    assert!(
        value["state_dir"]
            .as_str()
            .unwrap()
            .ends_with("Rebecca/State")
    );
    assert!(
        value["cache_dir"]
            .as_str()
            .unwrap()
            .ends_with("Rebecca/Cache")
    );
    assert!(
        value["history_file"]
            .as_str()
            .unwrap()
            .ends_with("Rebecca/State/audit.jsonl")
    );
    assert!(!value["state_dir"].as_str().unwrap().contains('\\'));
    assert!(!value["cache_dir"].as_str().unwrap().contains('\\'));
    assert!(!value["history_file"].as_str().unwrap().contains('\\'));
}

#[test]
fn config_paths_reports_malformed_config_file() {
    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"), "[app_paths\n").unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .args(["config", "paths"])
        .output()
        .unwrap();

    assert!(!output.status.success());

    let stderr = common::support::stderr(&output);
    assert!(stderr.contains("config parse failed"));
    assert!(stderr.contains("config.toml"));
}

#[test]
fn config_paths_reports_unsupported_config_version() {
    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"), "version = 2\n").unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .args(["config", "paths"])
        .output()
        .unwrap();

    assert!(!output.status.success());

    let stderr = common::support::stderr(&output);
    assert!(stderr.contains("config parse failed"));
    assert!(stderr.contains("unsupported config version 2"));
    assert!(stderr.contains("config.toml"));
}

#[test]
fn history_uses_config_file_overrides() {
    let temp = tempfile::tempdir().unwrap();
    let config_dir = temp.path().join("rebecca-config");
    let history_path = temp.path().join("custom-state").join("audit.jsonl");
    fs::create_dir_all(&config_dir).unwrap();
    if let Some(parent) = history_path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(
        config_dir.join("config.toml"),
        format!(
            r#"
version = 1

[app_paths]
history_file = '{}'
"#,
            history_path.display()
        ),
    )
    .unwrap();

    let mut plan = CleanupPlan::empty(PlanRequest::for_platform(
        Platform::Windows,
        DeleteMode::RecoverableDelete,
    ));
    let mut target = CleanupTarget::allowed(
        "windows.user-temp",
        std::path::PathBuf::from(r"C:\Temp\file.tmp"),
        10,
        DeleteMode::RecoverableDelete,
    );
    target.mark_completed(0, 10, None);
    plan.targets.push(target);
    plan.recompute_summary();
    HistoryStore::new(history_path.clone())
        .append_plan(&plan)
        .unwrap();

    let output = common::isolated::isolated_rebecca(&temp)
        .env_remove("REBECCA_HISTORY_FILE")
        .args(["history", "--format", "json"])
        .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.as_array().unwrap().len(), 1);
}

#[test]
fn doctor_permissions_prints_permission_label() {
    let output = common::command::rebecca()
        .args(["doctor", "permissions"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Privilege level:"));
    assert!(stdout.contains("Suggested action:"));
    if cfg!(target_os = "macos") {
        assert!(stdout.contains("macOS privacy:"));
        assert!(stdout.contains("macOS privacy action kind:"));
        assert!(stdout.contains("macOS privacy action:"));
    }
}

#[test]
fn doctor_help_omits_steam_command() {
    let output = common::command::rebecca()
        .args(["doctor", "--help"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        common::support::stderr(&output)
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("permissions"));
    assert!(!stdout.contains("steam"));
}