From 649b4223380d47c61a1422a2449a1faf9367a2f8 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 22:26:46 +0000 Subject: [PATCH] story-kit: merge 108_story_test_coverage_http_agents_rs_to_70 --- server/src/http/agents.rs | 303 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/server/src/http/agents.rs b/server/src/http/agents.rs index 5dda0d8..c1117d5 100644 --- a/server/src/http/agents.rs +++ b/server/src/http/agents.rs @@ -379,4 +379,307 @@ mod tests { let result = api.list_agents().await.unwrap().0; assert!(result.iter().any(|a| a.story_id == "42_story_whatever")); } + + fn make_project_toml(root: &path::Path, content: &str) { + let sk_dir = root.join(".story_kit"); + std::fs::create_dir_all(&sk_dir).unwrap(); + std::fs::write(sk_dir.join("project.toml"), content).unwrap(); + } + + // --- get_agent_config tests --- + + #[tokio::test] + async fn get_agent_config_returns_default_when_no_toml() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.get_agent_config().await.unwrap().0; + // Default config has one agent named "default" + assert_eq!(result.len(), 1); + assert_eq!(result[0].name, "default"); + } + + #[tokio::test] + async fn get_agent_config_returns_configured_agents() { + let tmp = TempDir::new().unwrap(); + make_project_toml( + tmp.path(), + r#" +[[agent]] +name = "coder-1" +role = "Full-stack engineer" +model = "sonnet" +max_turns = 30 +max_budget_usd = 5.0 + +[[agent]] +name = "qa" +role = "QA reviewer" +model = "haiku" +"#, + ); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.get_agent_config().await.unwrap().0; + assert_eq!(result.len(), 2); + assert_eq!(result[0].name, "coder-1"); + assert_eq!(result[0].role, "Full-stack engineer"); + assert_eq!(result[0].model, Some("sonnet".to_string())); + assert_eq!(result[0].max_turns, Some(30)); + assert_eq!(result[0].max_budget_usd, Some(5.0)); + assert_eq!(result[1].name, "qa"); + assert_eq!(result[1].model, Some("haiku".to_string())); + } + + #[tokio::test] + async fn get_agent_config_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.get_agent_config().await; + assert!(result.is_err()); + } + + // --- reload_config tests --- + + #[tokio::test] + async fn reload_config_returns_default_when_no_toml() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.reload_config().await.unwrap().0; + assert_eq!(result.len(), 1); + assert_eq!(result[0].name, "default"); + } + + #[tokio::test] + async fn reload_config_returns_configured_agents() { + let tmp = TempDir::new().unwrap(); + make_project_toml( + tmp.path(), + r#" +[[agent]] +name = "supervisor" +role = "Coordinator" +model = "opus" +allowed_tools = ["Read", "Bash"] +"#, + ); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.reload_config().await.unwrap().0; + assert_eq!(result.len(), 1); + assert_eq!(result[0].name, "supervisor"); + assert_eq!(result[0].role, "Coordinator"); + assert_eq!(result[0].model, Some("opus".to_string())); + assert_eq!( + result[0].allowed_tools, + Some(vec!["Read".to_string(), "Bash".to_string()]) + ); + } + + #[tokio::test] + async fn reload_config_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.reload_config().await; + assert!(result.is_err()); + } + + // --- list_worktrees tests --- + + #[tokio::test] + async fn list_worktrees_returns_empty_when_no_worktree_dir() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.list_worktrees().await.unwrap().0; + assert!(result.is_empty()); + } + + #[tokio::test] + async fn list_worktrees_returns_entries_from_dir() { + let tmp = TempDir::new().unwrap(); + let worktrees_dir = tmp.path().join(".story_kit").join("worktrees"); + std::fs::create_dir_all(worktrees_dir.join("42_story_foo")).unwrap(); + std::fs::create_dir_all(worktrees_dir.join("43_story_bar")).unwrap(); + + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let mut result = api.list_worktrees().await.unwrap().0; + result.sort_by(|a, b| a.story_id.cmp(&b.story_id)); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].story_id, "42_story_foo"); + assert_eq!(result[1].story_id, "43_story_bar"); + } + + #[tokio::test] + async fn list_worktrees_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api.list_worktrees().await; + assert!(result.is_err()); + } + + // --- stop_agent tests --- + + #[tokio::test] + async fn stop_agent_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .stop_agent(Json(StopAgentPayload { + story_id: "42_story_foo".to_string(), + agent_name: "coder-1".to_string(), + })) + .await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn stop_agent_returns_error_when_agent_not_found() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .stop_agent(Json(StopAgentPayload { + story_id: "nonexistent_story".to_string(), + agent_name: "coder-1".to_string(), + })) + .await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn stop_agent_succeeds_with_running_agent() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + ctx.agents + .inject_test_agent("42_story_foo", "coder-1", AgentStatus::Running); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .stop_agent(Json(StopAgentPayload { + story_id: "42_story_foo".to_string(), + agent_name: "coder-1".to_string(), + })) + .await + .unwrap() + .0; + assert!(result); + } + + // --- start_agent error path --- + + #[tokio::test] + async fn start_agent_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .start_agent(Json(StartAgentPayload { + story_id: "42_story_foo".to_string(), + agent_name: None, + })) + .await; + assert!(result.is_err()); + } + + // --- create_worktree error path --- + + #[tokio::test] + async fn create_worktree_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .create_worktree(Json(CreateWorktreePayload { + story_id: "42_story_foo".to_string(), + })) + .await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn create_worktree_returns_error_when_not_a_git_repo() { + let tmp = TempDir::new().unwrap(); + // project_root is set but has no git repo — git worktree add will fail + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .create_worktree(Json(CreateWorktreePayload { + story_id: "42_story_foo".to_string(), + })) + .await; + assert!(result.is_err()); + } + + // --- remove_worktree error paths --- + + #[tokio::test] + async fn remove_worktree_returns_error_when_no_project_root() { + let tmp = TempDir::new().unwrap(); + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + *ctx.state.project_root.lock().unwrap() = None; + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .remove_worktree(Path("42_story_foo".to_string())) + .await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn remove_worktree_returns_error_when_worktree_not_found() { + let tmp = TempDir::new().unwrap(); + // project_root is set but no worktree exists for this story_id + let ctx = AppContext::new_test(tmp.path().to_path_buf()); + let api = AgentsApi { + ctx: Arc::new(ctx), + }; + let result = api + .remove_worktree(Path("nonexistent_story".to_string())) + .await; + assert!(result.is_err()); + } }