From 9b2969fdba9d9b878ed02a23a50afda03e38554b Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 24 Feb 2026 00:26:49 +0000 Subject: [PATCH] story-kit: merge 128_story_test_coverage_worktree_rs --- server/src/worktree.rs | 226 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/server/src/worktree.rs b/server/src/worktree.rs index 0a28dab..a655f2e 100644 --- a/server/src/worktree.rs +++ b/server/src/worktree.rs @@ -306,6 +306,7 @@ async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; + use crate::config::ComponentConfig; use std::fs; use tempfile::TempDir; @@ -437,4 +438,229 @@ mod tests { ".story_kit/work/ must still exist in the main checkout" ); } + + #[test] + fn branch_name_format() { + assert_eq!(branch_name("42_my_story"), "feature/story-42_my_story"); + assert_eq!(branch_name("1_test"), "feature/story-1_test"); + } + + #[test] + fn detect_base_branch_returns_branch_in_git_repo() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + let branch = detect_base_branch(&project_root); + assert!(!branch.is_empty()); + } + + #[test] + fn detect_base_branch_falls_back_to_master_for_non_git_dir() { + let tmp = TempDir::new().unwrap(); + let branch = detect_base_branch(tmp.path()); + assert_eq!(branch, "master"); + } + + #[test] + fn configure_sparse_checkout_is_noop() { + let tmp = TempDir::new().unwrap(); + assert!(configure_sparse_checkout(tmp.path()).is_ok()); + } + + #[tokio::test] + async fn run_shell_command_succeeds_for_echo() { + let tmp = TempDir::new().unwrap(); + let result = run_shell_command("echo hello", tmp.path()).await; + assert!(result.is_ok(), "Expected success: {:?}", result.err()); + } + + #[tokio::test] + async fn run_shell_command_fails_for_nonzero_exit() { + let tmp = TempDir::new().unwrap(); + let result = run_shell_command("exit 1", tmp.path()).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("failed")); + } + + #[tokio::test] + async fn run_setup_commands_no_components_succeeds() { + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig { + component: vec![], + agent: vec![], + }; + assert!(run_setup_commands(tmp.path(), &config).await.is_ok()); + } + + #[tokio::test] + async fn run_setup_commands_runs_each_command_successfully() { + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig { + component: vec![ComponentConfig { + name: "test".to_string(), + path: ".".to_string(), + setup: vec!["echo setup_ok".to_string()], + teardown: vec![], + }], + agent: vec![], + }; + assert!(run_setup_commands(tmp.path(), &config).await.is_ok()); + } + + #[tokio::test] + async fn run_setup_commands_propagates_failure() { + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig { + component: vec![ComponentConfig { + name: "test".to_string(), + path: ".".to_string(), + setup: vec!["exit 1".to_string()], + teardown: vec![], + }], + agent: vec![], + }; + let result = run_setup_commands(tmp.path(), &config).await; + assert!(result.is_err(), "Expected failure from failing setup command"); + assert!(result.unwrap_err().contains("failed")); + } + + #[tokio::test] + async fn run_teardown_commands_ignores_failures() { + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig { + component: vec![ComponentConfig { + name: "test".to_string(), + path: ".".to_string(), + setup: vec![], + teardown: vec!["exit 1".to_string()], + }], + agent: vec![], + }; + // Teardown failures are best-effort — should not propagate + assert!(run_teardown_commands(tmp.path(), &config).await.is_ok()); + } + + #[tokio::test] + async fn create_worktree_fresh_creates_dir_and_mcp_json() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + let config = ProjectConfig { + component: vec![], + agent: vec![], + }; + let info = create_worktree(&project_root, "42_fresh_test", &config, 3001) + .await + .unwrap(); + + assert!(info.path.exists()); + assert!(info.path.join(".mcp.json").exists()); + let mcp = fs::read_to_string(info.path.join(".mcp.json")).unwrap(); + assert!(mcp.contains("3001")); + assert_eq!(info.branch, "feature/story-42_fresh_test"); + } + + #[tokio::test] + async fn create_worktree_reuses_existing_path_and_updates_port() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + let config = ProjectConfig { + component: vec![], + agent: vec![], + }; + // First creation + let _info1 = create_worktree(&project_root, "43_reuse_test", &config, 3001) + .await + .unwrap(); + // Second call — worktree already exists, reuse path, update port + let info2 = create_worktree(&project_root, "43_reuse_test", &config, 3002) + .await + .unwrap(); + + let mcp = fs::read_to_string(info2.path.join(".mcp.json")).unwrap(); + assert!(mcp.contains("3002"), "MCP json should be updated to new port"); + } + + #[test] + fn remove_worktree_sync_cleans_up_directory() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + let wt_path = project_root + .join(".story_kit") + .join("worktrees") + .join("test_rm"); + create_worktree_sync(&project_root, &wt_path, "feature/test-rm").unwrap(); + assert!(wt_path.exists()); + + remove_worktree_sync(&project_root, &wt_path, "feature/test-rm").unwrap(); + assert!(!wt_path.exists()); + } + + #[tokio::test] + async fn remove_worktree_by_story_id_returns_err_when_not_found() { + let tmp = TempDir::new().unwrap(); + let config = ProjectConfig { + component: vec![], + agent: vec![], + }; + + let result = remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &config).await; + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .contains("Worktree not found for story: 99_nonexistent") + ); + } + + #[tokio::test] + async fn remove_worktree_by_story_id_removes_existing_worktree() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + let config = ProjectConfig { + component: vec![], + agent: vec![], + }; + create_worktree(&project_root, "88_remove_by_id", &config, 3001) + .await + .unwrap(); + + let result = + remove_worktree_by_story_id(&project_root, "88_remove_by_id", &config).await; + assert!(result.is_ok(), "Expected removal to succeed: {:?}", result.err()); + } + + #[tokio::test] + async fn remove_worktree_async_removes_directory() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + let config = ProjectConfig { + component: vec![], + agent: vec![], + }; + let info = create_worktree(&project_root, "77_remove_async", &config, 3001) + .await + .unwrap(); + + let path = info.path.clone(); + assert!(path.exists()); + remove_worktree(&project_root, &info, &config).await.unwrap(); + assert!(!path.exists()); + } }