From c3cad6b3d8872d5095cf4acd43e4f70733b0e651 Mon Sep 17 00:00:00 2001 From: Huskies Agent Date: Tue, 21 Jul 2026 20:13:43 +0000 Subject: [PATCH] huskies: merge 1244 refactor Deduplicate the worktree module cluster (cleanup/remove/sweep/create/lifecycle share ~60-line clones) --- server/src/agents/pool/worktree_lifecycle.rs | 30 +------ server/src/worktree/cleanup.rs | 72 +---------------- server/src/worktree/create.rs | 64 +-------------- server/src/worktree/git.rs | 30 +------ server/src/worktree/mod.rs | 4 + server/src/worktree/remove.rs | 64 +-------------- server/src/worktree/sweep.rs | 71 +---------------- server/src/worktree/test_support.rs | 84 ++++++++++++++++++++ 8 files changed, 95 insertions(+), 324 deletions(-) create mode 100644 server/src/worktree/test_support.rs diff --git a/server/src/agents/pool/worktree_lifecycle.rs b/server/src/agents/pool/worktree_lifecycle.rs index dd07773a..b5725387 100644 --- a/server/src/agents/pool/worktree_lifecycle.rs +++ b/server/src/agents/pool/worktree_lifecycle.rs @@ -184,39 +184,11 @@ pub(crate) async fn on_terminal_transition(project_root: &Path, story_id: &str) #[cfg(test)] mod tests { use super::*; + use crate::worktree::test_support::init_git_repo; use std::fs; use std::process::Command; use tempfile::TempDir; - fn init_git_repo(dir: &Path) { - use crate::git_test_support::git_ok; - git_ok( - Command::new("git").args(["init"]).current_dir(dir).output(), - "git init", - ); - git_ok( - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(dir) - .output(), - "git config user.email", - ); - git_ok( - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(dir) - .output(), - "git config user.name", - ); - git_ok( - Command::new("git") - .args(["commit", "--allow-empty", "-m", "init"]) - .current_dir(dir) - .output(), - "git commit", - ); - } - fn setup_project(tmp: &TempDir) -> PathBuf { let root = tmp.path().join("project"); fs::create_dir_all(root.join(".huskies")).unwrap(); diff --git a/server/src/worktree/cleanup.rs b/server/src/worktree/cleanup.rs index f0248d40..0b04a982 100644 --- a/server/src/worktree/cleanup.rs +++ b/server/src/worktree/cleanup.rs @@ -169,81 +169,11 @@ pub fn format_report(report: &CleanupReport, confirm: bool) -> String { #[cfg(test)] mod tests { use super::*; - use crate::config::WatcherConfig; - use chrono::Utc; + use crate::worktree::test_support::{done_stage, empty_config, init_git_repo}; use std::fs; use std::path::PathBuf; - use std::process::Command; use tempfile::TempDir; - fn init_git_repo(dir: &std::path::Path) { - use crate::git_test_support::git_ok; - git_ok( - Command::new("git").args(["init"]).current_dir(dir).output(), - "git init", - ); - git_ok( - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(dir) - .output(), - "git config user.email", - ); - git_ok( - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(dir) - .output(), - "git config user.name", - ); - git_ok( - Command::new("git") - .args(["commit", "--allow-empty", "-m", "init"]) - .current_dir(dir) - .output(), - "git commit", - ); - } - - fn empty_config() -> ProjectConfig { - ProjectConfig { - component: vec![], - agent: vec![], - watcher: WatcherConfig::default(), - disk_watch: Default::default(), - default_qa: "server".to_string(), - default_coder_model: None, - max_coders: None, - max_retries: 2, - base_branch: None, - rate_limit_notifications: true, - web_ui_status_consumer: true, - matrix_status_consumer: true, - slack_status_consumer: true, - discord_status_consumer: true, - whatsapp_status_consumer: true, - timezone: None, - rendezvous: None, - trusted_keys: Vec::new(), - crdt_require_token: false, - crdt_tokens: Vec::new(), - max_mesh_peers: 3, - gateway_url: None, - gateway_project: None, - status_push_enabled: true, - merge_failure_block_threshold: 3, - gc_min_free_gb: 0, - snapshot_interval_ops: 5000, - } - } - - fn done_stage() -> Stage { - Stage::Done { - merged_at: Utc::now(), - merge_commit: crate::pipeline_state::GitSha("abc123".to_string()), - } - } - async fn setup_project_with_real_worktree(story_id: &str) -> (TempDir, PathBuf) { let tmp = TempDir::new().unwrap(); let project_root = tmp.path().join("project"); diff --git a/server/src/worktree/create.rs b/server/src/worktree/create.rs index 9ae1e4a9..28a5be6f 100644 --- a/server/src/worktree/create.rs +++ b/server/src/worktree/create.rs @@ -210,72 +210,12 @@ pub(crate) async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), Strin #[cfg(test)] mod tests { use super::*; - use crate::config::{ComponentConfig, WatcherConfig}; + use crate::config::ComponentConfig; + use crate::worktree::test_support::{empty_config, init_git_repo}; use std::fs; use std::process::Command; use tempfile::TempDir; - fn init_git_repo(dir: &Path) { - use crate::git_test_support::git_ok; - git_ok( - Command::new("git").args(["init"]).current_dir(dir).output(), - "git init", - ); - git_ok( - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(dir) - .output(), - "git config user.email", - ); - git_ok( - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(dir) - .output(), - "git config user.name", - ); - git_ok( - Command::new("git") - .args(["commit", "--allow-empty", "-m", "init"]) - .current_dir(dir) - .output(), - "git commit", - ); - } - - fn empty_config() -> ProjectConfig { - ProjectConfig { - component: vec![], - agent: vec![], - watcher: WatcherConfig::default(), - disk_watch: Default::default(), - default_qa: "server".to_string(), - default_coder_model: None, - max_coders: None, - max_retries: 2, - base_branch: None, - rate_limit_notifications: true, - web_ui_status_consumer: true, - matrix_status_consumer: true, - slack_status_consumer: true, - discord_status_consumer: true, - whatsapp_status_consumer: true, - timezone: None, - rendezvous: None, - trusted_keys: Vec::new(), - crdt_require_token: false, - crdt_tokens: Vec::new(), - max_mesh_peers: 3, - gateway_url: None, - gateway_project: None, - status_push_enabled: true, - merge_failure_block_threshold: 3, - gc_min_free_gb: 0, - snapshot_interval_ops: 5000, - } - } - fn failing_setup_config() -> ProjectConfig { ProjectConfig { component: vec![ComponentConfig { diff --git a/server/src/worktree/git.rs b/server/src/worktree/git.rs index 15ea085c..b347fcdb 100644 --- a/server/src/worktree/git.rs +++ b/server/src/worktree/git.rs @@ -232,38 +232,10 @@ pub fn migrate_slug_paths(project_root: &Path, migrations: &[(String, String)]) #[cfg(test)] mod tests { use super::*; + use crate::worktree::test_support::init_git_repo; use std::fs; use tempfile::TempDir; - fn init_git_repo(dir: &Path) { - use crate::git_test_support::git_ok; - git_ok( - Command::new("git").args(["init"]).current_dir(dir).output(), - "git init", - ); - git_ok( - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(dir) - .output(), - "git config user.email", - ); - git_ok( - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(dir) - .output(), - "git config user.name", - ); - git_ok( - Command::new("git") - .args(["commit", "--allow-empty", "-m", "init"]) - .current_dir(dir) - .output(), - "git commit", - ); - } - #[test] fn branch_name_format() { assert_eq!(branch_name("42_my_story"), "feature/story-42_my_story"); diff --git a/server/src/worktree/mod.rs b/server/src/worktree/mod.rs index d5a5bc12..e319499a 100644 --- a/server/src/worktree/mod.rs +++ b/server/src/worktree/mod.rs @@ -6,6 +6,10 @@ mod create; mod git; mod remove; mod sweep; +/// Shared test fixtures (`init_git_repo`, `empty_config`) reused across the +/// worktree module's test suites and the pool worktree-lifecycle tests. +#[cfg(test)] +pub(crate) mod test_support; pub use cleanup::{format_report, run_cleanup}; pub use create::create_worktree; diff --git a/server/src/worktree/remove.rs b/server/src/worktree/remove.rs index 3cc01489..56599928 100644 --- a/server/src/worktree/remove.rs +++ b/server/src/worktree/remove.rs @@ -68,72 +68,10 @@ pub async fn remove_worktree_by_story_id( #[cfg(test)] mod tests { use super::*; - use crate::config::WatcherConfig; + use crate::worktree::test_support::{empty_config, init_git_repo}; use std::fs; - use std::process::Command; use tempfile::TempDir; - fn init_git_repo(dir: &std::path::Path) { - use crate::git_test_support::git_ok; - git_ok( - Command::new("git").args(["init"]).current_dir(dir).output(), - "git init", - ); - git_ok( - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(dir) - .output(), - "git config user.email", - ); - git_ok( - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(dir) - .output(), - "git config user.name", - ); - git_ok( - Command::new("git") - .args(["commit", "--allow-empty", "-m", "init"]) - .current_dir(dir) - .output(), - "git commit", - ); - } - - fn empty_config() -> ProjectConfig { - ProjectConfig { - component: vec![], - agent: vec![], - watcher: WatcherConfig::default(), - disk_watch: Default::default(), - default_qa: "server".to_string(), - default_coder_model: None, - max_coders: None, - max_retries: 2, - base_branch: None, - rate_limit_notifications: true, - web_ui_status_consumer: true, - matrix_status_consumer: true, - slack_status_consumer: true, - discord_status_consumer: true, - whatsapp_status_consumer: true, - timezone: None, - rendezvous: None, - trusted_keys: Vec::new(), - crdt_require_token: false, - crdt_tokens: Vec::new(), - max_mesh_peers: 3, - gateway_url: None, - gateway_project: None, - status_push_enabled: true, - merge_failure_block_threshold: 3, - gc_min_free_gb: 0, - snapshot_interval_ops: 5000, - } - } - #[tokio::test] async fn remove_worktree_by_story_id_returns_err_when_not_found() { let tmp = TempDir::new().unwrap(); diff --git a/server/src/worktree/sweep.rs b/server/src/worktree/sweep.rs index 31397695..e5c7a113 100644 --- a/server/src/worktree/sweep.rs +++ b/server/src/worktree/sweep.rs @@ -89,82 +89,13 @@ where #[cfg(test)] mod tests { use super::*; - use crate::config::WatcherConfig; + use crate::worktree::test_support::{done_stage, empty_config, init_git_repo}; use chrono::Utc; use std::fs; use std::num::NonZeroU32; use std::path::PathBuf; - use std::process::Command; use tempfile::TempDir; - fn init_git_repo(dir: &Path) { - use crate::git_test_support::git_ok; - git_ok( - Command::new("git").args(["init"]).current_dir(dir).output(), - "git init", - ); - git_ok( - Command::new("git") - .args(["config", "user.email", "test@test.com"]) - .current_dir(dir) - .output(), - "git config user.email", - ); - git_ok( - Command::new("git") - .args(["config", "user.name", "Test"]) - .current_dir(dir) - .output(), - "git config user.name", - ); - git_ok( - Command::new("git") - .args(["commit", "--allow-empty", "-m", "init"]) - .current_dir(dir) - .output(), - "git commit", - ); - } - - fn empty_config() -> ProjectConfig { - ProjectConfig { - component: vec![], - agent: vec![], - watcher: WatcherConfig::default(), - disk_watch: Default::default(), - default_qa: "server".to_string(), - default_coder_model: None, - max_coders: None, - max_retries: 2, - base_branch: None, - rate_limit_notifications: true, - web_ui_status_consumer: true, - matrix_status_consumer: true, - slack_status_consumer: true, - discord_status_consumer: true, - whatsapp_status_consumer: true, - timezone: None, - rendezvous: None, - trusted_keys: Vec::new(), - crdt_require_token: false, - crdt_tokens: Vec::new(), - max_mesh_peers: 3, - gateway_url: None, - gateway_project: None, - status_push_enabled: true, - merge_failure_block_threshold: 3, - gc_min_free_gb: 0, - snapshot_interval_ops: 5000, - } - } - - fn done_stage() -> Stage { - Stage::Done { - merged_at: Utc::now(), - merge_commit: crate::pipeline_state::GitSha("abc123".to_string()), - } - } - fn archived_stage() -> Stage { Stage::Archived { archived_at: Utc::now(), diff --git a/server/src/worktree/test_support.rs b/server/src/worktree/test_support.rs new file mode 100644 index 00000000..e76bb5dd --- /dev/null +++ b/server/src/worktree/test_support.rs @@ -0,0 +1,84 @@ +//! Shared test fixtures for worktree module tests: a throwaway git repo and a +//! minimal [`ProjectConfig`] with no components, used by the worktree +//! create/remove/cleanup/sweep tests and the pool worktree-lifecycle +//! subscriber tests so each doesn't hand-roll its own copy. + +use crate::config::{ProjectConfig, WatcherConfig}; +use crate::git_test_support::git_ok; +use crate::pipeline_state::{GitSha, Stage}; +use chrono::Utc; +use std::path::Path; +use std::process::Command; + +/// Initialize a git repo at `dir` with test identity configured and a +/// committed empty initial commit, so worktree creation has a valid base +/// branch to fork from. +pub(crate) fn init_git_repo(dir: &Path) { + git_ok( + Command::new("git").args(["init"]).current_dir(dir).output(), + "git init", + ); + git_ok( + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(dir) + .output(), + "git config user.email", + ); + git_ok( + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(dir) + .output(), + "git config user.name", + ); + git_ok( + Command::new("git") + .args(["commit", "--allow-empty", "-m", "init"]) + .current_dir(dir) + .output(), + "git commit", + ); +} + +/// A minimal [`ProjectConfig`] with no components, suitable for worktree +/// tests that don't exercise setup/teardown commands. +pub(crate) fn empty_config() -> ProjectConfig { + ProjectConfig { + component: vec![], + agent: vec![], + watcher: WatcherConfig::default(), + disk_watch: Default::default(), + default_qa: "server".to_string(), + default_coder_model: None, + max_coders: None, + max_retries: 2, + base_branch: None, + rate_limit_notifications: true, + web_ui_status_consumer: true, + matrix_status_consumer: true, + slack_status_consumer: true, + discord_status_consumer: true, + whatsapp_status_consumer: true, + timezone: None, + rendezvous: None, + trusted_keys: Vec::new(), + crdt_require_token: false, + crdt_tokens: Vec::new(), + max_mesh_peers: 3, + gateway_url: None, + gateway_project: None, + status_push_enabled: true, + merge_failure_block_threshold: 3, + gc_min_free_gb: 0, + snapshot_interval_ops: 5000, + } +} + +/// A `Stage::Done` fixture for tests that need a terminal-stage story. +pub(crate) fn done_stage() -> Stage { + Stage::Done { + merged_at: Utc::now(), + merge_commit: GitSha("abc123".to_string()), + } +}