diff --git a/src/tools/todo_write.rs b/src/tools/todo_write.rs index 3fc9b71..36e2abf 100644 --- a/src/tools/todo_write.rs +++ b/src/tools/todo_write.rs @@ -273,9 +273,9 @@ impl Tool for TodoWriteTool { }); } else { // 传入 id 但旧状态中没有 → 按新 item 处理 - if new_status != TodoStatus::Pending { + if !is_valid_new_status(&new_status) { validation_errors.push(format!( - "Item '{}': new items must start as 'pending', got '{}'", + "Item '{}': new items must start as 'pending' or 'in_progress', got '{}'", content, status_str )); continue; @@ -291,10 +291,10 @@ impl Tool for TodoWriteTool { }); } } else { - // 新 item(无 id)— 必须是 pending - if new_status != TodoStatus::Pending { + // 新 item(无 id)— 允许 pending 或 in_progress + if !is_valid_new_status(&new_status) { validation_errors.push(format!( - "Item '{}': new items must start as 'pending', got '{}'", + "Item '{}': new items must start as 'pending' or 'in_progress', got '{}'", content, status_str )); continue; @@ -423,6 +423,11 @@ pub(crate) fn scope_key_from_context(context: &ToolContext) -> Option { tid.or(sid).map(str::to_string) } +/// 新创建的 item 允许 pending 或 in_progress,不允许 completed/cancelled +fn is_valid_new_status(status: &TodoStatus) -> bool { + matches!(status, TodoStatus::Pending | TodoStatus::InProgress) +} + /// 校验状态转换合法性 fn validate_transition(old: &TodoStatus, new: &TodoStatus) -> Result<(), String> { if old.is_terminal() { @@ -694,7 +699,7 @@ mod tests { } #[tokio::test] - async fn test_new_item_must_be_pending() { + async fn test_new_item_cannot_be_completed_or_cancelled() { let tool = TodoWriteTool::new(test_state()); let context = test_context(); @@ -711,7 +716,49 @@ mod tests { .unwrap(); assert!(!result.success); - assert!(result.error.unwrap().contains("must start as 'pending'")); + assert!(result.error.unwrap().contains("new items must start as")); + + let result2 = tool + .execute_with_context( + &context, + json!({ + "todos": [ + {"content": "任务B", "status": "cancelled"} + ] + }), + ) + .await + .unwrap(); + + assert!(!result2.success); + assert!(result2.error.unwrap().contains("new items must start as")); + } + + #[tokio::test] + async fn test_new_item_can_start_as_in_progress() { + let tool = TodoWriteTool::new(test_state()); + let context = test_context(); + + // 直接创建第一个任务为 in_progress — 应该允许 + let result = tool + .execute_with_context( + &context, + json!({ + "todos": [ + {"content": "第一个任务", "status": "in_progress"}, + {"content": "第二个任务", "status": "pending"}, + {"content": "第三个任务", "status": "pending"} + ] + }), + ) + .await + .unwrap(); + + assert!(result.success); + let output: serde_json::Value = serde_json::from_str(&result.output).unwrap(); + let todos = output["current_todos"].as_array().unwrap(); + assert_eq!(todos.len(), 3); + assert_eq!(todos[0]["status"], "in_progress"); } #[tokio::test]