stagecrew 0.4.0

Disk usage management for shared or staging filesystems with automatic cleanup policies
Documentation
//! Integration tests for the `stagecrew add` CLI command.
//!
//! These tests verify the behavior of adding tracked paths via the CLI,
//! including path validation, duplicate detection, config persistence,
//! and optional initial scanning.

use std::fs;

use tempfile::TempDir;

use stagecrew::config::{AppPaths, Config};
use stagecrew::db::Database;

/// Helper to create a mock `AppPaths` pointing to a temp config directory.
fn mock_app_paths(temp_dir: &TempDir) -> AppPaths {
    // Allow: Environment variables are inherently unsafe. This is controlled test code
    // and the variable is only set for the duration of the test with a temp directory.
    // Using set_var in tests is necessary to override XDG directories.
    #[allow(clippy::undocumented_unsafe_blocks)]
    unsafe {
        std::env::set_var("XDG_CONFIG_HOME", temp_dir.path());
    }
    AppPaths::new()
}

#[tokio::test]
async fn add_path_to_empty_config() {
    let temp_dir = TempDir::with_prefix("stagecrew-add-test-")
        .expect("failed to create temp directory - check disk space");

    let tracked_path = temp_dir.path().join("staging");
    fs::create_dir_all(&tracked_path)
        .expect("failed to create staging directory - check write permissions");

    let paths = mock_app_paths(&temp_dir);

    // Simulate handle_add by directly manipulating config
    // (we can't call handle_add since it's private to main.rs)
    let canonical_path = tracked_path
        .canonicalize()
        .expect("failed to canonicalize path");

    let mut config = Config::default();
    config.tracked_paths.push(canonical_path.clone());
    config
        .save(&paths)
        .expect("failed to save config - check write permissions");

    // Verify config file was created and contains the path
    let loaded_config = Config::load(&paths).expect("failed to load saved config");
    assert_eq!(loaded_config.tracked_paths.len(), 1);
    assert_eq!(loaded_config.tracked_paths[0], canonical_path);
}

#[tokio::test]
async fn add_path_to_existing_config() {
    let temp_dir = TempDir::with_prefix("stagecrew-add-test-")
        .expect("failed to create temp directory - check disk space");

    let first_path = temp_dir.path().join("staging1");
    let second_path = temp_dir.path().join("staging2");
    fs::create_dir_all(&first_path)
        .expect("failed to create first staging directory - check write permissions");
    fs::create_dir_all(&second_path)
        .expect("failed to create second staging directory - check write permissions");

    let paths = mock_app_paths(&temp_dir);

    // Create initial config with one path
    let canonical_first = first_path
        .canonicalize()
        .expect("failed to canonicalize first path");
    let mut config = Config::default();
    config.tracked_paths = vec![canonical_first.clone()];
    config
        .save(&paths)
        .expect("failed to save initial config - check write permissions");

    // Add second path
    let canonical_second = second_path
        .canonicalize()
        .expect("failed to canonicalize second path");
    let mut loaded_config = Config::load(&paths).expect("failed to load existing config");
    loaded_config.tracked_paths.push(canonical_second.clone());
    loaded_config
        .save(&paths)
        .expect("failed to save updated config - check write permissions");

    // Verify both paths are present
    let final_config = Config::load(&paths).expect("failed to load final config");
    assert_eq!(final_config.tracked_paths.len(), 2);
    assert!(final_config.tracked_paths.contains(&canonical_first));
    assert!(final_config.tracked_paths.contains(&canonical_second));
}

#[tokio::test]
async fn add_duplicate_path_is_idempotent() {
    let temp_dir = TempDir::with_prefix("stagecrew-add-test-")
        .expect("failed to create temp directory - check disk space");

    let tracked_path = temp_dir.path().join("staging");
    fs::create_dir_all(&tracked_path)
        .expect("failed to create staging directory - check write permissions");

    let paths = mock_app_paths(&temp_dir);

    let canonical_path = tracked_path
        .canonicalize()
        .expect("failed to canonicalize path");

    // Add path once
    let mut config = Config::default();
    config.tracked_paths.push(canonical_path.clone());
    config
        .save(&paths)
        .expect("failed to save config - check write permissions");

    // Attempt to add the same path again
    let mut loaded_config = Config::load(&paths).expect("failed to load existing config");
    if !loaded_config.tracked_paths.contains(&canonical_path) {
        loaded_config.tracked_paths.push(canonical_path.clone());
    }
    loaded_config
        .save(&paths)
        .expect("failed to save updated config - check write permissions");

    // Verify only one entry exists
    let final_config = Config::load(&paths).expect("failed to load final config");
    assert_eq!(final_config.tracked_paths.len(), 1);
    assert_eq!(final_config.tracked_paths[0], canonical_path);
}

#[tokio::test]
async fn add_different_representations_of_same_path() {
    let temp_dir = TempDir::with_prefix("stagecrew-add-test-")
        .expect("failed to create temp directory - check disk space");

    let staging = temp_dir.path().join("staging");
    fs::create_dir_all(&staging)
        .expect("failed to create staging directory - check write permissions");

    // Create a path with . in it (e.g., /tmp/staging/./subdir)
    let subdir = staging.join("subdir");
    fs::create_dir_all(&subdir).expect("failed to create subdir - check write permissions");

    // Two representations of the same path
    let path1 = staging.join("subdir");
    let path2 = staging.join(".").join("subdir");

    let canonical1 = path1.canonicalize().expect("failed to canonicalize path1");
    let canonical2 = path2.canonicalize().expect("failed to canonicalize path2");

    // Canonicalization should make them identical
    assert_eq!(canonical1, canonical2);

    let paths = mock_app_paths(&temp_dir);

    // Add first representation
    let mut config = Config::default();
    config.tracked_paths.push(canonical1.clone());
    config
        .save(&paths)
        .expect("failed to save config - check write permissions");

    // Attempt to add second representation
    let mut loaded_config = Config::load(&paths).expect("failed to load existing config");
    if !loaded_config.tracked_paths.contains(&canonical2) {
        loaded_config.tracked_paths.push(canonical2);
    }
    loaded_config
        .save(&paths)
        .expect("failed to save updated config - check write permissions");

    // Verify only one entry exists
    let final_config = Config::load(&paths).expect("failed to load final config");
    assert_eq!(
        final_config.tracked_paths.len(),
        1,
        "duplicate paths with different representations should be deduplicated"
    );
}

#[tokio::test]
async fn add_with_scan_flag() {
    let temp_dir = TempDir::with_prefix("stagecrew-add-test-")
        .expect("failed to create temp directory - check disk space");

    let tracked_path = temp_dir.path().join("staging");
    fs::create_dir_all(&tracked_path)
        .expect("failed to create staging directory - check write permissions");

    // Create some files in the tracked path
    let file1 = tracked_path.join("file1.txt");
    let file2 = tracked_path.join("file2.txt");
    fs::write(&file1, b"test content 1").expect("failed to write file1");
    fs::write(&file2, b"test content 2").expect("failed to write file2");

    let paths = mock_app_paths(&temp_dir);

    let canonical_path = tracked_path
        .canonicalize()
        .expect("failed to canonicalize path");

    // Simulate handle_add with run_scan=true
    let mut config = Config::default();
    config.tracked_paths = vec![canonical_path.clone()];
    config
        .save(&paths)
        .expect("failed to save config - check write permissions");

    let db_path = paths
        .database_file(&config)
        .expect("failed to get database path");
    let db = Database::open(&db_path).expect("failed to open database");

    let scanner = stagecrew::scanner::Scanner::new();
    let app_config = stagecrew::config::AppConfig::from_global(config);
    let summary = stagecrew::scanner::scan_and_persist(&db, &scanner, &app_config)
        .await
        .expect("failed to scan and persist");

    // Verify scan results - now counts entries (both directory and file entries)
    // The root directory itself is an entry, plus 2 file entries = 3 total
    // But total_files in ScanSummary counts actual files found during walk
    assert_eq!(summary.total_files, 2);
    assert!(summary.total_size_bytes > 0);

    // Verify database contains the scanned data
    let roots = db.list_roots().expect("failed to list roots");
    assert_eq!(roots.len(), 1);
    assert_eq!(roots[0].path, canonical_path);

    // List entries under the root - should have 2 file entries
    let entries = db
        .list_entries_by_parent(roots[0].id, &canonical_path)
        .expect("failed to list entries");
    assert_eq!(entries.len(), 2, "should have 2 file entries under root");

    // Verify entries are files, not directories
    for entry in &entries {
        assert!(!entry.is_dir, "entries should be files");
    }
}