refactor: split http/mcp/agent_tools.rs (1094) into mod + worktree
The 1094-line agent_tools.rs is split: - worktree.rs: tool_create/list/remove_worktree, tool_get_editor_command, get_worktree_commits + their tests (~190 lines) - mod.rs: agent lifecycle tools (start/stop/list/output/config/wait/ remaining_turns_and_budget/read_coverage helper) + their tests Tests stay co-located. All 2636 tests pass; clippy clean.
This commit is contained in:
@@ -1,13 +1,19 @@
|
|||||||
//! MCP agent tools — start, stop, wait, list, and inspect agents via MCP.
|
//! MCP agent tools — start, stop, wait, list, and inspect agents via MCP.
|
||||||
|
|
||||||
use crate::agents::PipelineStage;
|
use crate::agents::PipelineStage;
|
||||||
use crate::config::ProjectConfig;
|
use crate::config::ProjectConfig;
|
||||||
use crate::http::context::AppContext;
|
use crate::http::context::AppContext;
|
||||||
use crate::service::settings::get_editor_command;
|
|
||||||
use crate::slog_warn;
|
use crate::slog_warn;
|
||||||
use crate::worktree;
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
pub(super) async fn tool_start_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
mod worktree;
|
||||||
|
|
||||||
|
pub(crate) use worktree::{
|
||||||
|
get_worktree_commits, tool_create_worktree, tool_get_editor_command, tool_list_worktrees,
|
||||||
|
tool_remove_worktree,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) async fn tool_start_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let story_id = args
|
let story_id = args
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
@@ -45,8 +51,7 @@ pub(super) async fn tool_start_agent(args: &Value, ctx: &AppContext) -> Result<S
|
|||||||
/// Try to read the overall line coverage percentage from the llvm-cov JSON report.
|
/// Try to read the overall line coverage percentage from the llvm-cov JSON report.
|
||||||
///
|
///
|
||||||
/// Expects the file at `{project_root}/.huskies/coverage/server.json`.
|
/// Expects the file at `{project_root}/.huskies/coverage/server.json`.
|
||||||
/// Returns `None` if the file is absent, unreadable, or cannot be parsed.
|
pub(crate) fn read_coverage_percent_from_json(project_root: &std::path::Path) -> Option<f64> {
|
||||||
pub(super) fn read_coverage_percent_from_json(project_root: &std::path::Path) -> Option<f64> {
|
|
||||||
let path = project_root
|
let path = project_root
|
||||||
.join(".huskies")
|
.join(".huskies")
|
||||||
.join("coverage")
|
.join("coverage")
|
||||||
@@ -58,7 +63,7 @@ pub(super) fn read_coverage_percent_from_json(project_root: &std::path::Path) ->
|
|||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn tool_stop_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
pub(crate) async fn tool_stop_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let story_id = args
|
let story_id = args
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
@@ -79,7 +84,7 @@ pub(super) async fn tool_stop_agent(args: &Value, ctx: &AppContext) -> Result<St
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
|
pub(crate) fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
|
||||||
let project_root = ctx.services.agents.get_project_root(&ctx.state).ok();
|
let project_root = ctx.services.agents.get_project_root(&ctx.state).ok();
|
||||||
let agents = ctx.services.agents.list_agents()?;
|
let agents = ctx.services.agents.list_agents()?;
|
||||||
serde_json::to_string_pretty(&json!(
|
serde_json::to_string_pretty(&json!(
|
||||||
@@ -109,8 +114,7 @@ pub(super) fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
|
|||||||
/// order. If `agent_name` is omitted, logs from every agent are included.
|
/// order. If `agent_name` is omitted, logs from every agent are included.
|
||||||
/// Supports `lines` (tail the last N lines) and `filter` (substring match).
|
/// Supports `lines` (tail the last N lines) and `filter` (substring match).
|
||||||
/// If a named agent is currently running, its buffered in-memory events are
|
/// If a named agent is currently running, its buffered in-memory events are
|
||||||
/// appended as "live" output so the caller sees everything in one call.
|
pub(crate) async fn tool_get_agent_output(
|
||||||
pub(super) async fn tool_get_agent_output(
|
|
||||||
args: &Value,
|
args: &Value,
|
||||||
ctx: &AppContext,
|
ctx: &AppContext,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
@@ -196,7 +200,7 @@ pub(super) async fn tool_get_agent_output(
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String> {
|
pub(crate) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String> {
|
||||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||||
let config = ProjectConfig::load(&project_root)?;
|
let config = ProjectConfig::load(&project_root)?;
|
||||||
|
|
||||||
@@ -240,8 +244,7 @@ pub(super) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String>
|
|||||||
///
|
///
|
||||||
/// Returns turns used, max turns, remaining turns, budget used, max budget,
|
/// Returns turns used, max turns, remaining turns, budget used, max budget,
|
||||||
/// and remaining budget for the named agent. Fails if the agent is not
|
/// and remaining budget for the named agent. Fails if the agent is not
|
||||||
/// currently running or pending.
|
pub(crate) fn tool_get_agent_remaining_turns_and_budget(
|
||||||
pub(super) fn tool_get_agent_remaining_turns_and_budget(
|
|
||||||
args: &Value,
|
args: &Value,
|
||||||
ctx: &AppContext,
|
ctx: &AppContext,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
@@ -361,7 +364,7 @@ pub(super) fn tool_get_agent_remaining_turns_and_budget(
|
|||||||
.map_err(|e| format!("Serialization error: {e}"))
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn tool_wait_for_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
pub(crate) async fn tool_wait_for_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let story_id = args
|
let story_id = args
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
@@ -407,106 +410,10 @@ pub(super) async fn tool_wait_for_agent(args: &Value, ctx: &AppContext) -> Resul
|
|||||||
.map_err(|e| format!("Serialization error: {e}"))
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn tool_create_worktree(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
|
||||||
let story_id = args
|
|
||||||
.get("story_id")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or("Missing required argument: story_id")?;
|
|
||||||
|
|
||||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
|
||||||
let info = ctx
|
|
||||||
.services
|
|
||||||
.agents
|
|
||||||
.create_worktree(&project_root, story_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!({
|
|
||||||
"story_id": story_id,
|
|
||||||
"worktree_path": info.path.to_string_lossy(),
|
|
||||||
"branch": info.branch,
|
|
||||||
"base_branch": info.base_branch,
|
|
||||||
}))
|
|
||||||
.map_err(|e| format!("Serialization error: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn tool_list_worktrees(ctx: &AppContext) -> Result<String, String> {
|
|
||||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
|
||||||
let entries = worktree::list_worktrees(&project_root)?;
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&json!(
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.map(|e| json!({
|
|
||||||
"story_id": e.story_id,
|
|
||||||
"path": e.path.to_string_lossy(),
|
|
||||||
}))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
))
|
|
||||||
.map_err(|e| format!("Serialization error: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn tool_remove_worktree(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
|
||||||
let story_id = args
|
|
||||||
.get("story_id")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or("Missing required argument: story_id")?;
|
|
||||||
|
|
||||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
|
||||||
let config = ProjectConfig::load(&project_root)?;
|
|
||||||
worktree::remove_worktree_by_story_id(&project_root, story_id, &config).await?;
|
|
||||||
|
|
||||||
Ok(format!("Worktree for story '{story_id}' removed."))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn tool_get_editor_command(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
|
||||||
let worktree_path = args
|
|
||||||
.get("worktree_path")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or("Missing required argument: worktree_path")?;
|
|
||||||
|
|
||||||
let editor = get_editor_command(&*ctx.store)
|
|
||||||
.ok_or_else(|| "No editor configured. Set one via PUT /api/settings/editor.".to_string())?;
|
|
||||||
|
|
||||||
Ok(format!("{editor} {worktree_path}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run `git log <base>..HEAD --oneline` in the worktree and return the commit
|
|
||||||
/// summaries, or `None` if git is unavailable or there are no new commits.
|
|
||||||
pub(super) async fn get_worktree_commits(
|
|
||||||
worktree_path: &str,
|
|
||||||
base_branch: &str,
|
|
||||||
) -> Option<Vec<String>> {
|
|
||||||
let wt = worktree_path.to_string();
|
|
||||||
let base = base_branch.to_string();
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
let output = std::process::Command::new("git")
|
|
||||||
.args(["log", &format!("{base}..HEAD"), "--oneline"])
|
|
||||||
.current_dir(&wt)
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let lines: Vec<String> = String::from_utf8(output.stdout)
|
|
||||||
.ok()?
|
|
||||||
.lines()
|
|
||||||
.filter(|l| !l.is_empty())
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect();
|
|
||||||
Some(lines)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::http::test_helpers::test_ctx;
|
use crate::http::test_helpers::test_ctx;
|
||||||
use crate::store::StoreOps;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_list_agents_empty() {
|
fn tool_list_agents_empty() {
|
||||||
@@ -794,92 +701,6 @@ stage = "coder"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tool_create_worktree_missing_story_id() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_create_worktree(&json!({}), &ctx).await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("story_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tool_remove_worktree_missing_story_id() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_remove_worktree(&json!({}), &ctx).await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("story_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tool_list_worktrees_empty_dir() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_list_worktrees(&ctx).unwrap();
|
|
||||||
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
|
||||||
assert!(parsed.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Editor command tool tests ─────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tool_get_editor_command_missing_worktree_path() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_get_editor_command(&json!({}), &ctx);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("worktree_path"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tool_get_editor_command_no_editor_configured() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
let result = tool_get_editor_command(&json!({"worktree_path": "/some/path"}), &ctx);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("No editor configured"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tool_get_editor_command_formats_correctly() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
ctx.store.set("editor_command", json!("zed"));
|
|
||||||
|
|
||||||
let result = tool_get_editor_command(
|
|
||||||
&json!({"worktree_path": "/home/user/worktrees/37_my_story"}),
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(result, "zed /home/user/worktrees/37_my_story");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tool_get_editor_command_works_with_vscode() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let ctx = test_ctx(tmp.path());
|
|
||||||
ctx.store.set("editor_command", json!("code"));
|
|
||||||
|
|
||||||
let result =
|
|
||||||
tool_get_editor_command(&json!({"worktree_path": "/path/to/worktree"}), &ctx).unwrap();
|
|
||||||
assert_eq!(result, "code /path/to/worktree");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_editor_command_in_tools_list() {
|
|
||||||
use super::super::handle_tools_list;
|
|
||||||
let resp = handle_tools_list(Some(json!(1)));
|
|
||||||
let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
|
|
||||||
let tool = tools.iter().find(|t| t["name"] == "get_editor_command");
|
|
||||||
assert!(tool.is_some(), "get_editor_command missing from tools list");
|
|
||||||
let t = tool.unwrap();
|
|
||||||
assert!(t["description"].is_string());
|
|
||||||
let required = t["inputSchema"]["required"].as_array().unwrap();
|
|
||||||
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
|
||||||
assert!(req_names.contains(&"worktree_path"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn wait_for_agent_tool_missing_story_id() {
|
async fn wait_for_agent_tool_missing_story_id() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
//! MCP worktree tools — create, list, remove, get editor command, and read worktree commits.
|
||||||
|
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::http::context::AppContext;
|
||||||
|
use crate::service::settings::get_editor_command;
|
||||||
|
use crate::config::ProjectConfig;
|
||||||
|
use crate::worktree;
|
||||||
|
|
||||||
|
pub(crate) async fn tool_create_worktree(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
|
let story_id = args
|
||||||
|
.get("story_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("Missing required argument: story_id")?;
|
||||||
|
|
||||||
|
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||||
|
let info = ctx
|
||||||
|
.services
|
||||||
|
.agents
|
||||||
|
.create_worktree(&project_root, story_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
serde_json::to_string_pretty(&json!({
|
||||||
|
"story_id": story_id,
|
||||||
|
"worktree_path": info.path.to_string_lossy(),
|
||||||
|
"branch": info.branch,
|
||||||
|
"base_branch": info.base_branch,
|
||||||
|
}))
|
||||||
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tool_list_worktrees(ctx: &AppContext) -> Result<String, String> {
|
||||||
|
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||||
|
let entries = worktree::list_worktrees(&project_root)?;
|
||||||
|
|
||||||
|
serde_json::to_string_pretty(&json!(
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| json!({
|
||||||
|
"story_id": e.story_id,
|
||||||
|
"path": e.path.to_string_lossy(),
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
))
|
||||||
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn tool_remove_worktree(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
|
let story_id = args
|
||||||
|
.get("story_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("Missing required argument: story_id")?;
|
||||||
|
|
||||||
|
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||||
|
let config = ProjectConfig::load(&project_root)?;
|
||||||
|
worktree::remove_worktree_by_story_id(&project_root, story_id, &config).await?;
|
||||||
|
|
||||||
|
Ok(format!("Worktree for story '{story_id}' removed."))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tool_get_editor_command(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
|
let worktree_path = args
|
||||||
|
.get("worktree_path")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("Missing required argument: worktree_path")?;
|
||||||
|
|
||||||
|
let editor = get_editor_command(&*ctx.store)
|
||||||
|
.ok_or_else(|| "No editor configured. Set one via PUT /api/settings/editor.".to_string())?;
|
||||||
|
|
||||||
|
Ok(format!("{editor} {worktree_path}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `git log <base>..HEAD --oneline` in the worktree and return the commit
|
||||||
|
pub(crate) async fn get_worktree_commits(
|
||||||
|
worktree_path: &str,
|
||||||
|
base_branch: &str,
|
||||||
|
) -> Option<Vec<String>> {
|
||||||
|
let wt = worktree_path.to_string();
|
||||||
|
let base = base_branch.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(["log", &format!("{base}..HEAD"), "--oneline"])
|
||||||
|
.current_dir(&wt)
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let lines: Vec<String> = String::from_utf8(output.stdout)
|
||||||
|
.ok()?
|
||||||
|
.lines()
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.map(|l| l.to_string())
|
||||||
|
.collect();
|
||||||
|
Some(lines)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::http::test_helpers::test_ctx;
|
||||||
|
use crate::store::StoreOps;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_create_worktree_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_create_worktree(&json!({}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_remove_worktree_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_remove_worktree(&json!({}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_list_worktrees_empty_dir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_list_worktrees(&ctx).unwrap();
|
||||||
|
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
||||||
|
assert!(parsed.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Editor command tool tests ─────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_editor_command_missing_worktree_path() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_get_editor_command(&json!({}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("worktree_path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_editor_command_no_editor_configured() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_get_editor_command(&json!({"worktree_path": "/some/path"}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("No editor configured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_editor_command_formats_correctly() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
ctx.store.set("editor_command", json!("zed"));
|
||||||
|
|
||||||
|
let result = tool_get_editor_command(
|
||||||
|
&json!({"worktree_path": "/home/user/worktrees/37_my_story"}),
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result, "zed /home/user/worktrees/37_my_story");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_editor_command_works_with_vscode() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
ctx.store.set("editor_command", json!("code"));
|
||||||
|
|
||||||
|
let result =
|
||||||
|
tool_get_editor_command(&json!({"worktree_path": "/path/to/worktree"}), &ctx).unwrap();
|
||||||
|
assert_eq!(result, "code /path/to/worktree");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_editor_command_in_tools_list() {
|
||||||
|
use super::super::super::tools_list::handle_tools_list;
|
||||||
|
let resp = handle_tools_list(Some(json!(1)));
|
||||||
|
let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
|
||||||
|
let tool = tools.iter().find(|t| t["name"] == "get_editor_command");
|
||||||
|
assert!(tool.is_some(), "get_editor_command missing from tools list");
|
||||||
|
let t = tool.unwrap();
|
||||||
|
assert!(t["description"].is_string());
|
||||||
|
let required = t["inputSchema"]["required"].as_array().unwrap();
|
||||||
|
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
||||||
|
assert!(req_names.contains(&"worktree_path"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user