From ddc4a57cd2698d904b1c52701d10f8d60da8093b Mon Sep 17 00:00:00 2001 From: dave Date: Sat, 28 Mar 2026 19:47:59 +0000 Subject: [PATCH] storkit: merge 444_refactor_extract_shared_test_helpers_test_ctx_write_story_file_make_api --- server/src/chat/commands/move_story.rs | 6 +-- server/src/chat/commands/show.rs | 6 +-- server/src/chat/commands/triage.rs | 6 +-- server/src/chat/commands/unblock.rs | 6 +-- server/src/chat/mod.rs | 2 + server/src/chat/test_helpers.rs | 15 ++++++ server/src/chat/transport/matrix/assign.rs | 6 +-- server/src/http/anthropic.rs | 34 +++++++------- server/src/http/io.rs | 49 ++++++++++---------- server/src/http/mcp/agent_tools.rs | 6 +-- server/src/http/mcp/diagnostics.rs | 6 +-- server/src/http/mcp/git_tools.rs | 5 +- server/src/http/mcp/merge_tools.rs | 6 +-- server/src/http/mcp/mod.rs | 6 +-- server/src/http/mcp/qa_tools.rs | 6 +-- server/src/http/mcp/shell_tools.rs | 6 +-- server/src/http/mcp/story_tools.rs | 6 +-- server/src/http/mod.rs | 2 + server/src/http/model.rs | 25 +++++----- server/src/http/project.rs | 35 +++++++------- server/src/http/settings.rs | 54 ++++++++++------------ server/src/http/test_helpers.rs | 21 +++++++++ server/src/io/mod.rs | 2 + server/src/io/onboarding.rs | 9 +--- server/src/io/search.rs | 10 +--- server/src/io/test_helpers.rs | 32 +++++++++++++ server/src/io/wizard.rs | 8 +--- 27 files changed, 188 insertions(+), 187 deletions(-) create mode 100644 server/src/chat/test_helpers.rs create mode 100644 server/src/http/test_helpers.rs create mode 100644 server/src/io/test_helpers.rs diff --git a/server/src/chat/commands/move_story.rs b/server/src/chat/commands/move_story.rs index 10eb1b4d..65d6f3ac 100644 --- a/server/src/chat/commands/move_story.rs +++ b/server/src/chat/commands/move_story.rs @@ -142,11 +142,7 @@ mod tests { try_handle_command(&dispatch, &format!("@timmy move {args}")) } - fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { - let dir = root.join(".storkit/work").join(stage); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join(filename), content).unwrap(); - } + use crate::chat::test_helpers::write_story_file; #[test] fn move_command_is_registered() { diff --git a/server/src/chat/commands/show.rs b/server/src/chat/commands/show.rs index 34921969..20293763 100644 --- a/server/src/chat/commands/show.rs +++ b/server/src/chat/commands/show.rs @@ -91,11 +91,7 @@ mod tests { try_handle_command(&dispatch, &format!("@timmy show {args}")) } - fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { - let dir = root.join(".storkit/work").join(stage); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join(filename), content).unwrap(); - } + use crate::chat::test_helpers::write_story_file; #[test] fn show_command_is_registered() { diff --git a/server/src/chat/commands/triage.rs b/server/src/chat/commands/triage.rs index 0371ea17..37e78ca9 100644 --- a/server/src/chat/commands/triage.rs +++ b/server/src/chat/commands/triage.rs @@ -296,11 +296,7 @@ mod tests { try_handle_command(&dispatch, &format!("@timmy status {args}")) } - fn write_story_file(root: &Path, stage: &str, filename: &str, content: &str) { - let dir = root.join(".storkit/work").join(stage); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join(filename), content).unwrap(); - } + use crate::chat::test_helpers::write_story_file; // -- registration ------------------------------------------------------- diff --git a/server/src/chat/commands/unblock.rs b/server/src/chat/commands/unblock.rs index 40fd3054..1fa2019d 100644 --- a/server/src/chat/commands/unblock.rs +++ b/server/src/chat/commands/unblock.rs @@ -164,11 +164,7 @@ mod tests { try_handle_command(&dispatch, &format!("@timmy unblock {args}")) } - fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { - let dir = root.join(".storkit/work").join(stage); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join(filename), content).unwrap(); - } + use crate::chat::test_helpers::write_story_file; #[test] fn unblock_command_is_registered() { diff --git a/server/src/chat/mod.rs b/server/src/chat/mod.rs index 0f561921..c46e1a4b 100644 --- a/server/src/chat/mod.rs +++ b/server/src/chat/mod.rs @@ -8,6 +8,8 @@ pub mod commands; pub mod timer; pub mod transport; pub mod util; +#[cfg(test)] +pub(crate) mod test_helpers; use async_trait::async_trait; diff --git a/server/src/chat/test_helpers.rs b/server/src/chat/test_helpers.rs new file mode 100644 index 00000000..8ae5f170 --- /dev/null +++ b/server/src/chat/test_helpers.rs @@ -0,0 +1,15 @@ +//! Shared test utilities for chat handler tests. +//! +//! Import with `use crate::chat::test_helpers::write_story_file;` + +use std::path::Path; + +/// Write a work-item file into the standard pipeline directory structure. +/// +/// Creates `.storkit/work/{stage}/{filename}` under `root`, creating any +/// missing parent directories. +pub(crate) fn write_story_file(root: &Path, stage: &str, filename: &str, content: &str) { + let dir = root.join(".storkit/work").join(stage); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join(filename), content).unwrap(); +} diff --git a/server/src/chat/transport/matrix/assign.rs b/server/src/chat/transport/matrix/assign.rs index f3ab9a81..33d21b00 100644 --- a/server/src/chat/transport/matrix/assign.rs +++ b/server/src/chat/transport/matrix/assign.rs @@ -350,11 +350,7 @@ mod tests { // -- handle_assign (no running coder) ------------------------------------ - fn write_story_file(root: &Path, stage: &str, filename: &str, content: &str) { - let dir = root.join(".storkit/work").join(stage); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join(filename), content).unwrap(); - } + use crate::chat::test_helpers::write_story_file; #[tokio::test] async fn handle_assign_returns_not_found_for_unknown_number() { diff --git a/server/src/http/anthropic.rs b/server/src/http/anthropic.rs index 10f3b086..2630a356 100644 --- a/server/src/http/anthropic.rs +++ b/server/src/http/anthropic.rs @@ -64,6 +64,13 @@ impl AnthropicApi { } } +#[cfg(test)] +impl From> for AnthropicApi { + fn from(ctx: Arc) -> Self { + Self::new(ctx) + } +} + #[OpenApi(tag = "AnthropicTags::Anthropic")] impl AnthropicApi { /// Check whether an Anthropic API key is stored. @@ -151,25 +158,16 @@ impl AnthropicApi { #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; + use crate::http::test_helpers::{make_api, test_ctx}; use serde_json::json; - use std::sync::Arc; use tempfile::TempDir; - fn test_ctx(dir: &TempDir) -> AppContext { - AppContext::new_test(dir.path().to_path_buf()) - } - - fn make_api(dir: &TempDir) -> AnthropicApi { - AnthropicApi::new(Arc::new(test_ctx(dir))) - } - // -- get_anthropic_api_key (private helper) -- #[test] fn get_api_key_returns_err_when_not_set() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); let result = get_anthropic_api_key(&ctx); assert!(result.is_err()); assert!(result.unwrap_err().contains("not found")); @@ -178,7 +176,7 @@ mod tests { #[test] fn get_api_key_returns_err_when_empty() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); ctx.store.set(KEY_ANTHROPIC_API_KEY, json!("")); let result = get_anthropic_api_key(&ctx); assert!(result.is_err()); @@ -188,7 +186,7 @@ mod tests { #[test] fn get_api_key_returns_err_when_not_string() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); ctx.store.set(KEY_ANTHROPIC_API_KEY, json!(12345)); let result = get_anthropic_api_key(&ctx); assert!(result.is_err()); @@ -198,7 +196,7 @@ mod tests { #[test] fn get_api_key_returns_key_when_set() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); ctx.store.set(KEY_ANTHROPIC_API_KEY, json!("sk-ant-test123")); let result = get_anthropic_api_key(&ctx); assert_eq!(result.unwrap(), "sk-ant-test123"); @@ -209,7 +207,7 @@ mod tests { #[tokio::test] async fn key_exists_returns_false_when_not_set() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.get_anthropic_api_key_exists().await.unwrap(); assert!(!result.0); } @@ -229,7 +227,7 @@ mod tests { #[tokio::test] async fn set_api_key_returns_true() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(ApiKeyPayload { api_key: "sk-ant-test123".to_string(), }); @@ -256,7 +254,7 @@ mod tests { #[tokio::test] async fn list_models_fails_when_no_key() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.list_anthropic_models().await; assert!(result.is_err()); } @@ -288,7 +286,7 @@ mod tests { #[test] fn new_creates_api_instance() { let dir = TempDir::new().unwrap(); - let _api = make_api(&dir); + let _api = make_api::(&dir); } #[test] diff --git a/server/src/http/io.rs b/server/src/http/io.rs index 5975e7b8..c40bca64 100644 --- a/server/src/http/io.rs +++ b/server/src/http/io.rs @@ -138,18 +138,19 @@ impl IoApi { } } +#[cfg(test)] +impl From> for IoApi { + fn from(ctx: std::sync::Arc) -> Self { + Self { ctx } + } +} + #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; + use crate::http::test_helpers::make_api; use tempfile::TempDir; - fn make_api(dir: &TempDir) -> IoApi { - IoApi { - ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())), - } - } - // --- list_directory_absolute --- #[tokio::test] @@ -158,7 +159,7 @@ mod tests { std::fs::create_dir(dir.path().join("subdir")).unwrap(); std::fs::write(dir.path().join("file.txt"), "content").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: dir.path().to_string_lossy().to_string(), }); @@ -176,7 +177,7 @@ mod tests { let empty = dir.path().join("empty"); std::fs::create_dir(&empty).unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: empty.to_string_lossy().to_string(), }); @@ -187,7 +188,7 @@ mod tests { #[tokio::test] async fn list_directory_absolute_errors_on_nonexistent_path() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: dir.path().join("nonexistent").to_string_lossy().to_string(), }); @@ -201,7 +202,7 @@ mod tests { let file = dir.path().join("not_a_dir.txt"); std::fs::write(&file, "content").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: file.to_string_lossy().to_string(), }); @@ -216,7 +217,7 @@ mod tests { let dir = TempDir::new().unwrap(); let new_dir = dir.path().join("new_dir"); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(CreateDirectoryPayload { path: new_dir.to_string_lossy().to_string(), }); @@ -231,7 +232,7 @@ mod tests { let existing = dir.path().join("existing"); std::fs::create_dir(&existing).unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(CreateDirectoryPayload { path: existing.to_string_lossy().to_string(), }); @@ -244,7 +245,7 @@ mod tests { let dir = TempDir::new().unwrap(); let nested = dir.path().join("a").join("b").join("c"); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(CreateDirectoryPayload { path: nested.to_string_lossy().to_string(), }); @@ -258,7 +259,7 @@ mod tests { #[tokio::test] async fn get_home_directory_returns_a_path() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.get_home_directory().await.unwrap(); let home = &result.0; assert!(!home.is_empty()); @@ -272,7 +273,7 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("hello.txt"), "hello world").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: "hello.txt".to_string(), }); @@ -283,7 +284,7 @@ mod tests { #[tokio::test] async fn read_file_errors_on_missing_file() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: "nonexistent.txt".to_string(), }); @@ -296,7 +297,7 @@ mod tests { #[tokio::test] async fn write_file_creates_file() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(WriteFilePayload { path: "output.txt".to_string(), content: "written content".to_string(), @@ -312,7 +313,7 @@ mod tests { #[tokio::test] async fn write_file_creates_parent_dirs() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(WriteFilePayload { path: "sub/dir/file.txt".to_string(), content: "nested".to_string(), @@ -334,7 +335,7 @@ mod tests { std::fs::write(dir.path().join("src/main.rs"), "fn main() {}").unwrap(); std::fs::write(dir.path().join("README.md"), "# readme").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.list_project_files().await.unwrap(); let files = &result.0; @@ -348,7 +349,7 @@ mod tests { std::fs::create_dir(dir.path().join("subdir")).unwrap(); std::fs::write(dir.path().join("file.txt"), "").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.list_project_files().await.unwrap(); let files = &result.0; @@ -363,7 +364,7 @@ mod tests { std::fs::write(dir.path().join("z_last.txt"), "").unwrap(); std::fs::write(dir.path().join("a_first.txt"), "").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.list_project_files().await.unwrap(); let files = &result.0; @@ -380,7 +381,7 @@ mod tests { std::fs::create_dir(dir.path().join("adir")).unwrap(); std::fs::write(dir.path().join("bfile.txt"), "").unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: ".".to_string(), }); @@ -394,7 +395,7 @@ mod tests { #[tokio::test] async fn list_directory_errors_on_nonexistent() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(FilePathPayload { path: "nonexistent_dir".to_string(), }); diff --git a/server/src/http/mcp/agent_tools.rs b/server/src/http/mcp/agent_tools.rs index a948f528..85c162f3 100644 --- a/server/src/http/mcp/agent_tools.rs +++ b/server/src/http/mcp/agent_tools.rs @@ -370,13 +370,9 @@ pub(super) async fn get_worktree_commits(worktree_path: &str, base_branch: &str) #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; + use crate::http::test_helpers::test_ctx; use crate::store::StoreOps; - fn test_ctx(dir: &std::path::Path) -> AppContext { - AppContext::new_test(dir.to_path_buf()) - } - #[test] fn tool_list_agents_empty() { let tmp = tempfile::tempdir().unwrap(); diff --git a/server/src/http/mcp/diagnostics.rs b/server/src/http/mcp/diagnostics.rs index 4e20da9e..3cdca08d 100644 --- a/server/src/http/mcp/diagnostics.rs +++ b/server/src/http/mcp/diagnostics.rs @@ -279,11 +279,7 @@ pub(super) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result AppContext { - AppContext::new_test(dir.to_path_buf()) - } + use crate::http::test_helpers::test_ctx; #[test] fn tool_get_server_logs_no_args_returns_string() { diff --git a/server/src/http/mcp/git_tools.rs b/server/src/http/mcp/git_tools.rs index 0161eca4..387c50c9 100644 --- a/server/src/http/mcp/git_tools.rs +++ b/server/src/http/mcp/git_tools.rs @@ -304,12 +304,9 @@ pub(super) async fn tool_git_log(args: &Value, ctx: &AppContext) -> Result AppContext { - AppContext::new_test(dir.to_path_buf()) - } - /// Create a temp directory with a git worktree structure and init a repo. fn setup_worktree() -> (tempfile::TempDir, PathBuf, AppContext) { let tmp = tempfile::tempdir().unwrap(); diff --git a/server/src/http/mcp/merge_tools.rs b/server/src/http/mcp/merge_tools.rs index 8476104a..53b2b673 100644 --- a/server/src/http/mcp/merge_tools.rs +++ b/server/src/http/mcp/merge_tools.rs @@ -164,11 +164,7 @@ pub(super) fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Resul #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; - - fn test_ctx(dir: &std::path::Path) -> AppContext { - AppContext::new_test(dir.to_path_buf()) - } + use crate::http::test_helpers::test_ctx; fn setup_git_repo_in(dir: &std::path::Path) { std::process::Command::new("git") diff --git a/server/src/http/mcp/mod.rs b/server/src/http/mcp/mod.rs index 1fe86fb6..6c36014c 100644 --- a/server/src/http/mcp/mod.rs +++ b/server/src/http/mcp/mod.rs @@ -1336,11 +1336,7 @@ async fn handle_tools_call( #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; - - fn test_ctx(dir: &std::path::Path) -> AppContext { - AppContext::new_test(dir.to_path_buf()) - } + use crate::http::test_helpers::test_ctx; #[test] fn json_rpc_response_serializes_success() { diff --git a/server/src/http/mcp/qa_tools.rs b/server/src/http/mcp/qa_tools.rs index a2ed7ac2..6acea206 100644 --- a/server/src/http/mcp/qa_tools.rs +++ b/server/src/http/mcp/qa_tools.rs @@ -194,11 +194,7 @@ pub(super) fn find_free_port(start: u16) -> u16 { #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; - - fn test_ctx(dir: &std::path::Path) -> AppContext { - AppContext::new_test(dir.to_path_buf()) - } + use crate::http::test_helpers::test_ctx; #[test] fn request_qa_in_tools_list() { diff --git a/server/src/http/mcp/shell_tools.rs b/server/src/http/mcp/shell_tools.rs index a24f945b..e5fa4a13 100644 --- a/server/src/http/mcp/shell_tools.rs +++ b/server/src/http/mcp/shell_tools.rs @@ -331,13 +331,9 @@ pub(super) fn handle_run_command_sse( #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; + use crate::http::test_helpers::test_ctx; use serde_json::json; - fn test_ctx(dir: &std::path::Path) -> AppContext { - AppContext::new_test(dir.to_path_buf()) - } - // ── is_dangerous ───────────────────────────────────────────────── #[test] diff --git a/server/src/http/mcp/story_tools.rs b/server/src/http/mcp/story_tools.rs index 067fc14a..49429f3b 100644 --- a/server/src/http/mcp/story_tools.rs +++ b/server/src/http/mcp/story_tools.rs @@ -549,11 +549,7 @@ pub(super) fn parse_test_cases(value: Option<&Value>) -> Result AppContext { - AppContext::new_test(dir.to_path_buf()) - } + use crate::http::test_helpers::test_ctx; #[test] fn parse_test_cases_empty() { diff --git a/server/src/http/mod.rs b/server/src/http/mod.rs index 886a44c5..11e167fa 100644 --- a/server/src/http/mod.rs +++ b/server/src/http/mod.rs @@ -1,6 +1,8 @@ pub mod agents; pub mod agents_sse; pub mod anthropic; +#[cfg(test)] +pub(crate) mod test_helpers; pub mod assets; pub mod bot_command; pub mod chat; diff --git a/server/src/http/model.rs b/server/src/http/model.rs index 7cd6462b..388f1d11 100644 --- a/server/src/http/model.rs +++ b/server/src/http/model.rs @@ -50,22 +50,23 @@ impl ModelApi { } } +#[cfg(test)] +impl From> for ModelApi { + fn from(ctx: std::sync::Arc) -> Self { + Self { ctx } + } +} + #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; + use crate::http::test_helpers::make_api; use tempfile::TempDir; - fn make_api(dir: &TempDir) -> ModelApi { - ModelApi { - ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())), - } - } - #[tokio::test] async fn get_model_preference_returns_none_when_unset() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.get_model_preference().await.unwrap(); assert!(result.0.is_none()); } @@ -73,7 +74,7 @@ mod tests { #[tokio::test] async fn set_model_preference_returns_true() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(ModelPayload { model: "claude-3-sonnet".to_string(), }); @@ -84,7 +85,7 @@ mod tests { #[tokio::test] async fn get_model_preference_returns_value_after_set() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(ModelPayload { model: "claude-3-sonnet".to_string(), @@ -98,7 +99,7 @@ mod tests { #[tokio::test] async fn set_model_preference_overwrites_previous_value() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); api.set_model_preference(Json(ModelPayload { model: "model-a".to_string(), @@ -119,7 +120,7 @@ mod tests { #[tokio::test] async fn get_ollama_models_returns_empty_list_for_unreachable_url() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); // Port 1 is reserved and should immediately refuse the connection. let base_url = Query(Some("http://127.0.0.1:1".to_string())); let result = api.get_ollama_models(base_url).await; diff --git a/server/src/http/project.rs b/server/src/http/project.rs index 37a48ab8..b23bd030 100644 --- a/server/src/http/project.rs +++ b/server/src/http/project.rs @@ -73,22 +73,23 @@ impl ProjectApi { } } +#[cfg(test)] +impl From> for ProjectApi { + fn from(ctx: std::sync::Arc) -> Self { + Self { ctx } + } +} + #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; + use crate::http::test_helpers::make_api; use tempfile::TempDir; - fn make_api(dir: &TempDir) -> ProjectApi { - ProjectApi { - ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())), - } - } - #[tokio::test] async fn get_current_project_returns_none_when_unset() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); // Clear the project root that new_test sets api.close_project().await.unwrap(); let result = api.get_current_project().await.unwrap(); @@ -98,7 +99,7 @@ mod tests { #[tokio::test] async fn get_current_project_returns_path_from_state() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.get_current_project().await.unwrap(); assert_eq!(result.0, Some(dir.path().to_string_lossy().to_string())); } @@ -106,7 +107,7 @@ mod tests { #[tokio::test] async fn open_project_succeeds_with_valid_directory() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let path = dir.path().to_string_lossy().to_string(); let payload = Json(PathPayload { path: path.clone() }); let result = api.open_project(payload).await.unwrap(); @@ -116,7 +117,7 @@ mod tests { #[tokio::test] async fn open_project_fails_with_nonexistent_file_path() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); // Create a file (not a directory) to trigger validation error let file_path = dir.path().join("not_a_dir.txt"); std::fs::write(&file_path, "content").unwrap(); @@ -130,7 +131,7 @@ mod tests { #[tokio::test] async fn close_project_returns_true() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.close_project().await.unwrap(); assert!(result.0); } @@ -138,7 +139,7 @@ mod tests { #[tokio::test] async fn close_project_clears_current_project() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); // Verify project is set initially let before = api.get_current_project().await.unwrap(); @@ -155,7 +156,7 @@ mod tests { #[tokio::test] async fn list_known_projects_returns_empty_initially() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); // Close the project so the store has no known projects api.close_project().await.unwrap(); let result = api.list_known_projects().await.unwrap(); @@ -165,7 +166,7 @@ mod tests { #[tokio::test] async fn list_known_projects_returns_project_after_open() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let path = dir.path().to_string_lossy().to_string(); api.open_project(Json(PathPayload { path: path.clone() })) @@ -179,7 +180,7 @@ mod tests { #[tokio::test] async fn forget_known_project_removes_project() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let path = dir.path().to_string_lossy().to_string(); api.open_project(Json(PathPayload { path: path.clone() })) @@ -202,7 +203,7 @@ mod tests { #[tokio::test] async fn forget_known_project_returns_true_for_nonexistent_path() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api .forget_known_project(Json(PathPayload { path: "/some/unknown/path".to_string(), diff --git a/server/src/http/settings.rs b/server/src/http/settings.rs index 87a58605..5e74683f 100644 --- a/server/src/http/settings.rs +++ b/server/src/http/settings.rs @@ -104,27 +104,23 @@ pub fn get_editor_command_from_store(ctx: &AppContext) -> Option { .and_then(|v| v.as_str().map(|s| s.to_string())) } +#[cfg(test)] +impl From> for SettingsApi { + fn from(ctx: std::sync::Arc) -> Self { + Self { ctx } + } +} + #[cfg(test)] mod tests { use super::*; - use crate::http::context::AppContext; - use std::sync::Arc; + use crate::http::test_helpers::{make_api, test_ctx}; use tempfile::TempDir; - fn test_ctx(dir: &TempDir) -> AppContext { - AppContext::new_test(dir.path().to_path_buf()) - } - - fn make_api(dir: &TempDir) -> SettingsApi { - SettingsApi { - ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())), - } - } - #[tokio::test] async fn get_editor_returns_none_when_unset() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api.get_editor().await.unwrap(); assert!(result.0.editor_command.is_none()); } @@ -132,7 +128,7 @@ mod tests { #[tokio::test] async fn set_editor_stores_command() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let payload = Json(EditorCommandPayload { editor_command: Some("zed".to_string()), }); @@ -143,7 +139,7 @@ mod tests { #[tokio::test] async fn set_editor_clears_command_on_null() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); api.set_editor(Json(EditorCommandPayload { editor_command: Some("zed".to_string()), })) @@ -161,7 +157,7 @@ mod tests { #[tokio::test] async fn set_editor_clears_command_on_empty_string() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api .set_editor(Json(EditorCommandPayload { editor_command: Some(String::new()), @@ -174,7 +170,7 @@ mod tests { #[tokio::test] async fn set_editor_trims_whitespace_only() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api .set_editor(Json(EditorCommandPayload { editor_command: Some(" ".to_string()), @@ -187,7 +183,7 @@ mod tests { #[tokio::test] async fn get_editor_returns_value_after_set() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); api.set_editor(Json(EditorCommandPayload { editor_command: Some("cursor".to_string()), })) @@ -200,7 +196,7 @@ mod tests { #[test] fn editor_command_defaults_to_null() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); let result = get_editor_command_from_store(&ctx); assert!(result.is_none()); } @@ -208,7 +204,7 @@ mod tests { #[test] fn set_editor_command_persists_in_store() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); ctx.store.set(EDITOR_COMMAND_KEY, json!("zed")); ctx.store.save().unwrap(); @@ -220,7 +216,7 @@ mod tests { #[test] fn get_editor_command_from_store_returns_value() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); ctx.store.set(EDITOR_COMMAND_KEY, json!("code")); let result = get_editor_command_from_store(&ctx); @@ -230,7 +226,7 @@ mod tests { #[test] fn delete_editor_command_returns_none() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); ctx.store.set(EDITOR_COMMAND_KEY, json!("cursor")); ctx.store.delete(EDITOR_COMMAND_KEY); @@ -258,7 +254,7 @@ mod tests { #[tokio::test] async fn get_editor_http_handler_returns_null_when_not_set() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); let api = SettingsApi { ctx: Arc::new(ctx), }; @@ -269,7 +265,7 @@ mod tests { #[tokio::test] async fn set_editor_http_handler_stores_value() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); let api = SettingsApi { ctx: Arc::new(ctx), }; @@ -286,7 +282,7 @@ mod tests { #[tokio::test] async fn set_editor_http_handler_clears_value_when_null() { let dir = TempDir::new().unwrap(); - let ctx = test_ctx(&dir); + let ctx = test_ctx(dir.path()); let api = SettingsApi { ctx: Arc::new(ctx), }; @@ -310,7 +306,7 @@ mod tests { #[tokio::test] async fn open_file_returns_error_when_no_editor_configured() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); let result = api .open_file(Query("src/main.rs".to_string()), Query(Some(42))) .await; @@ -322,7 +318,7 @@ mod tests { #[tokio::test] async fn open_file_spawns_editor_with_path_and_line() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); // Configure the editor to "echo" which is a safe no-op command api.set_editor(Json(EditorCommandPayload { editor_command: Some("echo".to_string()), @@ -339,7 +335,7 @@ mod tests { #[tokio::test] async fn open_file_spawns_editor_with_path_only_when_no_line() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); api.set_editor(Json(EditorCommandPayload { editor_command: Some("echo".to_string()), })) @@ -355,7 +351,7 @@ mod tests { #[tokio::test] async fn open_file_returns_error_for_nonexistent_editor() { let dir = TempDir::new().unwrap(); - let api = make_api(&dir); + let api = make_api::(&dir); api.set_editor(Json(EditorCommandPayload { editor_command: Some("this_editor_does_not_exist_xyz_abc".to_string()), })) diff --git a/server/src/http/test_helpers.rs b/server/src/http/test_helpers.rs new file mode 100644 index 00000000..c4a5651f --- /dev/null +++ b/server/src/http/test_helpers.rs @@ -0,0 +1,21 @@ +//! Shared test utilities for HTTP handler tests. +//! +//! Import with `use crate::http::test_helpers::{make_api, test_ctx};` + +use crate::http::context::AppContext; +use std::path::Path; +use std::sync::Arc; +use tempfile::TempDir; + +/// Build an [`AppContext`] rooted at `dir` for use in tests. +pub(crate) fn test_ctx(dir: &Path) -> AppContext { + AppContext::new_test(dir.to_path_buf()) +} + +/// Build an API struct rooted in `dir` for use in tests. +/// +/// Requires the API type to implement `From>`. Add a +/// `#[cfg(test)]` impl block to each API struct to opt in. +pub(crate) fn make_api>>(dir: &TempDir) -> T { + Arc::new(test_ctx(dir.path())).into() +} diff --git a/server/src/io/mod.rs b/server/src/io/mod.rs index 60f7074d..825f6888 100644 --- a/server/src/io/mod.rs +++ b/server/src/io/mod.rs @@ -5,3 +5,5 @@ pub mod shell; pub mod story_metadata; pub mod watcher; pub mod wizard; +#[cfg(test)] +pub(crate) mod test_helpers; diff --git a/server/src/io/onboarding.rs b/server/src/io/onboarding.rs index fd500993..8e0e435a 100644 --- a/server/src/io/onboarding.rs +++ b/server/src/io/onboarding.rs @@ -74,17 +74,10 @@ fn needs_project_toml(story_kit: &Path) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::io::test_helpers::setup_project; use std::fs; use tempfile::TempDir; - fn setup_project(dir: &TempDir) -> std::path::PathBuf { - let root = dir.path().to_path_buf(); - let sk = root.join(".storkit"); - fs::create_dir_all(sk.join("specs").join("tech")).unwrap(); - fs::create_dir_all(root.join("script")).unwrap(); - root - } - // ── needs_onboarding ────────────────────────────────────────── #[test] diff --git a/server/src/io/search.rs b/server/src/io/search.rs index 8225fac3..bdd68fa5 100644 --- a/server/src/io/search.rs +++ b/server/src/io/search.rs @@ -64,18 +64,12 @@ pub async fn search_files_impl(query: String, root: PathBuf) -> Result TempDir { let dir = TempDir::new().unwrap(); - for (path, content) in files { - let full = dir.path().join(path); - if let Some(parent) = full.parent() { - fs::create_dir_all(parent).unwrap(); - } - fs::write(full, content).unwrap(); - } + create_test_files(&dir, files); dir } diff --git a/server/src/io/test_helpers.rs b/server/src/io/test_helpers.rs new file mode 100644 index 00000000..8c815cbe --- /dev/null +++ b/server/src/io/test_helpers.rs @@ -0,0 +1,32 @@ +//! Shared test utilities for I/O module tests. +//! +//! Import with `use crate::io::test_helpers::{create_test_files, setup_project};` + +use std::fs; +use std::path::PathBuf; +use tempfile::TempDir; + +/// Create a minimal storkit project directory structure under `dir`. +/// +/// Creates `.storkit/specs/tech/` and `script/`, then returns the root path. +/// Used by onboarding and wizard tests. +pub(crate) fn setup_project(dir: &TempDir) -> PathBuf { + let root = dir.path().to_path_buf(); + let sk = root.join(".storkit"); + fs::create_dir_all(sk.join("specs").join("tech")).unwrap(); + fs::create_dir_all(root.join("script")).unwrap(); + root +} + +/// Write a set of files into `dir` at the given relative paths. +/// +/// Parent directories are created automatically. Used by search tests. +pub(crate) fn create_test_files(dir: &TempDir, files: &[(&str, &str)]) { + for (path, content) in files { + let full = dir.path().join(path); + if let Some(parent) = full.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(full, content).unwrap(); + } +} diff --git a/server/src/io/wizard.rs b/server/src/io/wizard.rs index 4718e4a1..7d64fccd 100644 --- a/server/src/io/wizard.rs +++ b/server/src/io/wizard.rs @@ -255,15 +255,9 @@ pub fn format_wizard_state(state: &WizardState) -> String { #[cfg(test)] mod tests { use super::*; + use crate::io::test_helpers::setup_project; use tempfile::TempDir; - fn setup_project(dir: &TempDir) -> std::path::PathBuf { - let root = dir.path().to_path_buf(); - let sk = root.join(".storkit"); - std::fs::create_dir_all(&sk).unwrap(); - root - } - #[test] fn default_state_has_all_steps_pending() { let state = WizardState::default();