From 2b8ccebafa9b6c4aa95bfd5b8e4dec03767b81a5 Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 11 Apr 2026 11:40:01 +0900 Subject: [PATCH] Improve worktree command regression tests --- tests/custom_path_behavior_test.rs | 70 +++++++++++++ tests/unit/commands/delete.rs | 113 ++++++++++++++------- tests/unit/commands/mod.rs | 74 +++++++++----- tests/unit/commands/rename.rs | 153 ++++++++++++++++++++++++++--- tests/unit/commands/switch.rs | 123 ++++++++++++++++------- tests/unit/core/utils.rs | 12 --- 6 files changed, 420 insertions(+), 125 deletions(-) diff --git a/tests/custom_path_behavior_test.rs b/tests/custom_path_behavior_test.rs index a3c3391..bc7fa8a 100644 --- a/tests/custom_path_behavior_test.rs +++ b/tests/custom_path_behavior_test.rs @@ -7,6 +7,8 @@ use anyhow::Result; use git_workers::commands::create_worktree_with_ui; +use std::fs; +use std::process::Command; use tempfile::TempDir; mod common; @@ -270,6 +272,74 @@ fn test_custom_path_with_branch_selection() -> Result<()> { Ok(()) } +/// Test that tag selection creates a new branch named after the worktree +#[test] +fn test_custom_path_with_tag_selection() -> Result<()> { + let temp_dir = TempDir::new()?; + let test_repo = TestRepo::new(&temp_dir)?; + let manager = test_repo.manager()?; + + Command::new("git") + .args(["tag", "v1.2.3"]) + .current_dir(test_repo.path()) + .output()?; + + let ui = TestUI::new() + .with_input("release-preview") // worktree name + .with_selection(1) // custom path option + .with_input("releases/") // directory for tagged releases + .with_selection(2) // select tag + .with_selection(0) // select the first tag + .with_confirmation(false); // don't switch + + let result = create_worktree_with_ui(&manager, &ui)?; + assert!(!result); + + let worktrees = manager.list_worktrees()?; + let worktree = worktrees + .iter() + .find(|w| w.name == "release-preview") + .expect("release-preview worktree should exist"); + + assert!(worktree.path.ends_with("releases/release-preview")); + assert_eq!(worktree.branch, "release-preview"); + + Ok(()) +} + +/// Test that opting into switch writes the selected path for shell integration +#[test] +fn test_create_worktree_with_switch_writes_switch_file() -> Result<()> { + let temp_dir = TempDir::new()?; + let test_repo = TestRepo::new(&temp_dir)?; + let manager = test_repo.manager()?; + + let switch_file = temp_dir.path().join("switch-target.txt"); + std::env::set_var("GW_SWITCH_FILE", &switch_file); + + let ui = TestUI::new() + .with_input("switch-target") // worktree name + .with_selection(0) // same level as repository + .with_selection(0) // create from HEAD + .with_confirmation(true); // switch to new worktree + + let result = create_worktree_with_ui(&manager, &ui)?; + std::env::remove_var("GW_SWITCH_FILE"); + + assert!(result); + + let worktrees = manager.list_worktrees()?; + let worktree = worktrees + .iter() + .find(|w| w.name == "switch-target") + .expect("switch-target worktree should exist"); + + let switch_target = fs::read_to_string(&switch_file)?; + assert_eq!(switch_target.trim(), worktree.path.to_string_lossy()); + + Ok(()) +} + /// Test UI examples match actual behavior #[test] fn test_ui_examples_are_accurate() -> Result<()> { diff --git a/tests/unit/commands/delete.rs b/tests/unit/commands/delete.rs index 9b48a45..1843d20 100644 --- a/tests/unit/commands/delete.rs +++ b/tests/unit/commands/delete.rs @@ -4,12 +4,59 @@ //! including removal confirmation and cleanup operations. use anyhow::Result; -use git_workers::commands::WorktreeDeleteConfig; +use git_workers::commands::{delete_worktree_with_ui, execute_deletion, WorktreeDeleteConfig}; +use git_workers::git::GitWorktreeManager; use git_workers::infrastructure::git::WorktreeInfo; +use git_workers::ui::MockUI; use std::fs; use std::path::PathBuf; use tempfile::TempDir; +fn setup_test_repo() -> Result<(TempDir, GitWorktreeManager)> { + let temp_dir = TempDir::new()?; + + std::process::Command::new("git") + .arg("init") + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.email", "test@example.com"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.name", "Test User"]) + .current_dir(temp_dir.path()) + .output()?; + + fs::write(temp_dir.path().join("README.md"), "# Test")?; + std::process::Command::new("git") + .arg("add") + .arg("README.md") + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["commit", "-m", "Initial commit"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["branch", "-m", "main"]) + .current_dir(temp_dir.path()) + .output()?; + + let manager = GitWorktreeManager::new_from_path(temp_dir.path())?; + Ok((temp_dir, manager)) +} + +fn unique_name(prefix: &str) -> String { + format!( + "{prefix}-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ) +} + #[test] fn test_worktree_delete_config_creation() { let config = WorktreeDeleteConfig { @@ -27,47 +74,39 @@ fn test_worktree_delete_config_creation() { // Integration test for actual deletion #[test] -fn test_worktree_deletion_simulation() -> Result<()> { - let temp_dir = TempDir::new()?; +fn test_execute_deletion_removes_worktree_and_branch() -> Result<()> { + let (_temp_dir, manager) = setup_test_repo()?; + let name = unique_name("delete-me"); + let worktree_path = manager.create_worktree_with_new_branch(&name, &name, "main")?; - // Initialize repository - std::process::Command::new("git") - .arg("init") - .current_dir(temp_dir.path()) - .output()?; + let config = WorktreeDeleteConfig { + name: name.clone(), + path: worktree_path.clone(), + branch: name.clone(), + delete_branch: true, + }; - // Create initial commit - fs::write(temp_dir.path().join("README.md"), "# Test")?; - std::process::Command::new("git") - .arg("add") - .arg(".") - .current_dir(temp_dir.path()) - .output()?; - std::process::Command::new("git") - .arg("commit") - .arg("-m") - .arg("Initial commit") - .current_dir(temp_dir.path()) - .output()?; + execute_deletion(&config, &manager)?; - // Create a worktree - let worktree_path = temp_dir.path().join("../feature"); - std::process::Command::new("git") - .args([ - "worktree", - "add", - worktree_path.to_str().unwrap(), - "-b", - "feature", - ]) - .current_dir(temp_dir.path()) - .output()?; + assert!(!worktree_path.exists()); + + let (local_branches, _) = manager.list_all_branches()?; + assert!(!local_branches.contains(&name)); + + Ok(()) +} + +#[test] +fn test_delete_worktree_with_ui_cancel_keeps_worktree() -> Result<()> { + let (_temp_dir, manager) = setup_test_repo()?; + let name = unique_name("delete-cancel"); + manager.create_worktree_with_new_branch(&name, &name, "main")?; - // Verify worktree exists - assert!(worktree_path.exists()); + let ui = MockUI::new().with_selection(0); + delete_worktree_with_ui(&manager, &ui)?; - // Test would validate deletion target if API was public - // assert!(validate_deletion_target("feature").is_ok()); + let worktrees = manager.list_worktrees()?; + assert!(worktrees.iter().any(|w| w.name == name)); Ok(()) } diff --git a/tests/unit/commands/mod.rs b/tests/unit/commands/mod.rs index c4f7804..d8501c3 100644 --- a/tests/unit/commands/mod.rs +++ b/tests/unit/commands/mod.rs @@ -13,7 +13,9 @@ use anyhow::Result; use git_workers::commands::{find_config_file_path, get_worktree_icon, validate_custom_path}; use git_workers::constants; use git_workers::infrastructure::git::{GitWorktreeManager, WorktreeInfo}; +use serial_test::serial; use std::fs; +use std::path::{Path, PathBuf}; use tempfile::TempDir; // ============================================================================ @@ -45,6 +47,15 @@ fn setup_non_bare_repo() -> Result<(TempDir, GitWorktreeManager)> { .current_dir(temp_dir.path()) .output()?; + std::process::Command::new("git") + .args(["config", "user.email", "test@example.com"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.name", "Test User"]) + .current_dir(temp_dir.path()) + .output()?; + // Create initial commit fs::write(temp_dir.path().join("README.md"), "# Test")?; std::process::Command::new("git") @@ -63,6 +74,24 @@ fn setup_non_bare_repo() -> Result<(TempDir, GitWorktreeManager)> { Ok((temp_dir, manager)) } +struct CurrentDirGuard { + original: PathBuf, +} + +impl CurrentDirGuard { + fn change_to(path: &Path) -> Result { + let original = std::env::current_dir()?; + std::env::set_current_dir(path)?; + Ok(Self { original }) + } +} + +impl Drop for CurrentDirGuard { + fn drop(&mut self) { + let _ = std::env::set_current_dir(&self.original); + } +} + // ============================================================================ // Icon and Display Tests // ============================================================================ @@ -149,41 +178,34 @@ fn test_get_worktree_icon_locked() -> Result<()> { // ============================================================================ #[test] -#[ignore = "Test requires isolated environment to avoid finding project config"] -fn test_find_config_file_path_in_bare_repo() -> Result<()> { +#[serial] +fn test_find_config_file_path_defaults_to_current_bare_repo_directory() -> Result<()> { let (temp_dir, manager) = setup_test_repo()?; - - // Create main worktree - let main_path = temp_dir.path().join("main"); - fs::create_dir_all(&main_path)?; - - // Create config file in main worktree - let config_path = main_path.join(".git-workers.toml"); - fs::write(&config_path, "[worktree]\npattern = \"subdirectory\"")?; - - // Update git config to point to main worktree - std::process::Command::new("git") - .args(["config", "core.worktree", main_path.to_str().unwrap()]) - .current_dir(temp_dir.path()) - .output()?; + let _guard = CurrentDirGuard::change_to(temp_dir.path())?; let found_path = find_config_file_path(&manager)?; - assert_eq!(found_path, config_path); + assert_eq!( + found_path, + std::env::current_dir()?.join(".git-workers.toml") + ); Ok(()) } #[test] -#[ignore = "Test requires isolated environment to avoid finding project config"] -fn test_find_config_file_path_in_worktree() -> Result<()> { +#[serial] +fn test_find_config_file_path_prefers_current_worktree_directory() -> Result<()> { let (temp_dir, _manager) = setup_non_bare_repo()?; - // Create config file in repository root - let config_path = temp_dir.path().join(".git-workers.toml"); - fs::write(&config_path, "[worktree]\npattern = \"same-level\"")?; + fs::write( + temp_dir.path().join(".git-workers.toml"), + "[worktree]\npattern = \"same-level\"", + )?; - // Create a worktree - let worktree_path = temp_dir.path().join("../feature"); + let worktree_path = temp_dir.path().parent().unwrap().join(format!( + "{}-feature", + temp_dir.path().file_name().unwrap().to_string_lossy() + )); std::process::Command::new("git") .args([ "worktree", @@ -195,9 +217,11 @@ fn test_find_config_file_path_in_worktree() -> Result<()> { .current_dir(temp_dir.path()) .output()?; + let worktree_path = worktree_path.canonicalize()?; + let _guard = CurrentDirGuard::change_to(&worktree_path)?; let manager = GitWorktreeManager::new_from_path(&worktree_path)?; let found_path = find_config_file_path(&manager)?; - assert_eq!(found_path, config_path); + assert_eq!(found_path, worktree_path.join(".git-workers.toml")); Ok(()) } diff --git a/tests/unit/commands/rename.rs b/tests/unit/commands/rename.rs index 2f81c40..ecb45a1 100644 --- a/tests/unit/commands/rename.rs +++ b/tests/unit/commands/rename.rs @@ -3,8 +3,60 @@ //! This module tests the business logic for worktree renaming, //! including validation and path handling. -use git_workers::commands::WorktreeRenameConfig; +use anyhow::Result; +use git_workers::commands::{ + execute_rename, rename_worktree_with_ui, validate_rename_operation, WorktreeRenameConfig, +}; +use git_workers::git::GitWorktreeManager; +use git_workers::ui::MockUI; +use std::fs; use std::path::PathBuf; +use tempfile::TempDir; + +fn setup_test_repo() -> Result<(TempDir, GitWorktreeManager)> { + let temp_dir = TempDir::new()?; + + std::process::Command::new("git") + .arg("init") + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.email", "test@example.com"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.name", "Test User"]) + .current_dir(temp_dir.path()) + .output()?; + + fs::write(temp_dir.path().join("README.md"), "# Test")?; + std::process::Command::new("git") + .arg("add") + .arg("README.md") + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["commit", "-m", "Initial commit"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["branch", "-m", "main"]) + .current_dir(temp_dir.path()) + .output()?; + + let manager = GitWorktreeManager::new_from_path(temp_dir.path())?; + Ok((temp_dir, manager)) +} + +fn unique_name(prefix: &str) -> String { + format!( + "{prefix}-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ) +} #[test] fn test_worktree_rename_config() { @@ -28,23 +80,94 @@ fn test_worktree_rename_config() { } #[test] -#[allow(clippy::const_is_empty)] -fn test_rename_validation_basic() { - // Basic string validation tests - assert!(!"old-name".is_empty()); - assert!(!"new-name".is_empty()); - assert!("old-name" != "new-name"); +fn test_validate_rename_operation_rejects_reserved_and_equal_names() { + assert!(validate_rename_operation("feature", "feature").is_err()); + assert!(validate_rename_operation("main", "other").is_err()); + assert!(validate_rename_operation("feature", "master").is_err()); } #[test] -fn test_path_generation_for_rename() { - let base_path = PathBuf::from("/tmp/worktrees"); - let old_name = "feature"; - let new_name = "feature-v2"; +fn test_execute_rename_updates_worktree_path_without_branch_rename() -> Result<()> { + let (_temp_dir, manager) = setup_test_repo()?; + let old_name = unique_name("rename-me"); + let new_name = unique_name("renamed-worktree"); + let old_path = manager.create_worktree_with_new_branch( + &old_name, + &format!("feature/{old_name}"), + "main", + )?; + let new_path = old_path.parent().unwrap().join(&new_name); + + let config = WorktreeRenameConfig { + old_name: old_name.clone(), + new_name: new_name.clone(), + old_path: old_path.clone(), + new_path: new_path.clone(), + old_branch: format!("feature/{old_name}"), + new_branch: None, + rename_branch: false, + }; + + execute_rename(&config, &manager)?; + + let worktrees = manager.list_worktrees()?; + let renamed = worktrees + .iter() + .find(|w| w.name == new_name) + .expect("renamed worktree should exist"); + + assert_eq!(renamed.git_name, old_name); + assert_eq!(renamed.branch, format!("feature/{}", renamed.git_name)); + assert_eq!(renamed.path, new_path.canonicalize()?); + + Ok(()) +} + +#[test] +fn test_execute_rename_can_rename_branch_too() -> Result<()> { + let (_temp_dir, manager) = setup_test_repo()?; + let old_name = unique_name("branch-sync"); + let new_name = unique_name("branch-sync-renamed"); + let old_path = manager.create_worktree_with_new_branch(&old_name, &old_name, "main")?; + let new_path = old_path.parent().unwrap().join(&new_name); + + let config = WorktreeRenameConfig { + old_name: old_name.clone(), + new_name: new_name.clone(), + old_path: old_path.clone(), + new_path, + old_branch: old_name.clone(), + new_branch: Some(new_name.clone()), + rename_branch: true, + }; + + execute_rename(&config, &manager)?; + + let (local_branches, _) = manager.list_all_branches()?; + assert!(local_branches.contains(&new_name)); + assert!(!local_branches.contains(&old_name)); + + Ok(()) +} + +#[test] +fn test_rename_worktree_with_ui_cancel_keeps_existing_worktree() -> Result<()> { + let (_temp_dir, manager) = setup_test_repo()?; + let old_name = unique_name("preview-rename"); + let new_name = unique_name("renamed-preview"); + manager.create_worktree_with_new_branch(&old_name, &old_name, "main")?; + + let ui = MockUI::new() + .with_selection(0) + .with_input(&new_name) + .with_confirm(true) + .with_confirm(false); + + rename_worktree_with_ui(&manager, &ui)?; - let old_path = base_path.join(old_name); - let new_path = base_path.join(new_name); + let worktrees = manager.list_worktrees()?; + assert!(worktrees.iter().any(|w| w.name == old_name)); + assert!(!worktrees.iter().any(|w| w.name == new_name)); - assert_eq!(old_path.to_str().unwrap(), "/tmp/worktrees/feature"); - assert_eq!(new_path.to_str().unwrap(), "/tmp/worktrees/feature-v2"); + Ok(()) } diff --git a/tests/unit/commands/switch.rs b/tests/unit/commands/switch.rs index d42d705..51c4f3e 100644 --- a/tests/unit/commands/switch.rs +++ b/tests/unit/commands/switch.rs @@ -3,8 +3,49 @@ //! This module tests the business logic for worktree switching, //! including validation and state management. -use git_workers::commands::WorktreeSwitchConfig; +use anyhow::Result; +use git_workers::commands::{switch_worktree_with_ui, WorktreeSwitchConfig}; +use git_workers::git::GitWorktreeManager; +use git_workers::ui::MockUI; +use serial_test::serial; +use std::fs; use std::path::PathBuf; +use tempfile::TempDir; + +fn setup_test_repo() -> Result<(TempDir, GitWorktreeManager)> { + let temp_dir = TempDir::new()?; + + std::process::Command::new("git") + .arg("init") + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.email", "test@example.com"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["config", "user.name", "Test User"]) + .current_dir(temp_dir.path()) + .output()?; + + fs::write(temp_dir.path().join("README.md"), "# Test")?; + std::process::Command::new("git") + .arg("add") + .arg("README.md") + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["commit", "-m", "Initial commit"]) + .current_dir(temp_dir.path()) + .output()?; + std::process::Command::new("git") + .args(["branch", "-m", "main"]) + .current_dir(temp_dir.path()) + .output()?; + + let manager = GitWorktreeManager::new_from_path(temp_dir.path())?; + Ok((temp_dir, manager)) +} #[test] fn test_worktree_switch_config() { @@ -20,45 +61,55 @@ fn test_worktree_switch_config() { } #[test] -#[allow(clippy::const_is_empty)] -fn test_switch_validation_basic() { - // Basic validation tests - assert!(!"feature".is_empty()); - assert!("".is_empty()); // Empty string validation - assert!("feature branch".contains(' ')); // Whitespace detection - assert!(!"feature".contains(' ')); // Valid name -} +#[serial] +fn test_switch_worktree_with_ui_writes_selected_path() -> Result<()> { + let (temp_dir, manager) = setup_test_repo()?; + let unique = format!( + "feature-switch-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let target_path = manager.create_worktree_with_new_branch(&unique, &unique, "main")?; + let switch_file = temp_dir.path().join("switch-target.txt"); + let worktrees = manager.list_worktrees()?; + let selection = if worktrees.iter().any(|w| w.is_current) { + 1 + } else { + 0 + }; -#[test] -fn test_switch_path_validation() { - let valid_path = PathBuf::from("/tmp/worktrees/feature"); - assert!(valid_path.exists() || !valid_path.exists()); // Path existence check + std::env::set_var("GW_SWITCH_FILE", &switch_file); + + let ui = MockUI::new().with_selection(selection); + let switched = switch_worktree_with_ui(&manager, &ui)?; + + std::env::remove_var("GW_SWITCH_FILE"); - let relative_path = PathBuf::from("../feature"); - assert!(relative_path.is_relative()); + assert!(switched); + let written_path = fs::read_to_string(&switch_file)?; + assert_eq!(written_path.trim(), target_path.to_string_lossy()); - let absolute_path = PathBuf::from("/tmp/feature"); - assert!(absolute_path.is_absolute()); + Ok(()) } #[test] -fn test_switch_worktree_names() { - let valid_names = vec![ - "feature", - "feature-123", - "bugfix", - "release-v1.0.0", - "experiment_new", - ]; - - for name in valid_names { - assert!(!name.is_empty()); - assert!(!name.contains(' ')); - } - - let invalid_names = vec!["", "feature branch", "feature\ttab", "feature\nnewline"]; - - for name in invalid_names { - assert!(name.is_empty() || name.contains(char::is_whitespace)); - } +fn test_switch_worktree_with_ui_cancelled_selection_returns_false() -> Result<()> { + let (_temp_dir, manager) = setup_test_repo()?; + let unique = format!( + "feature-cancel-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + manager.create_worktree_with_new_branch(&unique, &unique, "main")?; + + let ui = MockUI::new(); + let switched = switch_worktree_with_ui(&manager, &ui)?; + + assert!(!switched); + + Ok(()) } diff --git a/tests/unit/core/utils.rs b/tests/unit/core/utils.rs index 832ccdd..52d8881 100644 --- a/tests/unit/core/utils.rs +++ b/tests/unit/core/utils.rs @@ -66,18 +66,6 @@ fn test_path_operations() { assert!(!relative_path.is_absolute()); } -#[test] -fn test_confirm_action() { - // This function requires user input, so we can't test it directly - // We would need to mock the dialoguer library for proper testing -} - -#[test] -fn test_press_any_key_to_continue() { - // This function waits for user input, so we can't test it directly - // We would need to mock stdin for proper testing -} - #[test] fn test_terminal_operations() { // Test that terminal functions exist and can be created