storkit: merge 444_refactor_extract_shared_test_helpers_test_ctx_write_story_file_make_api

This commit is contained in:
dave
2026-03-28 19:47:59 +00:00
parent d216f3c267
commit ddc4a57cd2
27 changed files with 188 additions and 187 deletions
+1 -5
View File
@@ -142,11 +142,7 @@ mod tests {
try_handle_command(&dispatch, &format!("@timmy move {args}")) try_handle_command(&dispatch, &format!("@timmy move {args}"))
} }
fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { use crate::chat::test_helpers::write_story_file;
let dir = root.join(".storkit/work").join(stage);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(filename), content).unwrap();
}
#[test] #[test]
fn move_command_is_registered() { fn move_command_is_registered() {
+1 -5
View File
@@ -91,11 +91,7 @@ mod tests {
try_handle_command(&dispatch, &format!("@timmy show {args}")) try_handle_command(&dispatch, &format!("@timmy show {args}"))
} }
fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { use crate::chat::test_helpers::write_story_file;
let dir = root.join(".storkit/work").join(stage);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(filename), content).unwrap();
}
#[test] #[test]
fn show_command_is_registered() { fn show_command_is_registered() {
+1 -5
View File
@@ -296,11 +296,7 @@ mod tests {
try_handle_command(&dispatch, &format!("@timmy status {args}")) try_handle_command(&dispatch, &format!("@timmy status {args}"))
} }
fn write_story_file(root: &Path, stage: &str, filename: &str, content: &str) { use crate::chat::test_helpers::write_story_file;
let dir = root.join(".storkit/work").join(stage);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(filename), content).unwrap();
}
// -- registration ------------------------------------------------------- // -- registration -------------------------------------------------------
+1 -5
View File
@@ -164,11 +164,7 @@ mod tests {
try_handle_command(&dispatch, &format!("@timmy unblock {args}")) try_handle_command(&dispatch, &format!("@timmy unblock {args}"))
} }
fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { use crate::chat::test_helpers::write_story_file;
let dir = root.join(".storkit/work").join(stage);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(filename), content).unwrap();
}
#[test] #[test]
fn unblock_command_is_registered() { fn unblock_command_is_registered() {
+2
View File
@@ -8,6 +8,8 @@ pub mod commands;
pub mod timer; pub mod timer;
pub mod transport; pub mod transport;
pub mod util; pub mod util;
#[cfg(test)]
pub(crate) mod test_helpers;
use async_trait::async_trait; use async_trait::async_trait;
+15
View File
@@ -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();
}
+1 -5
View File
@@ -350,11 +350,7 @@ mod tests {
// -- handle_assign (no running coder) ------------------------------------ // -- handle_assign (no running coder) ------------------------------------
fn write_story_file(root: &Path, stage: &str, filename: &str, content: &str) { use crate::chat::test_helpers::write_story_file;
let dir = root.join(".storkit/work").join(stage);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(filename), content).unwrap();
}
#[tokio::test] #[tokio::test]
async fn handle_assign_returns_not_found_for_unknown_number() { async fn handle_assign_returns_not_found_for_unknown_number() {
+16 -18
View File
@@ -64,6 +64,13 @@ impl AnthropicApi {
} }
} }
#[cfg(test)]
impl From<Arc<AppContext>> for AnthropicApi {
fn from(ctx: Arc<AppContext>) -> Self {
Self::new(ctx)
}
}
#[OpenApi(tag = "AnthropicTags::Anthropic")] #[OpenApi(tag = "AnthropicTags::Anthropic")]
impl AnthropicApi { impl AnthropicApi {
/// Check whether an Anthropic API key is stored. /// Check whether an Anthropic API key is stored.
@@ -151,25 +158,16 @@ impl AnthropicApi {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::{make_api, test_ctx};
use serde_json::json; use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir; 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) -- // -- get_anthropic_api_key (private helper) --
#[test] #[test]
fn get_api_key_returns_err_when_not_set() { fn get_api_key_returns_err_when_not_set() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
let result = get_anthropic_api_key(&ctx); let result = get_anthropic_api_key(&ctx);
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("not found")); assert!(result.unwrap_err().contains("not found"));
@@ -178,7 +176,7 @@ mod tests {
#[test] #[test]
fn get_api_key_returns_err_when_empty() { fn get_api_key_returns_err_when_empty() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
ctx.store.set(KEY_ANTHROPIC_API_KEY, json!("")); ctx.store.set(KEY_ANTHROPIC_API_KEY, json!(""));
let result = get_anthropic_api_key(&ctx); let result = get_anthropic_api_key(&ctx);
assert!(result.is_err()); assert!(result.is_err());
@@ -188,7 +186,7 @@ mod tests {
#[test] #[test]
fn get_api_key_returns_err_when_not_string() { fn get_api_key_returns_err_when_not_string() {
let dir = TempDir::new().unwrap(); 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)); ctx.store.set(KEY_ANTHROPIC_API_KEY, json!(12345));
let result = get_anthropic_api_key(&ctx); let result = get_anthropic_api_key(&ctx);
assert!(result.is_err()); assert!(result.is_err());
@@ -198,7 +196,7 @@ mod tests {
#[test] #[test]
fn get_api_key_returns_key_when_set() { fn get_api_key_returns_key_when_set() {
let dir = TempDir::new().unwrap(); 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")); ctx.store.set(KEY_ANTHROPIC_API_KEY, json!("sk-ant-test123"));
let result = get_anthropic_api_key(&ctx); let result = get_anthropic_api_key(&ctx);
assert_eq!(result.unwrap(), "sk-ant-test123"); assert_eq!(result.unwrap(), "sk-ant-test123");
@@ -209,7 +207,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn key_exists_returns_false_when_not_set() { async fn key_exists_returns_false_when_not_set() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<AnthropicApi>(&dir);
let result = api.get_anthropic_api_key_exists().await.unwrap(); let result = api.get_anthropic_api_key_exists().await.unwrap();
assert!(!result.0); assert!(!result.0);
} }
@@ -229,7 +227,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_api_key_returns_true() { async fn set_api_key_returns_true() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<AnthropicApi>(&dir);
let payload = Json(ApiKeyPayload { let payload = Json(ApiKeyPayload {
api_key: "sk-ant-test123".to_string(), api_key: "sk-ant-test123".to_string(),
}); });
@@ -256,7 +254,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn list_models_fails_when_no_key() { async fn list_models_fails_when_no_key() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<AnthropicApi>(&dir);
let result = api.list_anthropic_models().await; let result = api.list_anthropic_models().await;
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -288,7 +286,7 @@ mod tests {
#[test] #[test]
fn new_creates_api_instance() { fn new_creates_api_instance() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let _api = make_api(&dir); let _api = make_api::<AnthropicApi>(&dir);
} }
#[test] #[test]
+25 -24
View File
@@ -138,18 +138,19 @@ impl IoApi {
} }
} }
#[cfg(test)]
impl From<std::sync::Arc<AppContext>> for IoApi {
fn from(ctx: std::sync::Arc<AppContext>) -> Self {
Self { ctx }
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::make_api;
use tempfile::TempDir; use tempfile::TempDir;
fn make_api(dir: &TempDir) -> IoApi {
IoApi {
ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
}
}
// --- list_directory_absolute --- // --- list_directory_absolute ---
#[tokio::test] #[tokio::test]
@@ -158,7 +159,7 @@ mod tests {
std::fs::create_dir(dir.path().join("subdir")).unwrap(); std::fs::create_dir(dir.path().join("subdir")).unwrap();
std::fs::write(dir.path().join("file.txt"), "content").unwrap(); std::fs::write(dir.path().join("file.txt"), "content").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: dir.path().to_string_lossy().to_string(), path: dir.path().to_string_lossy().to_string(),
}); });
@@ -176,7 +177,7 @@ mod tests {
let empty = dir.path().join("empty"); let empty = dir.path().join("empty");
std::fs::create_dir(&empty).unwrap(); std::fs::create_dir(&empty).unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: empty.to_string_lossy().to_string(), path: empty.to_string_lossy().to_string(),
}); });
@@ -187,7 +188,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn list_directory_absolute_errors_on_nonexistent_path() { async fn list_directory_absolute_errors_on_nonexistent_path() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: dir.path().join("nonexistent").to_string_lossy().to_string(), 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"); let file = dir.path().join("not_a_dir.txt");
std::fs::write(&file, "content").unwrap(); std::fs::write(&file, "content").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: file.to_string_lossy().to_string(), path: file.to_string_lossy().to_string(),
}); });
@@ -216,7 +217,7 @@ mod tests {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let new_dir = dir.path().join("new_dir"); let new_dir = dir.path().join("new_dir");
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(CreateDirectoryPayload { let payload = Json(CreateDirectoryPayload {
path: new_dir.to_string_lossy().to_string(), path: new_dir.to_string_lossy().to_string(),
}); });
@@ -231,7 +232,7 @@ mod tests {
let existing = dir.path().join("existing"); let existing = dir.path().join("existing");
std::fs::create_dir(&existing).unwrap(); std::fs::create_dir(&existing).unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(CreateDirectoryPayload { let payload = Json(CreateDirectoryPayload {
path: existing.to_string_lossy().to_string(), path: existing.to_string_lossy().to_string(),
}); });
@@ -244,7 +245,7 @@ mod tests {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let nested = dir.path().join("a").join("b").join("c"); let nested = dir.path().join("a").join("b").join("c");
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(CreateDirectoryPayload { let payload = Json(CreateDirectoryPayload {
path: nested.to_string_lossy().to_string(), path: nested.to_string_lossy().to_string(),
}); });
@@ -258,7 +259,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_home_directory_returns_a_path() { async fn get_home_directory_returns_a_path() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let result = api.get_home_directory().await.unwrap(); let result = api.get_home_directory().await.unwrap();
let home = &result.0; let home = &result.0;
assert!(!home.is_empty()); assert!(!home.is_empty());
@@ -272,7 +273,7 @@ mod tests {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("hello.txt"), "hello world").unwrap(); std::fs::write(dir.path().join("hello.txt"), "hello world").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: "hello.txt".to_string(), path: "hello.txt".to_string(),
}); });
@@ -283,7 +284,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn read_file_errors_on_missing_file() { async fn read_file_errors_on_missing_file() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: "nonexistent.txt".to_string(), path: "nonexistent.txt".to_string(),
}); });
@@ -296,7 +297,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn write_file_creates_file() { async fn write_file_creates_file() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(WriteFilePayload { let payload = Json(WriteFilePayload {
path: "output.txt".to_string(), path: "output.txt".to_string(),
content: "written content".to_string(), content: "written content".to_string(),
@@ -312,7 +313,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn write_file_creates_parent_dirs() { async fn write_file_creates_parent_dirs() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(WriteFilePayload { let payload = Json(WriteFilePayload {
path: "sub/dir/file.txt".to_string(), path: "sub/dir/file.txt".to_string(),
content: "nested".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("src/main.rs"), "fn main() {}").unwrap();
std::fs::write(dir.path().join("README.md"), "# readme").unwrap(); std::fs::write(dir.path().join("README.md"), "# readme").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let result = api.list_project_files().await.unwrap(); let result = api.list_project_files().await.unwrap();
let files = &result.0; let files = &result.0;
@@ -348,7 +349,7 @@ mod tests {
std::fs::create_dir(dir.path().join("subdir")).unwrap(); std::fs::create_dir(dir.path().join("subdir")).unwrap();
std::fs::write(dir.path().join("file.txt"), "").unwrap(); std::fs::write(dir.path().join("file.txt"), "").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let result = api.list_project_files().await.unwrap(); let result = api.list_project_files().await.unwrap();
let files = &result.0; 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("z_last.txt"), "").unwrap();
std::fs::write(dir.path().join("a_first.txt"), "").unwrap(); std::fs::write(dir.path().join("a_first.txt"), "").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let result = api.list_project_files().await.unwrap(); let result = api.list_project_files().await.unwrap();
let files = &result.0; let files = &result.0;
@@ -380,7 +381,7 @@ mod tests {
std::fs::create_dir(dir.path().join("adir")).unwrap(); std::fs::create_dir(dir.path().join("adir")).unwrap();
std::fs::write(dir.path().join("bfile.txt"), "").unwrap(); std::fs::write(dir.path().join("bfile.txt"), "").unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: ".".to_string(), path: ".".to_string(),
}); });
@@ -394,7 +395,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn list_directory_errors_on_nonexistent() { async fn list_directory_errors_on_nonexistent() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<IoApi>(&dir);
let payload = Json(FilePathPayload { let payload = Json(FilePathPayload {
path: "nonexistent_dir".to_string(), path: "nonexistent_dir".to_string(),
}); });
+1 -5
View File
@@ -370,13 +370,9 @@ pub(super) async fn get_worktree_commits(worktree_path: &str, base_branch: &str)
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
use crate::store::StoreOps; use crate::store::StoreOps;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
#[test] #[test]
fn tool_list_agents_empty() { fn tool_list_agents_empty() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+1 -5
View File
@@ -279,11 +279,7 @@ pub(super) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result<String, St
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
#[test] #[test]
fn tool_get_server_logs_no_args_returns_string() { fn tool_get_server_logs_no_args_returns_string() {
+1 -4
View File
@@ -304,12 +304,9 @@ pub(super) async fn tool_git_log(args: &Value, ctx: &AppContext) -> Result<Strin
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::context::AppContext;
use crate::http::test_helpers::test_ctx;
use serde_json::json; use serde_json::json;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
/// Create a temp directory with a git worktree structure and init a repo. /// Create a temp directory with a git worktree structure and init a repo.
fn setup_worktree() -> (tempfile::TempDir, PathBuf, AppContext) { fn setup_worktree() -> (tempfile::TempDir, PathBuf, AppContext) {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+1 -5
View File
@@ -164,11 +164,7 @@ pub(super) fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Resul
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
fn setup_git_repo_in(dir: &std::path::Path) { fn setup_git_repo_in(dir: &std::path::Path) {
std::process::Command::new("git") std::process::Command::new("git")
+1 -5
View File
@@ -1336,11 +1336,7 @@ async fn handle_tools_call(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
#[test] #[test]
fn json_rpc_response_serializes_success() { fn json_rpc_response_serializes_success() {
+1 -5
View File
@@ -194,11 +194,7 @@ pub(super) fn find_free_port(start: u16) -> u16 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
#[test] #[test]
fn request_qa_in_tools_list() { fn request_qa_in_tools_list() {
+1 -5
View File
@@ -331,13 +331,9 @@ pub(super) fn handle_run_command_sse(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
use serde_json::json; use serde_json::json;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
// ── is_dangerous ───────────────────────────────────────────────── // ── is_dangerous ─────────────────────────────────────────────────
#[test] #[test]
+1 -5
View File
@@ -549,11 +549,7 @@ pub(super) fn parse_test_cases(value: Option<&Value>) -> Result<Vec<TestCaseResu
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::test_ctx;
fn test_ctx(dir: &std::path::Path) -> AppContext {
AppContext::new_test(dir.to_path_buf())
}
#[test] #[test]
fn parse_test_cases_empty() { fn parse_test_cases_empty() {
+2
View File
@@ -1,6 +1,8 @@
pub mod agents; pub mod agents;
pub mod agents_sse; pub mod agents_sse;
pub mod anthropic; pub mod anthropic;
#[cfg(test)]
pub(crate) mod test_helpers;
pub mod assets; pub mod assets;
pub mod bot_command; pub mod bot_command;
pub mod chat; pub mod chat;
+13 -12
View File
@@ -50,22 +50,23 @@ impl ModelApi {
} }
} }
#[cfg(test)]
impl From<std::sync::Arc<AppContext>> for ModelApi {
fn from(ctx: std::sync::Arc<AppContext>) -> Self {
Self { ctx }
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::make_api;
use tempfile::TempDir; use tempfile::TempDir;
fn make_api(dir: &TempDir) -> ModelApi {
ModelApi {
ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
}
}
#[tokio::test] #[tokio::test]
async fn get_model_preference_returns_none_when_unset() { async fn get_model_preference_returns_none_when_unset() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ModelApi>(&dir);
let result = api.get_model_preference().await.unwrap(); let result = api.get_model_preference().await.unwrap();
assert!(result.0.is_none()); assert!(result.0.is_none());
} }
@@ -73,7 +74,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_model_preference_returns_true() { async fn set_model_preference_returns_true() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ModelApi>(&dir);
let payload = Json(ModelPayload { let payload = Json(ModelPayload {
model: "claude-3-sonnet".to_string(), model: "claude-3-sonnet".to_string(),
}); });
@@ -84,7 +85,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_model_preference_returns_value_after_set() { async fn get_model_preference_returns_value_after_set() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ModelApi>(&dir);
let payload = Json(ModelPayload { let payload = Json(ModelPayload {
model: "claude-3-sonnet".to_string(), model: "claude-3-sonnet".to_string(),
@@ -98,7 +99,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_model_preference_overwrites_previous_value() { async fn set_model_preference_overwrites_previous_value() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ModelApi>(&dir);
api.set_model_preference(Json(ModelPayload { api.set_model_preference(Json(ModelPayload {
model: "model-a".to_string(), model: "model-a".to_string(),
@@ -119,7 +120,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_ollama_models_returns_empty_list_for_unreachable_url() { async fn get_ollama_models_returns_empty_list_for_unreachable_url() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ModelApi>(&dir);
// Port 1 is reserved and should immediately refuse the connection. // Port 1 is reserved and should immediately refuse the connection.
let base_url = Query(Some("http://127.0.0.1:1".to_string())); let base_url = Query(Some("http://127.0.0.1:1".to_string()));
let result = api.get_ollama_models(base_url).await; let result = api.get_ollama_models(base_url).await;
+18 -17
View File
@@ -73,22 +73,23 @@ impl ProjectApi {
} }
} }
#[cfg(test)]
impl From<std::sync::Arc<AppContext>> for ProjectApi {
fn from(ctx: std::sync::Arc<AppContext>) -> Self {
Self { ctx }
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::make_api;
use tempfile::TempDir; use tempfile::TempDir;
fn make_api(dir: &TempDir) -> ProjectApi {
ProjectApi {
ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
}
}
#[tokio::test] #[tokio::test]
async fn get_current_project_returns_none_when_unset() { async fn get_current_project_returns_none_when_unset() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
// Clear the project root that new_test sets // Clear the project root that new_test sets
api.close_project().await.unwrap(); api.close_project().await.unwrap();
let result = api.get_current_project().await.unwrap(); let result = api.get_current_project().await.unwrap();
@@ -98,7 +99,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_current_project_returns_path_from_state() { async fn get_current_project_returns_path_from_state() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
let result = api.get_current_project().await.unwrap(); let result = api.get_current_project().await.unwrap();
assert_eq!(result.0, Some(dir.path().to_string_lossy().to_string())); assert_eq!(result.0, Some(dir.path().to_string_lossy().to_string()));
} }
@@ -106,7 +107,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn open_project_succeeds_with_valid_directory() { async fn open_project_succeeds_with_valid_directory() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
let path = dir.path().to_string_lossy().to_string(); let path = dir.path().to_string_lossy().to_string();
let payload = Json(PathPayload { path: path.clone() }); let payload = Json(PathPayload { path: path.clone() });
let result = api.open_project(payload).await.unwrap(); let result = api.open_project(payload).await.unwrap();
@@ -116,7 +117,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn open_project_fails_with_nonexistent_file_path() { async fn open_project_fails_with_nonexistent_file_path() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
// Create a file (not a directory) to trigger validation error // Create a file (not a directory) to trigger validation error
let file_path = dir.path().join("not_a_dir.txt"); let file_path = dir.path().join("not_a_dir.txt");
std::fs::write(&file_path, "content").unwrap(); std::fs::write(&file_path, "content").unwrap();
@@ -130,7 +131,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn close_project_returns_true() { async fn close_project_returns_true() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
let result = api.close_project().await.unwrap(); let result = api.close_project().await.unwrap();
assert!(result.0); assert!(result.0);
} }
@@ -138,7 +139,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn close_project_clears_current_project() { async fn close_project_clears_current_project() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
// Verify project is set initially // Verify project is set initially
let before = api.get_current_project().await.unwrap(); let before = api.get_current_project().await.unwrap();
@@ -155,7 +156,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn list_known_projects_returns_empty_initially() { async fn list_known_projects_returns_empty_initially() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
// Close the project so the store has no known projects // Close the project so the store has no known projects
api.close_project().await.unwrap(); api.close_project().await.unwrap();
let result = api.list_known_projects().await.unwrap(); let result = api.list_known_projects().await.unwrap();
@@ -165,7 +166,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn list_known_projects_returns_project_after_open() { async fn list_known_projects_returns_project_after_open() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
let path = dir.path().to_string_lossy().to_string(); let path = dir.path().to_string_lossy().to_string();
api.open_project(Json(PathPayload { path: path.clone() })) api.open_project(Json(PathPayload { path: path.clone() }))
@@ -179,7 +180,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn forget_known_project_removes_project() { async fn forget_known_project_removes_project() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
let path = dir.path().to_string_lossy().to_string(); let path = dir.path().to_string_lossy().to_string();
api.open_project(Json(PathPayload { path: path.clone() })) api.open_project(Json(PathPayload { path: path.clone() }))
@@ -202,7 +203,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn forget_known_project_returns_true_for_nonexistent_path() { async fn forget_known_project_returns_true_for_nonexistent_path() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<ProjectApi>(&dir);
let result = api let result = api
.forget_known_project(Json(PathPayload { .forget_known_project(Json(PathPayload {
path: "/some/unknown/path".to_string(), path: "/some/unknown/path".to_string(),
+25 -29
View File
@@ -104,27 +104,23 @@ pub fn get_editor_command_from_store(ctx: &AppContext) -> Option<String> {
.and_then(|v| v.as_str().map(|s| s.to_string())) .and_then(|v| v.as_str().map(|s| s.to_string()))
} }
#[cfg(test)]
impl From<std::sync::Arc<AppContext>> for SettingsApi {
fn from(ctx: std::sync::Arc<AppContext>) -> Self {
Self { ctx }
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::context::AppContext; use crate::http::test_helpers::{make_api, test_ctx};
use std::sync::Arc;
use tempfile::TempDir; 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] #[tokio::test]
async fn get_editor_returns_none_when_unset() { async fn get_editor_returns_none_when_unset() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
let result = api.get_editor().await.unwrap(); let result = api.get_editor().await.unwrap();
assert!(result.0.editor_command.is_none()); assert!(result.0.editor_command.is_none());
} }
@@ -132,7 +128,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_editor_stores_command() { async fn set_editor_stores_command() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
let payload = Json(EditorCommandPayload { let payload = Json(EditorCommandPayload {
editor_command: Some("zed".to_string()), editor_command: Some("zed".to_string()),
}); });
@@ -143,7 +139,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_editor_clears_command_on_null() { async fn set_editor_clears_command_on_null() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
api.set_editor(Json(EditorCommandPayload { api.set_editor(Json(EditorCommandPayload {
editor_command: Some("zed".to_string()), editor_command: Some("zed".to_string()),
})) }))
@@ -161,7 +157,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_editor_clears_command_on_empty_string() { async fn set_editor_clears_command_on_empty_string() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
let result = api let result = api
.set_editor(Json(EditorCommandPayload { .set_editor(Json(EditorCommandPayload {
editor_command: Some(String::new()), editor_command: Some(String::new()),
@@ -174,7 +170,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_editor_trims_whitespace_only() { async fn set_editor_trims_whitespace_only() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
let result = api let result = api
.set_editor(Json(EditorCommandPayload { .set_editor(Json(EditorCommandPayload {
editor_command: Some(" ".to_string()), editor_command: Some(" ".to_string()),
@@ -187,7 +183,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_editor_returns_value_after_set() { async fn get_editor_returns_value_after_set() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
api.set_editor(Json(EditorCommandPayload { api.set_editor(Json(EditorCommandPayload {
editor_command: Some("cursor".to_string()), editor_command: Some("cursor".to_string()),
})) }))
@@ -200,7 +196,7 @@ mod tests {
#[test] #[test]
fn editor_command_defaults_to_null() { fn editor_command_defaults_to_null() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
let result = get_editor_command_from_store(&ctx); let result = get_editor_command_from_store(&ctx);
assert!(result.is_none()); assert!(result.is_none());
} }
@@ -208,7 +204,7 @@ mod tests {
#[test] #[test]
fn set_editor_command_persists_in_store() { fn set_editor_command_persists_in_store() {
let dir = TempDir::new().unwrap(); 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.set(EDITOR_COMMAND_KEY, json!("zed"));
ctx.store.save().unwrap(); ctx.store.save().unwrap();
@@ -220,7 +216,7 @@ mod tests {
#[test] #[test]
fn get_editor_command_from_store_returns_value() { fn get_editor_command_from_store_returns_value() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
ctx.store.set(EDITOR_COMMAND_KEY, json!("code")); ctx.store.set(EDITOR_COMMAND_KEY, json!("code"));
let result = get_editor_command_from_store(&ctx); let result = get_editor_command_from_store(&ctx);
@@ -230,7 +226,7 @@ mod tests {
#[test] #[test]
fn delete_editor_command_returns_none() { fn delete_editor_command_returns_none() {
let dir = TempDir::new().unwrap(); 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.set(EDITOR_COMMAND_KEY, json!("cursor"));
ctx.store.delete(EDITOR_COMMAND_KEY); ctx.store.delete(EDITOR_COMMAND_KEY);
@@ -258,7 +254,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_editor_http_handler_returns_null_when_not_set() { async fn get_editor_http_handler_returns_null_when_not_set() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
let api = SettingsApi { let api = SettingsApi {
ctx: Arc::new(ctx), ctx: Arc::new(ctx),
}; };
@@ -269,7 +265,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_editor_http_handler_stores_value() { async fn set_editor_http_handler_stores_value() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
let api = SettingsApi { let api = SettingsApi {
ctx: Arc::new(ctx), ctx: Arc::new(ctx),
}; };
@@ -286,7 +282,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn set_editor_http_handler_clears_value_when_null() { async fn set_editor_http_handler_clears_value_when_null() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let ctx = test_ctx(&dir); let ctx = test_ctx(dir.path());
let api = SettingsApi { let api = SettingsApi {
ctx: Arc::new(ctx), ctx: Arc::new(ctx),
}; };
@@ -310,7 +306,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn open_file_returns_error_when_no_editor_configured() { async fn open_file_returns_error_when_no_editor_configured() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
let result = api let result = api
.open_file(Query("src/main.rs".to_string()), Query(Some(42))) .open_file(Query("src/main.rs".to_string()), Query(Some(42)))
.await; .await;
@@ -322,7 +318,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn open_file_spawns_editor_with_path_and_line() { async fn open_file_spawns_editor_with_path_and_line() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
// Configure the editor to "echo" which is a safe no-op command // Configure the editor to "echo" which is a safe no-op command
api.set_editor(Json(EditorCommandPayload { api.set_editor(Json(EditorCommandPayload {
editor_command: Some("echo".to_string()), editor_command: Some("echo".to_string()),
@@ -339,7 +335,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn open_file_spawns_editor_with_path_only_when_no_line() { async fn open_file_spawns_editor_with_path_only_when_no_line() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
api.set_editor(Json(EditorCommandPayload { api.set_editor(Json(EditorCommandPayload {
editor_command: Some("echo".to_string()), editor_command: Some("echo".to_string()),
})) }))
@@ -355,7 +351,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn open_file_returns_error_for_nonexistent_editor() { async fn open_file_returns_error_for_nonexistent_editor() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let api = make_api(&dir); let api = make_api::<SettingsApi>(&dir);
api.set_editor(Json(EditorCommandPayload { api.set_editor(Json(EditorCommandPayload {
editor_command: Some("this_editor_does_not_exist_xyz_abc".to_string()), editor_command: Some("this_editor_does_not_exist_xyz_abc".to_string()),
})) }))
+21
View File
@@ -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<Arc<AppContext>>`. Add a
/// `#[cfg(test)]` impl block to each API struct to opt in.
pub(crate) fn make_api<T: From<Arc<AppContext>>>(dir: &TempDir) -> T {
Arc::new(test_ctx(dir.path())).into()
}
+2
View File
@@ -5,3 +5,5 @@ pub mod shell;
pub mod story_metadata; pub mod story_metadata;
pub mod watcher; pub mod watcher;
pub mod wizard; pub mod wizard;
#[cfg(test)]
pub(crate) mod test_helpers;
+1 -8
View File
@@ -74,17 +74,10 @@ fn needs_project_toml(story_kit: &Path) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::io::test_helpers::setup_project;
use std::fs; use std::fs;
use tempfile::TempDir; 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 ────────────────────────────────────────── // ── needs_onboarding ──────────────────────────────────────────
#[test] #[test]
+2 -8
View File
@@ -64,18 +64,12 @@ pub async fn search_files_impl(query: String, root: PathBuf) -> Result<Vec<Searc
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs; use crate::io::test_helpers::create_test_files;
use tempfile::TempDir; use tempfile::TempDir;
fn setup_project(files: &[(&str, &str)]) -> TempDir { fn setup_project(files: &[(&str, &str)]) -> TempDir {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
for (path, content) in files { create_test_files(&dir, files);
let full = dir.path().join(path);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(full, content).unwrap();
}
dir dir
} }
+32
View File
@@ -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();
}
}
+1 -7
View File
@@ -255,15 +255,9 @@ pub fn format_wizard_state(state: &WizardState) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::io::test_helpers::setup_project;
use tempfile::TempDir; 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] #[test]
fn default_state_has_all_steps_pending() { fn default_state_has_all_steps_pending() {
let state = WizardState::default(); let state = WizardState::default();