883 lines
32 KiB
Rust
883 lines
32 KiB
Rust
use crate::models::*;
|
|
use crate::router::McpTool;
|
|
use crate::state::MemoryState;
|
|
use crate::tools::*;
|
|
use async_trait::async_trait;
|
|
use serde_json::Value;
|
|
use std::sync::Arc;
|
|
|
|
pub struct AddTaskHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for AddTaskHandler {
|
|
fn name(&self) -> &'static str {
|
|
"add_task"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<AddTaskTool>("add_task", "Execute add_task")
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let now = crate::handlers::utils::now_secs();
|
|
let task_id = uuid::Uuid::new_v4().to_string();
|
|
|
|
let deps = req.dependencies.unwrap_or_default();
|
|
|
|
let task = Task {
|
|
id: task_id.clone(),
|
|
title: req.title,
|
|
status: "pending".to_string(),
|
|
description: req.description,
|
|
created_at: now,
|
|
updated_at: now,
|
|
git_branch: req.git_branch,
|
|
parent_id: req.parent_id,
|
|
dependencies: deps,
|
|
acceptance_criteria: vec![],
|
|
expires_at: None,
|
|
};
|
|
let idx = state.get_search_index();
|
|
drop(idx.index_task(&task));
|
|
state.project.tasks.modify(|tasks| {
|
|
tasks.push(task.clone());
|
|
});
|
|
state.record_activity("task_create", &format!("Created task: {}", task.title), Some(&task.description));
|
|
Ok(format!("Task added with ID: {}", task_id).to_string())
|
|
}
|
|
}
|
|
|
|
pub struct DeleteTaskHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for DeleteTaskHandler {
|
|
fn name(&self) -> &'static str {
|
|
"delete_task"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<DeleteTaskTool>("delete_task", "Execute delete_task")
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: DeleteTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let mut deleted_count = 0;
|
|
let mut actually_deleted = Vec::new();
|
|
state.project.tasks.modify(|tasks| {
|
|
let initial_len = tasks.len();
|
|
|
|
// Build index-based children map
|
|
let mut children_map: std::collections::HashMap<usize, Vec<usize>> =
|
|
std::collections::HashMap::new();
|
|
let mut id_to_index = std::collections::HashMap::new();
|
|
for (idx, t) in tasks.iter().enumerate() {
|
|
id_to_index.insert(t.id.as_str(), idx);
|
|
}
|
|
|
|
for (idx, t) in tasks.iter().enumerate() {
|
|
if let Some(pid) = &t.parent_id
|
|
&& let Some(&parent_idx) = id_to_index.get(pid.as_str())
|
|
{
|
|
children_map.entry(parent_idx).or_default().push(idx);
|
|
}
|
|
}
|
|
|
|
let mut to_delete_idx = std::collections::HashSet::new();
|
|
if let Some(&start_idx) = id_to_index.get(req.id.as_str()) {
|
|
let mut queue = std::collections::VecDeque::new();
|
|
queue.push_back(start_idx);
|
|
|
|
while let Some(curr) = queue.pop_front() {
|
|
if to_delete_idx.insert(curr)
|
|
&& let Some(children) = children_map.get(&curr)
|
|
{
|
|
queue.extend(children.iter().copied());
|
|
}
|
|
}
|
|
}
|
|
|
|
for &idx in &to_delete_idx {
|
|
actually_deleted.push(tasks[idx].id.clone());
|
|
}
|
|
|
|
tasks.retain(|t| !actually_deleted.contains(&t.id));
|
|
deleted_count = initial_len - tasks.len();
|
|
});
|
|
|
|
if deleted_count > 0 {
|
|
let idx = state.get_search_index();
|
|
for id in actually_deleted {
|
|
drop(idx.delete_document(&id));
|
|
}
|
|
Ok(vec![
|
|
format!("Deleted task and its children ({} total).", deleted_count).to_string(),
|
|
][0]
|
|
.clone())
|
|
} else {
|
|
Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct UpdateTaskStatusHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for UpdateTaskStatusHandler {
|
|
fn name(&self) -> &'static str {
|
|
"update_task_status"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<UpdateTaskStatusTool>(
|
|
"update_task_status",
|
|
"Execute update_task_status",
|
|
)
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: UpdateTaskStatusTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let mut found = false;
|
|
let mut blocked = false;
|
|
let mut blocker_details = String::new();
|
|
let target_status = req.status.to_lowercase();
|
|
|
|
state.project.tasks.modify(|tasks| {
|
|
// Find target task
|
|
let target_idx = tasks
|
|
.iter()
|
|
.position(|t| t.id == req.id || t.title == req.id);
|
|
let target_idx = match target_idx {
|
|
Some(idx) => idx,
|
|
None => return,
|
|
};
|
|
|
|
found = true;
|
|
|
|
if target_status == "done" || target_status == "completed" {
|
|
// 1. Check Acceptance Criteria
|
|
if tasks[target_idx]
|
|
.acceptance_criteria
|
|
.iter()
|
|
.any(|c| !c.is_met)
|
|
{
|
|
blocked = true;
|
|
blocker_details = "Unmet acceptance criteria exist.".to_string();
|
|
}
|
|
|
|
// 2. Check dependencies
|
|
if !blocked {
|
|
let mut uncompleted_deps = Vec::new();
|
|
for dep_id in &tasks[target_idx].dependencies {
|
|
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
|
&& dep_task.status != "completed"
|
|
&& dep_task.status != "done"
|
|
{
|
|
uncompleted_deps.push(dep_task.title.as_str());
|
|
}
|
|
}
|
|
if !uncompleted_deps.is_empty() {
|
|
blocked = true;
|
|
blocker_details =
|
|
format!("Blocked by dependencies: {}", uncompleted_deps.join(", "));
|
|
}
|
|
}
|
|
|
|
// 3. Check child tasks
|
|
if !blocked {
|
|
let target_id_ref = tasks[target_idx].id.as_str();
|
|
let mut uncompleted_children = Vec::new();
|
|
for child in tasks
|
|
.iter()
|
|
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
|
|
{
|
|
if child.status != "completed" && child.status != "done" {
|
|
uncompleted_children.push(child.title.as_str());
|
|
}
|
|
}
|
|
if !uncompleted_children.is_empty() {
|
|
blocked = true;
|
|
blocker_details = format!(
|
|
"Blocked by child tasks: {}",
|
|
uncompleted_children.join(", ")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !blocked {
|
|
// Apply update
|
|
tasks[target_idx].status = target_status.clone();
|
|
tasks[target_idx].updated_at = crate::handlers::utils::now_secs();
|
|
|
|
// Cascade cancellation to children
|
|
if target_status == "cancelled" || target_status == "abandoned" {
|
|
let mut children_map: std::collections::HashMap<usize, Vec<usize>> =
|
|
std::collections::HashMap::new();
|
|
|
|
// First pass: map string ID to index to build the adjacency list by index
|
|
let mut id_to_idx = std::collections::HashMap::new();
|
|
for (idx, t) in tasks.iter().enumerate() {
|
|
id_to_idx.insert(t.id.as_str(), idx);
|
|
}
|
|
|
|
for (idx, t) in tasks.iter().enumerate() {
|
|
if let Some(pid) = &t.parent_id
|
|
&& let Some(&p_idx) = id_to_idx.get(pid.as_str())
|
|
{
|
|
children_map.entry(p_idx).or_default().push(idx);
|
|
}
|
|
}
|
|
|
|
if let Some(&start_idx) = id_to_idx.get(tasks[target_idx].id.as_str()) {
|
|
let mut queue = std::collections::VecDeque::new();
|
|
queue.push_back(start_idx);
|
|
|
|
while let Some(curr) = queue.pop_front() {
|
|
if let Some(child_indices) = children_map.get(&curr) {
|
|
for &idx in child_indices {
|
|
if tasks[idx].status != "completed"
|
|
&& tasks[idx].status != target_status
|
|
{
|
|
tasks[idx].status = target_status.clone();
|
|
queue.push_back(idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if blocked {
|
|
Err(crate::error::AppError::Internal(format!(
|
|
"Error: Cannot transition task. {}",
|
|
blocker_details
|
|
)))
|
|
} else if found {
|
|
state.record_activity("task_update", &format!("Task {} status -> {}", req.id, req.status), None);
|
|
Ok("Task status updated.".to_string())
|
|
} else {
|
|
Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ListActiveTasksHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for ListActiveTasksHandler {
|
|
fn name(&self) -> &'static str {
|
|
"list_active_tasks"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<ListActiveTasksTool>(
|
|
"list_active_tasks",
|
|
"Execute list_active_tasks",
|
|
)
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let level = req.summary_level.as_deref().unwrap_or("detailed");
|
|
let data = state.project.tasks.read_with(|tasks| {
|
|
let filtered: Vec<_> = tasks
|
|
.iter()
|
|
.filter(|t| {
|
|
let status_match = t.status != "done" && t.status != "completed";
|
|
let branch_match = match &req.git_branch {
|
|
Some(branch) => {
|
|
t.git_branch.is_none()
|
|
|| t.git_branch.as_deref() == Some(branch.as_str())
|
|
}
|
|
None => true,
|
|
};
|
|
status_match && branch_match
|
|
})
|
|
.map(|t| match level {
|
|
"compact" => serde_json::json!({
|
|
"id": t.id,
|
|
"title": t.title,
|
|
"status": t.status,
|
|
}),
|
|
"full" => serde_json::to_value(t).unwrap_or_default(),
|
|
_ => serde_json::json!({
|
|
"id": t.id,
|
|
"title": t.title,
|
|
"status": t.status,
|
|
"description": t.description,
|
|
"git_branch": t.git_branch,
|
|
}),
|
|
})
|
|
.collect();
|
|
let mut json_str = serde_json::to_string(&filtered)?;
|
|
if let Some(max_t) = req.max_tokens {
|
|
let char_limit = max_t * 4;
|
|
if json_str.len() > char_limit {
|
|
json_str.truncate(char_limit);
|
|
json_str.push_str(" ...[truncated due to max_tokens]");
|
|
}
|
|
}
|
|
Ok::<String, crate::error::AppError>(json_str)
|
|
})?;
|
|
Ok(data)
|
|
}
|
|
}
|
|
|
|
pub struct SetAcceptanceCriteriaHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for SetAcceptanceCriteriaHandler {
|
|
fn name(&self) -> &'static str {
|
|
"set_acceptance_criteria"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<SetAcceptanceCriteriaTool>(
|
|
"set_acceptance_criteria",
|
|
"Execute set_acceptance_criteria",
|
|
)
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: SetAcceptanceCriteriaTool =
|
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let mut success = false;
|
|
state.project.tasks.modify(|tasks| {
|
|
if let Some(task) = tasks.iter_mut().rev().find(|t| t.title == req.task_title) {
|
|
task.acceptance_criteria = req
|
|
.criteria
|
|
.into_iter()
|
|
.map(|desc| crate::models::AcceptanceCriteria {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
description: desc,
|
|
is_met: false,
|
|
})
|
|
.collect();
|
|
task.updated_at = crate::handlers::utils::now_secs();
|
|
success = true;
|
|
}
|
|
});
|
|
if success {
|
|
Ok("Acceptance criteria set successfully.".to_string())
|
|
} else {
|
|
Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct VerifyAcceptanceCriteriaHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for VerifyAcceptanceCriteriaHandler {
|
|
fn name(&self) -> &'static str {
|
|
"verify_acceptance_criteria"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<VerifyAcceptanceCriteriaTool>(
|
|
"verify_acceptance_criteria",
|
|
"Execute verify_acceptance_criteria",
|
|
)
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: VerifyAcceptanceCriteriaTool =
|
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let mut success = false;
|
|
let mut already_met = false;
|
|
state.project.tasks.modify(|tasks| {
|
|
if let Some(task) = tasks.iter_mut().find(|t| t.id == req.task_id)
|
|
&& let Some(ac) = task
|
|
.acceptance_criteria
|
|
.iter_mut()
|
|
.find(|c| c.id == req.criteria || c.description == req.criteria)
|
|
{
|
|
if ac.is_met {
|
|
already_met = true;
|
|
} else {
|
|
ac.is_met = true;
|
|
success = true;
|
|
task.updated_at = crate::handlers::utils::now_secs();
|
|
}
|
|
}
|
|
});
|
|
if success {
|
|
Ok(format!(
|
|
"Acceptance criteria verified with proof: {}",
|
|
req.proof
|
|
))
|
|
} else if already_met {
|
|
Ok("Acceptance criteria was already met.".to_string())
|
|
} else {
|
|
Err(crate::error::AppError::Internal(
|
|
"Acceptance criteria or task not found.".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct AddMilestoneHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for AddMilestoneHandler {
|
|
fn name(&self) -> &'static str {
|
|
"add_milestone"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<AddMilestoneTool>("add_milestone", "Execute add_milestone")
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: AddMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
state.project.milestones.modify(|ms| {
|
|
ms.push(crate::models::Milestone {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
title: req.title,
|
|
status: "pending".to_string(),
|
|
namespace: req.namespace,
|
|
target_date: None,
|
|
})
|
|
});
|
|
Ok("Milestone added".to_string())
|
|
}
|
|
}
|
|
|
|
pub struct UpdateMilestoneHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for UpdateMilestoneHandler {
|
|
fn name(&self) -> &'static str {
|
|
"update_milestone"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<UpdateMilestoneTool>("update_milestone", "Execute update_milestone")
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: UpdateMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let mut found = false;
|
|
state.project.milestones.modify(|ms| {
|
|
for m in ms.iter_mut() {
|
|
if m.id == req.id {
|
|
m.status = req.status.clone();
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
if found {
|
|
Ok("Milestone updated".to_string())
|
|
} else {
|
|
Err(crate::error::AppError::Internal(
|
|
"Milestone not found. Please verify the milestone ID using list_milestones."
|
|
.to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ListMilestonesHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for ListMilestonesHandler {
|
|
fn name(&self) -> &'static str {
|
|
"list_milestones"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<ListMilestonesTool>("list_milestones", "Execute list_milestones")
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
let data = state.project.milestones.read_with(|items| {
|
|
let filtered: Vec<_> = items
|
|
.iter()
|
|
.filter(|i| {
|
|
if let Some(ns) = &req.namespace {
|
|
&i.namespace == ns
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.collect();
|
|
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
|
})?;
|
|
Ok(data)
|
|
}
|
|
}
|
|
|
|
pub struct TasksHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for TasksHandler {
|
|
fn name(&self) -> &'static str {
|
|
"tasks"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<TasksTool>(
|
|
"tasks",
|
|
"Consolidated task management (add, update, delete, list, set_criteria, verify)",
|
|
)
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
match req.action {
|
|
TaskAction::Add => {
|
|
let title = req.title.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
|
|
})?;
|
|
let description = req.description.unwrap_or_default();
|
|
let add_args = serde_json::json!({
|
|
"title": title,
|
|
"description": description,
|
|
"git_branch": req.git_branch,
|
|
"parent_id": req.parent_id,
|
|
"dependencies": req.dependencies,
|
|
});
|
|
AddTaskHandler.execute(add_args, state).await
|
|
}
|
|
TaskAction::Update => {
|
|
let id = req.id.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
})?;
|
|
let status = req.status.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'completed', or 'cancelled') in request and retry.".to_string())
|
|
})?;
|
|
let update_args = serde_json::json!({
|
|
"id": id,
|
|
"status": status,
|
|
});
|
|
UpdateTaskStatusHandler.execute(update_args, state).await
|
|
}
|
|
TaskAction::Delete => {
|
|
let id = req.id.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
})?;
|
|
let del_args = serde_json::json!({
|
|
"id": id,
|
|
});
|
|
DeleteTaskHandler.execute(del_args, state).await
|
|
}
|
|
TaskAction::List => {
|
|
let list_args = serde_json::json!({
|
|
"git_branch": req.git_branch,
|
|
"summary_level": req.summary_level,
|
|
"max_tokens": req.max_tokens,
|
|
});
|
|
ListActiveTasksHandler.execute(list_args, state).await
|
|
}
|
|
TaskAction::SetCriteria => {
|
|
let id = req.id.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'id' for action 'set_criteria'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
})?;
|
|
let criteria = req.criteria.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria descriptions in request and retry.".to_string())
|
|
})?;
|
|
let set_args = serde_json::json!({
|
|
"id": id,
|
|
"acceptance_criteria": criteria,
|
|
});
|
|
SetAcceptanceCriteriaHandler.execute(set_args, state).await
|
|
}
|
|
TaskAction::Verify => {
|
|
let id = req.id.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'id' for action 'verify'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
})?;
|
|
let verify_args = serde_json::json!({
|
|
"id": id,
|
|
});
|
|
VerifyAcceptanceCriteriaHandler.execute(verify_args, state).await
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct MilestonesHandler;
|
|
|
|
#[async_trait]
|
|
impl McpTool for MilestonesHandler {
|
|
fn name(&self) -> &'static str {
|
|
"milestones"
|
|
}
|
|
|
|
fn schema(&self) -> Value {
|
|
crate::mcp::tool_def::<MilestonesTool>(
|
|
"milestones",
|
|
"Consolidated milestone management (add, update, list)",
|
|
)
|
|
}
|
|
|
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
match req.action {
|
|
MilestoneAction::Add => {
|
|
let title = req.title.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
|
|
})?;
|
|
let add_args = serde_json::json!({
|
|
"title": title,
|
|
"namespace": req.namespace.unwrap_or_else(|| crate::models::default_namespace()),
|
|
});
|
|
AddMilestoneHandler.execute(add_args, state).await
|
|
}
|
|
MilestoneAction::Update => {
|
|
let id = req.id.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide milestone 'id' string in request and retry.".to_string())
|
|
})?;
|
|
let status = req.status.ok_or_else(|| {
|
|
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide milestone 'status' in request and retry.".to_string())
|
|
})?;
|
|
let update_args = serde_json::json!({
|
|
"id": id,
|
|
"status": status,
|
|
});
|
|
UpdateMilestoneHandler.execute(update_args, state).await
|
|
}
|
|
MilestoneAction::List => {
|
|
let list_args = serde_json::json!({
|
|
"namespace": req.namespace,
|
|
});
|
|
ListMilestonesHandler.execute(list_args, state).await
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
use tempfile::tempdir;
|
|
|
|
#[tokio::test]
|
|
async fn test_add_task_and_list() {
|
|
let dir = tempdir().unwrap();
|
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
|
|
let add_handler = AddTaskHandler;
|
|
let args = json!({
|
|
"title": "Fix the hyperdrive",
|
|
"description": "It's making a strange noise",
|
|
"acceptance_criteria": ["Stop the noise", "Reach lightspeed"],
|
|
});
|
|
|
|
let res = add_handler
|
|
.execute(args, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res.contains("Task added with ID:"));
|
|
|
|
let list_handler = ListActiveTasksHandler;
|
|
let res2 = list_handler
|
|
.execute(json!({}), state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res2.contains("Fix the hyperdrive"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_update_task_status() {
|
|
let dir = tempdir().unwrap();
|
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
|
|
let add_handler = AddTaskHandler;
|
|
let res = add_handler
|
|
.execute(
|
|
json!({"title": "Test", "description": "test"}),
|
|
state.clone(),
|
|
)
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
|
|
let id_start = res.find("ID: ").unwrap() + 4;
|
|
let task_id = res[id_start..].trim();
|
|
|
|
let update_handler = UpdateTaskStatusHandler;
|
|
let args = json!({
|
|
"id": task_id,
|
|
"status": "done"
|
|
});
|
|
let res3 = update_handler
|
|
.execute(args, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert_eq!(res3, "Task status updated.");
|
|
|
|
let list_handler = ListActiveTasksHandler;
|
|
let res4 = list_handler
|
|
.execute(json!({}), state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(!res4.contains(task_id));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_milestones_and_criteria() {
|
|
let dir = tempdir().unwrap();
|
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
|
|
// Add Milestone
|
|
let add_milestone = AddMilestoneHandler;
|
|
let args_ms = json!({
|
|
"name": "v1.0",
|
|
"title": "Release 1.0",
|
|
"description": "First release",
|
|
"target_date": 1700000000,
|
|
"end_date": 1700000000,
|
|
"namespace": "global"
|
|
});
|
|
let res1 = add_milestone
|
|
.execute(args_ms, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res1.contains("Milestone added"));
|
|
|
|
// Fetch milestone ID from state directly to update
|
|
let ms_id = state.project.milestones.read_with(|ms| ms[0].id.clone());
|
|
|
|
// Update Milestone
|
|
let update_ms = UpdateMilestoneHandler;
|
|
let args_ums = json!({
|
|
"id": ms_id,
|
|
"status": "completed"
|
|
});
|
|
let res2 = update_ms
|
|
.execute(args_ums, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert_eq!(res2, "Milestone updated");
|
|
|
|
// List Milestones
|
|
let list_ms = ListMilestonesHandler;
|
|
let res3 = list_ms
|
|
.execute(json!({"namespace": "global"}), state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res3.contains("completed"));
|
|
assert!(res3.contains("Release 1.0"));
|
|
|
|
// Task Acceptance Criteria
|
|
let add_task = AddTaskHandler;
|
|
let res_task = add_task
|
|
.execute(
|
|
json!({"title": "Test", "description": "desc"}),
|
|
state.clone(),
|
|
)
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
let task_id = res_task[res_task.find("ID: ").unwrap() + 4..].trim();
|
|
|
|
let set_ac = SetAcceptanceCriteriaHandler;
|
|
let args_ac = json!({
|
|
"task_id": task_id,
|
|
"task_title": "Test",
|
|
"criteria": ["Do X", "Do Y"]
|
|
});
|
|
let res4 = set_ac
|
|
.execute(args_ac, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert_eq!(res4, "Acceptance criteria set successfully.");
|
|
|
|
let verify_ac = VerifyAcceptanceCriteriaHandler;
|
|
let args_vac = json!({
|
|
"task_id": task_id,
|
|
"criteria": "Do X",
|
|
"proof": "I did X"
|
|
});
|
|
let res5 = verify_ac
|
|
.execute(args_vac, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res5.contains("Acceptance criteria verified"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_task_with_children() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
|
|
let add_task = AddTaskHandler;
|
|
let parent = add_task
|
|
.execute(
|
|
json!({"title": "Parent", "description": "p"}),
|
|
state.clone(),
|
|
)
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
let parent_id = parent[parent.find("ID: ").unwrap() + 4..]
|
|
.trim()
|
|
.to_string();
|
|
|
|
let child = add_task
|
|
.execute(
|
|
json!({"title": "Child", "description": "c", "parent_id": parent_id}),
|
|
state.clone(),
|
|
)
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
let _child_id = child[child.find("ID: ").unwrap() + 4..].trim().to_string();
|
|
|
|
let del_task = DeleteTaskHandler;
|
|
let res_del = del_task
|
|
.execute(json!({"id": parent_id}), state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res_del.contains("Deleted task and its children (2 total)."));
|
|
}
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_list_milestones_with_namespace() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
|
|
let add_milestone = AddMilestoneHandler;
|
|
let args_ms = serde_json::json!({
|
|
"name": "v1.0",
|
|
"title": "Release 1.0",
|
|
"description": "First release",
|
|
"target_date": 1700000000,
|
|
"end_date": 1700000000,
|
|
"namespace": "global"
|
|
});
|
|
let res1 = add_milestone
|
|
.execute(args_ms, state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res1.contains("Milestone added"));
|
|
|
|
let list_ms = ListMilestonesHandler;
|
|
let res2 = list_ms
|
|
.execute(serde_json::json!({"namespace": "global"}), state.clone())
|
|
.await
|
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
.unwrap();
|
|
assert!(res2.contains("Release 1.0"));
|
|
}
|
|
}
|
|
|