1use serde::{Deserialize, Serialize};
8use sqlx::Row;
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
14pub struct PlanRequest {
15 pub tasks: Vec<TaskTree>,
17}
18
19#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
21pub struct TaskTree {
22 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub name: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub spec: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub priority: Option<PriorityValue>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub children: Option<Vec<TaskTree>>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub depends_on: Option<Vec<String>>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none", alias = "task_id")]
46 pub id: Option<i64>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub status: Option<TaskStatus>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub active_form: Option<String>,
56
57 #[serde(
62 default,
63 skip_serializing_if = "Option::is_none",
64 deserialize_with = "deserialize_parent_id"
65 )]
66 pub parent_id: Option<Option<i64>>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub delete: Option<bool>,
71}
72
73fn deserialize_parent_id<'de, D>(
79 deserializer: D,
80) -> std::result::Result<Option<Option<i64>>, D::Error>
81where
82 D: serde::Deserializer<'de>,
83{
84 let inner: Option<i64> = Option::deserialize(deserializer)?;
91 Ok(Some(inner))
92}
93
94#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97pub enum TaskStatus {
98 #[serde(alias = "pending")]
99 Todo,
100 #[serde(alias = "in_progress")]
101 Doing,
102 #[serde(alias = "completed")]
103 Done,
104}
105
106impl TaskStatus {
107 pub fn as_db_str(&self) -> &'static str {
109 match self {
110 TaskStatus::Todo => "todo",
111 TaskStatus::Doing => "doing",
112 TaskStatus::Done => "done",
113 }
114 }
115
116 pub fn from_db_str(s: &str) -> Option<Self> {
118 match s {
119 "todo" | "pending" => Some(TaskStatus::Todo),
120 "doing" | "in_progress" => Some(TaskStatus::Doing),
121 "done" | "completed" => Some(TaskStatus::Done),
122 _ => None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
129#[serde(rename_all = "lowercase")]
130pub enum PriorityValue {
131 Critical,
132 High,
133 Medium,
134 Low,
135}
136
137impl PriorityValue {
138 pub fn to_int(&self) -> i32 {
140 match self {
141 PriorityValue::Critical => 1,
142 PriorityValue::High => 2,
143 PriorityValue::Medium => 3,
144 PriorityValue::Low => 4,
145 }
146 }
147
148 pub fn from_int(value: i32) -> Option<Self> {
150 match value {
151 1 => Some(PriorityValue::Critical),
152 2 => Some(PriorityValue::High),
153 3 => Some(PriorityValue::Medium),
154 4 => Some(PriorityValue::Low),
155 _ => None,
156 }
157 }
158
159 pub fn as_str(&self) -> &'static str {
161 match self {
162 PriorityValue::Critical => "critical",
163 PriorityValue::High => "high",
164 PriorityValue::Medium => "medium",
165 PriorityValue::Low => "low",
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct ExistingTaskInfo {
173 pub id: i64,
174 pub status: String,
175 pub spec: Option<String>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
180pub struct PlanResult {
181 pub success: bool,
183
184 pub task_id_map: HashMap<String, i64>,
186
187 pub created_count: usize,
189
190 pub updated_count: usize,
192
193 #[serde(default, skip_serializing_if = "is_zero")]
195 pub deleted_count: usize,
196
197 #[serde(default, skip_serializing_if = "is_zero_i64")]
199 pub cascade_deleted_count: i64,
200
201 pub dependency_count: usize,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
207 pub focused_task: Option<crate::db::models::TaskWithEvents>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub error: Option<String>,
212
213 #[serde(skip_serializing_if = "Vec::is_empty", default)]
215 pub warnings: Vec<String>,
216}
217
218fn is_zero(n: &usize) -> bool {
219 *n == 0
220}
221
222fn is_zero_i64(n: &i64) -> bool {
223 *n == 0
224}
225
226impl PlanResult {
227 pub fn success(
229 task_id_map: HashMap<String, i64>,
230 created_count: usize,
231 updated_count: usize,
232 deleted_count: usize,
233 dependency_count: usize,
234 focused_task: Option<crate::db::models::TaskWithEvents>,
235 ) -> Self {
236 Self {
237 success: true,
238 task_id_map,
239 created_count,
240 updated_count,
241 deleted_count,
242 cascade_deleted_count: 0,
243 dependency_count,
244 focused_task,
245 error: None,
246 warnings: Vec::new(),
247 }
248 }
249
250 #[allow(clippy::too_many_arguments)]
252 pub fn success_with_warnings(
253 task_id_map: HashMap<String, i64>,
254 created_count: usize,
255 updated_count: usize,
256 deleted_count: usize,
257 cascade_deleted_count: i64,
258 dependency_count: usize,
259 focused_task: Option<crate::db::models::TaskWithEvents>,
260 warnings: Vec<String>,
261 ) -> Self {
262 Self {
263 success: true,
264 task_id_map,
265 created_count,
266 updated_count,
267 deleted_count,
268 cascade_deleted_count,
269 dependency_count,
270 focused_task,
271 error: None,
272 warnings,
273 }
274 }
275
276 pub fn error(message: impl Into<String>) -> Self {
278 Self {
279 success: false,
280 task_id_map: HashMap::new(),
281 created_count: 0,
282 updated_count: 0,
283 deleted_count: 0,
284 cascade_deleted_count: 0,
285 dependency_count: 0,
286 focused_task: None,
287 error: Some(message.into()),
288 warnings: Vec::new(),
289 }
290 }
291}
292
293pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
300 let mut names = Vec::new();
301
302 for task in tasks {
303 if let Some(name) = &task.name {
304 names.push(name.clone());
305 }
306
307 if let Some(children) = &task.children {
308 names.extend(extract_all_names(children));
309 }
310 }
311
312 names
313}
314
315#[derive(Debug, Clone, PartialEq, Default)]
317pub struct FlatTask {
318 pub name: Option<String>,
320 pub spec: Option<String>,
321 pub priority: Option<PriorityValue>,
322 pub parent_name: Option<String>,
324 pub depends_on: Vec<String>,
325 pub id: Option<i64>,
327 pub status: Option<TaskStatus>,
328 pub active_form: Option<String>,
329 pub explicit_parent_id: Option<Option<i64>>,
334 pub delete: bool,
336}
337
338pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
339 flatten_task_tree_recursive(tasks, None)
340}
341
342fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
343 let mut flat = Vec::new();
344
345 for task in tasks {
346 let flat_task = FlatTask {
347 name: task.name.clone(),
348 spec: task.spec.clone(),
349 priority: task.priority.clone(),
350 parent_name: parent_name.clone(),
351 depends_on: task.depends_on.clone().unwrap_or_default(),
352 id: task.id,
353 status: task.status.clone(),
354 active_form: task.active_form.clone(),
355 explicit_parent_id: task.parent_id,
356 delete: task.delete.unwrap_or(false),
357 };
358
359 flat.push(flat_task);
360
361 if let Some(children) = &task.children {
363 if let Some(name) = &task.name {
364 flat.extend(flatten_task_tree_recursive(children, Some(name.clone())));
365 }
366 }
367 }
368
369 flat
370}
371
372#[derive(Debug, Clone, PartialEq)]
374pub enum Operation {
375 Create(FlatTask),
376 Update { id: i64, task: FlatTask },
377 Delete { id: i64 },
378}
379
380pub fn classify_operations(
389 flat_tasks: &[FlatTask],
390 existing_names: &HashMap<String, i64>,
391) -> Vec<Operation> {
392 let mut operations = Vec::new();
393
394 for task in flat_tasks {
395 if task.delete {
397 if let Some(id) = task.id {
398 operations.push(Operation::Delete { id });
399 }
400 continue;
402 }
403
404 let operation = if let Some(id) = task.id {
406 Operation::Update {
408 id,
409 task: task.clone(),
410 }
411 } else if let Some(name) = &task.name {
412 if let Some(id) = existing_names.get(name) {
414 Operation::Update {
416 id: *id,
417 task: task.clone(),
418 }
419 } else {
420 Operation::Create(task.clone())
422 }
423 } else {
424 continue;
426 };
427
428 operations.push(operation);
429 }
430
431 operations
432}
433
434pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
436 let mut seen = HashMap::new();
437 let mut duplicates = Vec::new();
438
439 for name in extract_all_names(tasks) {
440 let count = seen.entry(name.clone()).or_insert(0);
441 *count += 1;
442 if *count == 2 {
443 duplicates.push(name);
445 }
446 }
447
448 duplicates
449}
450
451use crate::error::{IntentError, Result};
456use sqlx::SqlitePool;
457
458pub struct PlanExecutor<'a> {
460 pool: &'a SqlitePool,
461 project_path: Option<String>,
462 default_parent_id: Option<i64>,
464}
465
466impl<'a> PlanExecutor<'a> {
467 pub fn new(pool: &'a SqlitePool) -> Self {
469 Self {
470 pool,
471 project_path: None,
472 default_parent_id: None,
473 }
474 }
475
476 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
478 Self {
479 pool,
480 project_path: Some(project_path),
481 default_parent_id: None,
482 }
483 }
484
485 pub fn with_default_parent(mut self, parent_id: i64) -> Self {
488 self.default_parent_id = Some(parent_id);
489 self
490 }
491
492 fn get_task_manager(&self) -> crate::tasks::TaskManager<'a> {
494 match &self.project_path {
495 Some(path) => crate::tasks::TaskManager::with_project_path(self.pool, path.clone()),
496 None => crate::tasks::TaskManager::new(self.pool),
497 }
498 }
499
500 #[tracing::instrument(skip(self, request), fields(task_count = request.tasks.len()))]
502 pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
503 let duplicates = find_duplicate_names(&request.tasks);
505 if !duplicates.is_empty() {
506 return Ok(PlanResult::error(format!(
507 "Duplicate task names in request: {:?}",
508 duplicates
509 )));
510 }
511
512 let all_names = extract_all_names(&request.tasks);
514
515 let existing = self.find_tasks_by_names(&all_names).await?;
517
518 let flat_tasks = flatten_task_tree(&request.tasks);
520
521 if let Err(e) = self.validate_dependencies(&flat_tasks) {
523 return Ok(PlanResult::error(e.to_string()));
524 }
525
526 if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
528 return Ok(PlanResult::error(e.to_string()));
529 }
530
531 if let Err(e) = self.validate_batch_single_doing(&flat_tasks) {
533 return Ok(PlanResult::error(e.to_string()));
534 }
535
536 let task_mgr = self.get_task_manager();
538
539 let mut tx = self.pool.begin().await?;
541
542 let mut task_id_map = HashMap::new();
544 let mut created_count = 0;
545 let mut updated_count = 0;
546 let mut warnings: Vec<String> = Vec::new();
547 let mut newly_created_names: std::collections::HashSet<String> =
548 std::collections::HashSet::new();
549 let mut deleted_count = 0;
550
551 let (delete_tasks, normal_tasks): (Vec<_>, Vec<_>) =
562 flat_tasks.iter().partition(|t| t.delete);
563
564 for task in &delete_tasks {
566 if task.id.is_none() {
567 return Ok(PlanResult::error(
568 "Delete operation requires 'id' field. Use {\"id\": <task_id>, \"delete\": true}",
569 ));
570 }
571 }
572
573 for task in &delete_tasks {
586 if let Some(id) = task.id {
587 if let Some((focused_id, session_id)) =
589 task_mgr.find_focused_in_subtree_in_tx(&mut tx, id).await?
590 {
591 if focused_id == id {
592 return Ok(PlanResult::error(format!(
594 "Task #{} is the current focus of session '{}'. That session must switch focus first.",
595 id, session_id
596 )));
597 } else {
598 return Ok(PlanResult::error(format!(
600 "Task #{} is the current focus of session '{}' and would be deleted by cascade (descendant of #{}). That session must switch focus first.",
601 focused_id, session_id, id
602 )));
603 }
604 }
605 }
606 }
607
608 let mut cascade_deleted_count: i64 = 0;
612 for task in &delete_tasks {
613 if let Some(id) = task.id {
614 let delete_result = task_mgr.delete_task_in_tx(&mut tx, id).await?;
615
616 if !delete_result.found {
617 warnings.push(format!(
620 "Task #{} not found (may have been already deleted)",
621 id
622 ));
623 } else {
624 deleted_count += 1;
625
626 if delete_result.descendant_count > 0 {
628 cascade_deleted_count += delete_result.descendant_count;
629 warnings.push(format!(
630 "Task #{} had {} descendant(s) that were also deleted (cascade)",
631 id, delete_result.descendant_count
632 ));
633 }
634 }
635 }
636 }
637
638 for task in &normal_tasks {
644 let task_name = match &task.name {
646 Some(name) => name,
647 None => continue, };
649
650 let is_becoming_doing = task.status.as_ref() == Some(&TaskStatus::Doing);
652 let has_spec = task
653 .spec
654 .as_ref()
655 .map(|s| !s.trim().is_empty())
656 .unwrap_or(false);
657
658 if let Some(existing_info) = existing.get(task_name) {
659 if is_becoming_doing && !has_spec {
664 let existing_is_doing = existing_info.status == "doing";
665 let existing_has_spec = existing_info
666 .spec
667 .as_ref()
668 .map(|s| !s.trim().is_empty())
669 .unwrap_or(false);
670
671 if !existing_is_doing && !existing_has_spec {
673 return Ok(PlanResult::error(format!(
674 "Task '{}': spec (description) is required when starting a task (status: doing).\n\n\
675 Before starting a task, please describe:\n \
676 • What is the goal of this task\n \
677 • How do you plan to approach it\n\n\
678 Tip: Use @file(path) to include content from a file",
679 task_name
680 )));
681 }
682 }
683
684 let is_becoming_done = task.status.as_ref() == Some(&TaskStatus::Done);
686
687 task_mgr
689 .update_task_in_tx(
690 &mut tx,
691 existing_info.id,
692 task.spec.as_deref(),
693 task.priority.as_ref().map(|p| p.to_int()),
694 if is_becoming_done {
696 None
697 } else {
698 task.status.as_ref().map(|s| s.as_db_str())
699 },
700 task.active_form.as_deref(),
701 )
702 .await?;
703
704 if is_becoming_done {
706 if let Err(e) = task_mgr
707 .complete_task_in_tx(&mut tx, existing_info.id)
708 .await
709 {
710 return Ok(PlanResult::error(format!(
712 "Cannot complete task '{}': {}\n\n\
713 Please complete all subtasks before marking the parent as done.",
714 task_name, e
715 )));
716 }
717 }
718
719 task_id_map.insert(task_name.clone(), existing_info.id);
720 updated_count += 1;
721 } else {
722 if is_becoming_doing && !has_spec {
726 return Ok(PlanResult::error(format!(
727 "Task '{}': spec (description) is required when starting a task (status: doing).\n\n\
728 Before starting a task, please describe:\n \
729 • What is the goal of this task\n \
730 • How do you plan to approach it\n\n\
731 Tip: Use @file(path) to include content from a file",
732 task_name
733 )));
734 }
735
736 let id = task_mgr
737 .create_task_in_tx(
738 &mut tx,
739 task_name,
740 task.spec.as_deref(),
741 task.priority.as_ref().map(|p| p.to_int()),
742 task.status.as_ref().map(|s| s.as_db_str()),
743 task.active_form.as_deref(),
744 "ai", )
746 .await?;
747 task_id_map.insert(task_name.clone(), id);
748 newly_created_names.insert(task_name.clone());
749 created_count += 1;
750
751 if !has_spec && !is_becoming_doing {
753 warnings.push(format!(
754 "Task '{}' has no description. Consider adding one for better context.",
755 task_name
756 ));
757 }
758 }
759 }
760
761 for task in &normal_tasks {
763 if let Some(parent_name) = &task.parent_name {
764 if let Some(task_name) = &task.name {
765 let task_id = task_id_map.get(task_name).ok_or_else(|| {
766 IntentError::InvalidInput(format!("Task not found: {}", task_name))
767 })?;
768 let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
769 IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
770 })?;
771 task_mgr
772 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
773 .await?;
774 }
775 }
776 }
777
778 for task in &normal_tasks {
781 if task.parent_name.is_some() {
783 continue;
784 }
785
786 if let Some(explicit_parent) = &task.explicit_parent_id {
788 if let Some(task_name) = &task.name {
789 let task_id = task_id_map.get(task_name).ok_or_else(|| {
790 IntentError::InvalidInput(format!("Task not found: {}", task_name))
791 })?;
792
793 match explicit_parent {
794 None => {
795 task_mgr.clear_parent_in_tx(&mut tx, *task_id).await?;
797 },
798 Some(parent_id) => {
799 task_mgr
802 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
803 .await?;
804 },
805 }
806 }
807 }
808 }
809
810 if let Some(default_parent) = self.default_parent_id {
812 for task in &normal_tasks {
813 if let Some(task_name) = &task.name {
818 if newly_created_names.contains(task_name)
819 && task.parent_name.is_none()
820 && task.explicit_parent_id.is_none()
821 {
822 if let Some(task_id) = task_id_map.get(task_name) {
823 task_mgr
824 .set_parent_in_tx(&mut tx, *task_id, default_parent)
825 .await?;
826 }
827 }
828 }
829 }
830 }
831
832 let dep_count = self
834 .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
835 .await?;
836
837 tx.commit().await?;
839
840 task_mgr.notify_batch_changed().await;
842
843 let doing_task = normal_tasks
846 .iter()
847 .find(|task| matches!(task.status, Some(TaskStatus::Doing)));
848
849 let focused_task_response = if let Some(doing_task) = doing_task {
850 if let Some(task_name) = &doing_task.name {
852 if let Some(task_id) = task_id_map.get(task_name) {
853 let response = task_mgr.start_task(*task_id, true).await?;
855 Some(response)
856 } else {
857 None
858 }
859 } else {
860 None
861 }
862 } else {
863 None
864 };
865
866 Ok(PlanResult::success_with_warnings(
868 task_id_map,
869 created_count,
870 updated_count,
871 deleted_count,
872 cascade_deleted_count,
873 dep_count,
874 focused_task_response,
875 warnings,
876 ))
877 }
878
879 async fn find_tasks_by_names(
881 &self,
882 names: &[String],
883 ) -> Result<HashMap<String, ExistingTaskInfo>> {
884 if names.is_empty() {
885 return Ok(HashMap::new());
886 }
887
888 let mut map = HashMap::new();
889
890 let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
893 let query = format!(
894 "SELECT id, name, status, spec FROM tasks WHERE name IN ({})",
895 placeholders
896 );
897
898 let mut query_builder = sqlx::query(&query);
899 for name in names {
900 query_builder = query_builder.bind(name);
901 }
902
903 let rows = query_builder.fetch_all(self.pool).await?;
904
905 for row in rows {
906 let id: i64 = row.get("id");
907 let name: String = row.get("name");
908 let status: String = row.get("status");
909 let spec: Option<String> = row.get("spec");
910 map.insert(name, ExistingTaskInfo { id, status, spec });
911 }
912
913 Ok(map)
914 }
915
916 async fn build_dependencies(
918 &self,
919 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
920 flat_tasks: &[FlatTask],
921 task_id_map: &HashMap<String, i64>,
922 ) -> Result<usize> {
923 let mut count = 0;
924
925 for task in flat_tasks {
926 if task.delete {
928 continue;
929 }
930 let task_name = match &task.name {
931 Some(name) => name,
932 None => continue,
933 };
934
935 if !task.depends_on.is_empty() {
936 let blocked_id = task_id_map.get(task_name).ok_or_else(|| {
937 IntentError::InvalidInput(format!("Task not found: {}", task_name))
938 })?;
939
940 for dep_name in &task.depends_on {
941 let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
942 IntentError::InvalidInput(format!(
943 "Dependency '{}' not found for task '{}'",
944 dep_name, task_name
945 ))
946 })?;
947
948 sqlx::query(
949 "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
950 )
951 .bind(blocking_id)
952 .bind(blocked_id)
953 .execute(&mut **tx)
954 .await?;
955
956 count += 1;
957 }
958 }
959 }
960
961 Ok(count)
962 }
963
964 fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
966 crate::plan_validation::validate_dependencies(flat_tasks)
967 }
968
969 fn validate_batch_single_doing(&self, flat_tasks: &[FlatTask]) -> Result<()> {
970 crate::plan_validation::validate_batch_single_doing(flat_tasks)
971 }
972
973 fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
974 crate::plan_validation::detect_circular_dependencies(flat_tasks)
975 }
976}
977
978#[derive(Debug, Default)]
980pub struct FileIncludeResult {
981 pub files_to_delete: Vec<PathBuf>,
983}
984
985fn parse_file_directive(value: &str) -> Option<(PathBuf, bool)> {
991 let trimmed = value.trim();
992
993 if !trimmed.starts_with("@file(") || !trimmed.ends_with(')') {
995 return None;
996 }
997
998 let inner = &trimmed[6..trimmed.len() - 1];
1000
1001 if let Some(path_str) = inner.strip_suffix(", keep") {
1003 Some((PathBuf::from(path_str.trim()), false)) } else if let Some(path_str) = inner.strip_suffix(",keep") {
1005 Some((PathBuf::from(path_str.trim()), false))
1006 } else {
1007 Some((PathBuf::from(inner.trim()), true)) }
1009}
1010
1011fn process_task_tree_includes(
1013 task: &mut TaskTree,
1014 files_to_delete: &mut Vec<PathBuf>,
1015) -> std::result::Result<(), String> {
1016 if let Some(ref spec_value) = task.spec {
1018 if let Some((file_path, should_delete)) = parse_file_directive(spec_value) {
1019 let content = std::fs::read_to_string(&file_path)
1021 .map_err(|e| format!("Failed to read @file({}): {}", file_path.display(), e))?;
1022
1023 task.spec = Some(content);
1024
1025 if should_delete {
1026 files_to_delete.push(file_path);
1027 }
1028 }
1029 }
1030
1031 if let Some(ref mut children) = task.children {
1033 for child in children.iter_mut() {
1034 process_task_tree_includes(child, files_to_delete)?;
1035 }
1036 }
1037
1038 Ok(())
1039}
1040
1041pub fn process_file_includes(
1063 request: &mut PlanRequest,
1064) -> std::result::Result<FileIncludeResult, String> {
1065 let mut result = FileIncludeResult::default();
1066
1067 for task in request.tasks.iter_mut() {
1068 process_task_tree_includes(task, &mut result.files_to_delete)?;
1069 }
1070
1071 Ok(result)
1072}
1073
1074pub fn cleanup_included_files(files: &[PathBuf]) {
1076 for file in files {
1077 if let Err(e) = std::fs::remove_file(file) {
1078 tracing::warn!("Failed to delete included file {}: {}", file.display(), e);
1080 }
1081 }
1082}
1083
1084impl crate::backend::PlanBackend for PlanExecutor<'_> {
1085 fn execute(
1086 &self,
1087 request: &PlanRequest,
1088 ) -> impl std::future::Future<Output = crate::error::Result<PlanResult>> + Send {
1089 self.execute(request)
1090 }
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096
1097 #[test]
1098 fn test_priority_value_to_int() {
1099 assert_eq!(PriorityValue::Critical.to_int(), 1);
1100 assert_eq!(PriorityValue::High.to_int(), 2);
1101 assert_eq!(PriorityValue::Medium.to_int(), 3);
1102 assert_eq!(PriorityValue::Low.to_int(), 4);
1103 }
1104
1105 #[test]
1106 fn test_priority_value_from_int() {
1107 assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
1108 assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
1109 assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
1110 assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
1111 assert_eq!(PriorityValue::from_int(999), None);
1112 }
1113
1114 #[test]
1115 fn test_priority_value_as_str() {
1116 assert_eq!(PriorityValue::Critical.as_str(), "critical");
1117 assert_eq!(PriorityValue::High.as_str(), "high");
1118 assert_eq!(PriorityValue::Medium.as_str(), "medium");
1119 assert_eq!(PriorityValue::Low.as_str(), "low");
1120 }
1121
1122 #[test]
1123 fn test_plan_request_deserialization_minimal() {
1124 let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
1125 let request: PlanRequest = serde_json::from_str(json).unwrap();
1126
1127 assert_eq!(request.tasks.len(), 1);
1128 assert_eq!(request.tasks[0].name, Some("Test Task".to_string()));
1129 assert_eq!(request.tasks[0].spec, None);
1130 assert_eq!(request.tasks[0].priority, None);
1131 assert_eq!(request.tasks[0].children, None);
1132 assert_eq!(request.tasks[0].depends_on, None);
1133 assert_eq!(request.tasks[0].id, None);
1134 }
1135
1136 #[test]
1137 fn test_plan_request_deserialization_full() {
1138 let json = r#"{
1139 "tasks": [{
1140 "name": "Parent Task",
1141 "spec": "Parent spec",
1142 "priority": "high",
1143 "children": [{
1144 "name": "Child Task",
1145 "spec": "Child spec"
1146 }],
1147 "depends_on": ["Other Task"],
1148 "task_id": 42
1149 }]
1150 }"#;
1151
1152 let request: PlanRequest = serde_json::from_str(json).unwrap();
1153
1154 assert_eq!(request.tasks.len(), 1);
1155 let parent = &request.tasks[0];
1156 assert_eq!(parent.name, Some("Parent Task".to_string()));
1157 assert_eq!(parent.spec, Some("Parent spec".to_string()));
1158 assert_eq!(parent.priority, Some(PriorityValue::High));
1159 assert_eq!(parent.id, Some(42));
1160
1161 let children = parent.children.as_ref().unwrap();
1162 assert_eq!(children.len(), 1);
1163 assert_eq!(children[0].name, Some("Child Task".to_string()));
1164
1165 let depends = parent.depends_on.as_ref().unwrap();
1166 assert_eq!(depends.len(), 1);
1167 assert_eq!(depends[0], "Other Task");
1168 }
1169
1170 #[test]
1171 fn test_plan_request_serialization() {
1172 let request = PlanRequest {
1173 tasks: vec![TaskTree {
1174 name: Some("Test Task".to_string()),
1175 spec: Some("Test spec".to_string()),
1176 priority: Some(PriorityValue::Medium),
1177 children: None,
1178 depends_on: None,
1179 id: None,
1180 status: None,
1181 active_form: None,
1182 parent_id: None,
1183 ..Default::default()
1184 }],
1185 };
1186
1187 let json = serde_json::to_string(&request).unwrap();
1188 assert!(json.contains("\"name\":\"Test Task\""));
1189 assert!(json.contains("\"spec\":\"Test spec\""));
1190 assert!(json.contains("\"priority\":\"medium\""));
1191 }
1192
1193 #[test]
1194 fn test_plan_result_success() {
1195 let mut map = HashMap::new();
1196 map.insert("Task 1".to_string(), 1);
1197 map.insert("Task 2".to_string(), 2);
1198
1199 let result = PlanResult::success(map.clone(), 2, 0, 0, 1, None);
1200
1201 assert!(result.success);
1202 assert_eq!(result.task_id_map, map);
1203 assert_eq!(result.created_count, 2);
1204 assert_eq!(result.updated_count, 0);
1205 assert_eq!(result.dependency_count, 1);
1206 assert_eq!(result.focused_task, None);
1207 assert_eq!(result.error, None);
1208 }
1209
1210 #[test]
1211 fn test_plan_result_error() {
1212 let result = PlanResult::error("Test error");
1213
1214 assert!(!result.success);
1215 assert_eq!(result.task_id_map.len(), 0);
1216 assert_eq!(result.created_count, 0);
1217 assert_eq!(result.updated_count, 0);
1218 assert_eq!(result.dependency_count, 0);
1219 assert_eq!(result.error, Some("Test error".to_string()));
1220 }
1221
1222 #[test]
1223 fn test_task_tree_nested() {
1224 let tree = TaskTree {
1225 name: Some("Parent".to_string()),
1226 spec: None,
1227 priority: None,
1228 children: Some(vec![
1229 TaskTree {
1230 name: Some("Child 1".to_string()),
1231 spec: None,
1232 priority: None,
1233 children: None,
1234 depends_on: None,
1235 id: None,
1236 status: None,
1237 active_form: None,
1238 parent_id: None,
1239 ..Default::default()
1240 },
1241 TaskTree {
1242 name: Some("Child 2".to_string()),
1243 spec: None,
1244 priority: Some(PriorityValue::High),
1245 children: None,
1246 depends_on: None,
1247 id: None,
1248 status: None,
1249 active_form: None,
1250 parent_id: None,
1251 ..Default::default()
1252 },
1253 ]),
1254 depends_on: None,
1255 id: None,
1256 status: None,
1257 active_form: None,
1258 parent_id: None,
1259 ..Default::default()
1260 };
1261
1262 let json = serde_json::to_string_pretty(&tree).unwrap();
1263 let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
1264
1265 assert_eq!(tree, deserialized);
1266 assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
1267 }
1268
1269 #[test]
1270 fn test_priority_value_case_insensitive_deserialization() {
1271 let json = r#"{"name": "Test", "priority": "high"}"#;
1273 let task: TaskTree = serde_json::from_str(json).unwrap();
1274 assert_eq!(task.priority, Some(PriorityValue::High));
1275
1276 }
1279
1280 #[test]
1281 fn test_extract_all_names_simple() {
1282 let tasks = vec![
1283 TaskTree {
1284 name: Some("Task 1".to_string()),
1285 spec: None,
1286 priority: None,
1287 children: None,
1288 depends_on: None,
1289 id: None,
1290 status: None,
1291 active_form: None,
1292 parent_id: None,
1293 ..Default::default()
1294 },
1295 TaskTree {
1296 name: Some("Task 2".to_string()),
1297 spec: None,
1298 priority: None,
1299 children: None,
1300 depends_on: None,
1301 id: None,
1302 status: None,
1303 active_form: None,
1304 parent_id: None,
1305 ..Default::default()
1306 },
1307 ];
1308
1309 let names = extract_all_names(&tasks);
1310 assert_eq!(names, vec!["Task 1", "Task 2"]);
1311 }
1312
1313 #[test]
1314 fn test_extract_all_names_nested() {
1315 let tasks = vec![TaskTree {
1316 name: Some("Parent".to_string()),
1317 spec: None,
1318 priority: None,
1319 children: Some(vec![
1320 TaskTree {
1321 name: Some("Child 1".to_string()),
1322 spec: None,
1323 priority: None,
1324 children: None,
1325 depends_on: None,
1326 id: None,
1327 status: None,
1328 active_form: None,
1329 parent_id: None,
1330 ..Default::default()
1331 },
1332 TaskTree {
1333 name: Some("Child 2".to_string()),
1334 spec: None,
1335 priority: None,
1336 children: Some(vec![TaskTree {
1337 name: Some("Grandchild".to_string()),
1338 spec: None,
1339 priority: None,
1340 children: None,
1341 depends_on: None,
1342 id: None,
1343 status: None,
1344 active_form: None,
1345 parent_id: None,
1346 ..Default::default()
1347 }]),
1348 depends_on: None,
1349 id: None,
1350 status: None,
1351 active_form: None,
1352 parent_id: None,
1353 ..Default::default()
1354 },
1355 ]),
1356 depends_on: None,
1357 id: None,
1358 status: None,
1359 active_form: None,
1360 parent_id: None,
1361 ..Default::default()
1362 }];
1363
1364 let names = extract_all_names(&tasks);
1365 assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
1366 }
1367
1368 #[test]
1369 fn test_flatten_task_tree_simple() {
1370 let tasks = vec![TaskTree {
1371 name: Some("Task 1".to_string()),
1372 spec: Some("Spec 1".to_string()),
1373 priority: Some(PriorityValue::High),
1374 children: None,
1375 depends_on: Some(vec!["Task 0".to_string()]),
1376 id: None,
1377 status: None,
1378 active_form: None,
1379 parent_id: None,
1380 ..Default::default()
1381 }];
1382
1383 let flat = flatten_task_tree(&tasks);
1384 assert_eq!(flat.len(), 1);
1385 assert_eq!(flat[0].name, Some("Task 1".to_string()));
1386 assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
1387 assert_eq!(flat[0].priority, Some(PriorityValue::High));
1388 assert_eq!(flat[0].parent_name, None);
1389 assert_eq!(flat[0].depends_on, vec!["Task 0"]);
1390 }
1391
1392 #[test]
1393 fn test_flatten_task_tree_nested() {
1394 let tasks = vec![TaskTree {
1395 name: Some("Parent".to_string()),
1396 spec: None,
1397 priority: None,
1398 children: Some(vec![
1399 TaskTree {
1400 name: Some("Child 1".to_string()),
1401 spec: None,
1402 priority: None,
1403 children: None,
1404 depends_on: None,
1405 id: None,
1406 status: None,
1407 active_form: None,
1408 parent_id: None,
1409 ..Default::default()
1410 },
1411 TaskTree {
1412 name: Some("Child 2".to_string()),
1413 spec: None,
1414 priority: None,
1415 children: None,
1416 depends_on: None,
1417 id: None,
1418 status: None,
1419 active_form: None,
1420 parent_id: None,
1421 ..Default::default()
1422 },
1423 ]),
1424 depends_on: None,
1425 id: None,
1426 status: None,
1427 active_form: None,
1428 parent_id: None,
1429 ..Default::default()
1430 }];
1431
1432 let flat = flatten_task_tree(&tasks);
1433 assert_eq!(flat.len(), 3);
1434
1435 assert_eq!(flat[0].name, Some("Parent".to_string()));
1437 assert_eq!(flat[0].parent_name, None);
1438
1439 assert_eq!(flat[1].name, Some("Child 1".to_string()));
1441 assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
1442
1443 assert_eq!(flat[2].name, Some("Child 2".to_string()));
1444 assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
1445 }
1446
1447 #[test]
1448 fn test_classify_operations_all_create() {
1449 let flat_tasks = vec![
1450 FlatTask {
1451 name: Some("Task 1".to_string()),
1452 spec: None,
1453 priority: None,
1454 parent_name: None,
1455 depends_on: vec![],
1456 id: None,
1457 status: None,
1458 active_form: None,
1459 explicit_parent_id: None,
1460 ..Default::default()
1461 },
1462 FlatTask {
1463 name: Some("Task 2".to_string()),
1464 spec: None,
1465 priority: None,
1466 parent_name: None,
1467 depends_on: vec![],
1468 id: None,
1469 status: None,
1470 active_form: None,
1471 explicit_parent_id: None,
1472 ..Default::default()
1473 },
1474 ];
1475
1476 let existing = HashMap::new();
1477 let operations = classify_operations(&flat_tasks, &existing);
1478
1479 assert_eq!(operations.len(), 2);
1480 assert!(matches!(operations[0], Operation::Create(_)));
1481 assert!(matches!(operations[1], Operation::Create(_)));
1482 }
1483
1484 #[test]
1485 fn test_classify_operations_all_update() {
1486 let flat_tasks = vec![
1487 FlatTask {
1488 name: Some("Task 1".to_string()),
1489 spec: None,
1490 priority: None,
1491 parent_name: None,
1492 depends_on: vec![],
1493 id: None,
1494 status: None,
1495 active_form: None,
1496 explicit_parent_id: None,
1497 ..Default::default()
1498 },
1499 FlatTask {
1500 name: Some("Task 2".to_string()),
1501 spec: None,
1502 priority: None,
1503 parent_name: None,
1504 depends_on: vec![],
1505 id: None,
1506 status: None,
1507 active_form: None,
1508 explicit_parent_id: None,
1509 ..Default::default()
1510 },
1511 ];
1512
1513 let mut existing = HashMap::new();
1514 existing.insert("Task 1".to_string(), 1);
1515 existing.insert("Task 2".to_string(), 2);
1516
1517 let operations = classify_operations(&flat_tasks, &existing);
1518
1519 assert_eq!(operations.len(), 2);
1520 assert!(matches!(operations[0], Operation::Update { id: 1, .. }));
1521 assert!(matches!(operations[1], Operation::Update { id: 2, .. }));
1522 }
1523
1524 #[test]
1525 fn test_classify_operations_mixed() {
1526 let flat_tasks = vec![
1527 FlatTask {
1528 name: Some("Existing Task".to_string()),
1529 spec: None,
1530 priority: None,
1531 parent_name: None,
1532 depends_on: vec![],
1533 id: None,
1534 status: None,
1535 active_form: None,
1536 explicit_parent_id: None,
1537 ..Default::default()
1538 },
1539 FlatTask {
1540 name: Some("New Task".to_string()),
1541 spec: None,
1542 priority: None,
1543 parent_name: None,
1544 depends_on: vec![],
1545 id: None,
1546 status: None,
1547 active_form: None,
1548 explicit_parent_id: None,
1549 ..Default::default()
1550 },
1551 ];
1552
1553 let mut existing = HashMap::new();
1554 existing.insert("Existing Task".to_string(), 42);
1555
1556 let operations = classify_operations(&flat_tasks, &existing);
1557
1558 assert_eq!(operations.len(), 2);
1559 assert!(matches!(operations[0], Operation::Update { id: 42, .. }));
1560 assert!(matches!(operations[1], Operation::Create(_)));
1561 }
1562
1563 #[test]
1564 fn test_classify_operations_explicit_task_id() {
1565 let flat_tasks = vec![FlatTask {
1566 name: Some("Task".to_string()),
1567 spec: None,
1568 priority: None,
1569 parent_name: None,
1570 depends_on: vec![],
1571 id: Some(99), status: None,
1573 active_form: None,
1574 explicit_parent_id: None,
1575 ..Default::default()
1576 }];
1577
1578 let existing = HashMap::new(); let operations = classify_operations(&flat_tasks, &existing);
1581
1582 assert_eq!(operations.len(), 1);
1584 assert!(matches!(operations[0], Operation::Update { id: 99, .. }));
1585 }
1586
1587 #[test]
1588 fn test_find_duplicate_names_no_duplicates() {
1589 let tasks = vec![
1590 TaskTree {
1591 name: Some("Task 1".to_string()),
1592 spec: None,
1593 priority: None,
1594 children: None,
1595 depends_on: None,
1596 id: None,
1597 status: None,
1598 active_form: None,
1599 parent_id: None,
1600 ..Default::default()
1601 },
1602 TaskTree {
1603 name: Some("Task 2".to_string()),
1604 spec: None,
1605 priority: None,
1606 children: None,
1607 depends_on: None,
1608 id: None,
1609 status: None,
1610 active_form: None,
1611 parent_id: None,
1612 ..Default::default()
1613 },
1614 ];
1615
1616 let duplicates = find_duplicate_names(&tasks);
1617 assert_eq!(duplicates.len(), 0);
1618 }
1619
1620 #[test]
1621 fn test_find_duplicate_names_with_duplicates() {
1622 let tasks = vec![
1623 TaskTree {
1624 name: Some("Duplicate".to_string()),
1625 spec: None,
1626 priority: None,
1627 children: None,
1628 depends_on: None,
1629 id: None,
1630 status: None,
1631 active_form: None,
1632 parent_id: None,
1633 ..Default::default()
1634 },
1635 TaskTree {
1636 name: Some("Unique".to_string()),
1637 spec: None,
1638 priority: None,
1639 children: None,
1640 depends_on: None,
1641 id: None,
1642 status: None,
1643 active_form: None,
1644 parent_id: None,
1645 ..Default::default()
1646 },
1647 TaskTree {
1648 name: Some("Duplicate".to_string()),
1649 spec: None,
1650 priority: None,
1651 children: None,
1652 depends_on: None,
1653 id: None,
1654 status: None,
1655 active_form: None,
1656 parent_id: None,
1657 ..Default::default()
1658 },
1659 ];
1660
1661 let duplicates = find_duplicate_names(&tasks);
1662 assert_eq!(duplicates.len(), 1);
1663 assert_eq!(duplicates[0], "Duplicate");
1664 }
1665
1666 #[test]
1667 fn test_find_duplicate_names_nested() {
1668 let tasks = vec![TaskTree {
1669 name: Some("Parent".to_string()),
1670 spec: None,
1671 priority: None,
1672 children: Some(vec![TaskTree {
1673 name: Some("Parent".to_string()), spec: None,
1675 priority: None,
1676 children: None,
1677 depends_on: None,
1678 id: None,
1679 status: None,
1680 active_form: None,
1681 parent_id: None,
1682 ..Default::default()
1683 }]),
1684 depends_on: None,
1685 id: None,
1686 status: None,
1687 active_form: None,
1688 parent_id: None,
1689 ..Default::default()
1690 }];
1691
1692 let duplicates = find_duplicate_names(&tasks);
1693 assert_eq!(duplicates.len(), 1);
1694 assert_eq!(duplicates[0], "Parent");
1695 }
1696
1697 #[test]
1698 fn test_flatten_task_tree_empty() {
1699 let tasks: Vec<TaskTree> = vec![];
1700 let flat = flatten_task_tree(&tasks);
1701 assert_eq!(flat.len(), 0);
1702 }
1703
1704 #[test]
1705 fn test_flatten_task_tree_deep_nesting() {
1706 let tasks = vec![TaskTree {
1708 name: Some("Root".to_string()),
1709 spec: None,
1710 priority: None,
1711 children: Some(vec![TaskTree {
1712 name: Some("Level1".to_string()),
1713 spec: None,
1714 priority: None,
1715 children: Some(vec![TaskTree {
1716 name: Some("Level2".to_string()),
1717 spec: None,
1718 priority: None,
1719 children: Some(vec![TaskTree {
1720 name: Some("Level3".to_string()),
1721 spec: None,
1722 priority: None,
1723 children: None,
1724 depends_on: None,
1725 id: None,
1726 status: None,
1727 active_form: None,
1728 parent_id: None,
1729 ..Default::default()
1730 }]),
1731 depends_on: None,
1732 id: None,
1733 status: None,
1734 active_form: None,
1735 parent_id: None,
1736 ..Default::default()
1737 }]),
1738 depends_on: None,
1739 id: None,
1740 status: None,
1741 active_form: None,
1742 parent_id: None,
1743 ..Default::default()
1744 }]),
1745 depends_on: None,
1746 id: None,
1747 status: None,
1748 active_form: None,
1749 parent_id: None,
1750 ..Default::default()
1751 }];
1752
1753 let flat = flatten_task_tree(&tasks);
1754 assert_eq!(flat.len(), 4);
1755
1756 assert_eq!(flat[0].name, Some("Root".to_string()));
1758 assert_eq!(flat[0].parent_name, None);
1759
1760 assert_eq!(flat[1].name, Some("Level1".to_string()));
1761 assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1762
1763 assert_eq!(flat[2].name, Some("Level2".to_string()));
1764 assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1765
1766 assert_eq!(flat[3].name, Some("Level3".to_string()));
1767 assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1768 }
1769
1770 #[test]
1771 fn test_flatten_task_tree_many_siblings() {
1772 let children: Vec<TaskTree> = (0..10)
1773 .map(|i| TaskTree {
1774 name: Some(format!("Child {}", i)),
1775 spec: None,
1776 priority: None,
1777 children: None,
1778 depends_on: None,
1779 id: None,
1780 status: None,
1781 active_form: None,
1782 parent_id: None,
1783 ..Default::default()
1784 })
1785 .collect();
1786
1787 let tasks = vec![TaskTree {
1788 name: Some("Parent".to_string()),
1789 spec: None,
1790 priority: None,
1791 children: Some(children),
1792 depends_on: None,
1793 id: None,
1794 status: None,
1795 active_form: None,
1796 parent_id: None,
1797 ..Default::default()
1798 }];
1799
1800 let flat = flatten_task_tree(&tasks);
1801 assert_eq!(flat.len(), 11); for child in flat.iter().skip(1).take(10) {
1805 assert_eq!(child.parent_name, Some("Parent".to_string()));
1806 }
1807 }
1808
1809 #[test]
1810 fn test_flatten_task_tree_complex_mixed() {
1811 let tasks = vec![
1813 TaskTree {
1814 name: Some("Task 1".to_string()),
1815 spec: None,
1816 priority: None,
1817 children: Some(vec![
1818 TaskTree {
1819 name: Some("Task 1.1".to_string()),
1820 spec: None,
1821 priority: None,
1822 children: None,
1823 depends_on: None,
1824 id: None,
1825 status: None,
1826 active_form: None,
1827 parent_id: None,
1828 ..Default::default()
1829 },
1830 TaskTree {
1831 name: Some("Task 1.2".to_string()),
1832 spec: None,
1833 priority: None,
1834 children: Some(vec![TaskTree {
1835 name: Some("Task 1.2.1".to_string()),
1836 spec: None,
1837 priority: None,
1838 children: None,
1839 depends_on: None,
1840 id: None,
1841 status: None,
1842 active_form: None,
1843 parent_id: None,
1844 ..Default::default()
1845 }]),
1846 depends_on: None,
1847 id: None,
1848 status: None,
1849 active_form: None,
1850 parent_id: None,
1851 ..Default::default()
1852 },
1853 ]),
1854 depends_on: None,
1855 id: None,
1856 status: None,
1857 active_form: None,
1858 parent_id: None,
1859 ..Default::default()
1860 },
1861 TaskTree {
1862 name: Some("Task 2".to_string()),
1863 spec: None,
1864 priority: None,
1865 children: None,
1866 depends_on: Some(vec!["Task 1".to_string()]),
1867 id: None,
1868 status: None,
1869 active_form: None,
1870 parent_id: None,
1871 ..Default::default()
1872 },
1873 ];
1874
1875 let flat = flatten_task_tree(&tasks);
1876 assert_eq!(flat.len(), 5);
1877
1878 assert_eq!(flat[0].name, Some("Task 1".to_string()));
1880 assert_eq!(flat[0].parent_name, None);
1881
1882 assert_eq!(flat[1].name, Some("Task 1.1".to_string()));
1883 assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
1884
1885 assert_eq!(flat[2].name, Some("Task 1.2".to_string()));
1886 assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
1887
1888 assert_eq!(flat[3].name, Some("Task 1.2.1".to_string()));
1889 assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
1890
1891 assert_eq!(flat[4].name, Some("Task 2".to_string()));
1892 assert_eq!(flat[4].parent_name, None);
1893 assert_eq!(flat[4].depends_on, vec!["Task 1"]);
1894 }
1895
1896 #[tokio::test]
1897 async fn test_plan_executor_integration() {
1898 use crate::test_utils::test_helpers::TestContext;
1899
1900 let ctx = TestContext::new().await;
1901
1902 let request = PlanRequest {
1904 tasks: vec![TaskTree {
1905 name: Some("Integration Test Plan".to_string()),
1906 spec: Some("Test plan execution end-to-end".to_string()),
1907 priority: Some(PriorityValue::High),
1908 children: Some(vec![
1909 TaskTree {
1910 name: Some("Subtask A".to_string()),
1911 spec: Some("First subtask".to_string()),
1912 priority: None,
1913 children: None,
1914 depends_on: None,
1915 id: None,
1916 status: None,
1917 active_form: None,
1918 parent_id: None,
1919 ..Default::default()
1920 },
1921 TaskTree {
1922 name: Some("Subtask B".to_string()),
1923 spec: Some("Second subtask depends on A".to_string()),
1924 priority: None,
1925 children: None,
1926 depends_on: Some(vec!["Subtask A".to_string()]),
1927 id: None,
1928 status: None,
1929 active_form: None,
1930 parent_id: None,
1931 ..Default::default()
1932 },
1933 ]),
1934 depends_on: None,
1935 id: None,
1936 status: None,
1937 active_form: None,
1938 parent_id: None,
1939 ..Default::default()
1940 }],
1941 };
1942
1943 let executor = PlanExecutor::new(&ctx.pool);
1945 let result = executor.execute(&request).await.unwrap();
1946
1947 assert!(result.success, "Plan execution should succeed");
1949 assert_eq!(result.created_count, 3, "Should create 3 tasks");
1950 assert_eq!(result.updated_count, 0, "Should not update any tasks");
1951 assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
1952 assert!(result.error.is_none(), "Should have no error");
1953
1954 assert_eq!(result.task_id_map.len(), 3);
1956 assert!(result.task_id_map.contains_key("Integration Test Plan"));
1957 assert!(result.task_id_map.contains_key("Subtask A"));
1958 assert!(result.task_id_map.contains_key("Subtask B"));
1959
1960 let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
1962 let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
1963 let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
1964
1965 let parent: (String, String, i64, Option<i64>) =
1967 sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
1968 .bind(parent_id)
1969 .fetch_one(&ctx.pool)
1970 .await
1971 .unwrap();
1972
1973 assert_eq!(parent.0, "Integration Test Plan");
1974 assert_eq!(parent.1, "Test plan execution end-to-end");
1975 assert_eq!(parent.2, 2); assert_eq!(parent.3, None); let subtask_a: (String, Option<i64>) =
1980 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
1981 .bind(subtask_a_id)
1982 .fetch_one(&ctx.pool)
1983 .await
1984 .unwrap();
1985
1986 assert_eq!(subtask_a.0, "Subtask A");
1987 assert_eq!(subtask_a.1, Some(parent_id)); let dep: (i64, i64) = sqlx::query_as(
1991 "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
1992 )
1993 .bind(subtask_b_id)
1994 .fetch_one(&ctx.pool)
1995 .await
1996 .unwrap();
1997
1998 assert_eq!(dep.0, subtask_a_id); assert_eq!(dep.1, subtask_b_id); }
2001
2002 #[tokio::test]
2003 async fn test_plan_executor_idempotency() {
2004 use crate::test_utils::test_helpers::TestContext;
2005
2006 let ctx = TestContext::new().await;
2007
2008 let request = PlanRequest {
2010 tasks: vec![TaskTree {
2011 name: Some("Idempotent Task".to_string()),
2012 spec: Some("Initial spec".to_string()),
2013 priority: Some(PriorityValue::High),
2014 children: Some(vec![
2015 TaskTree {
2016 name: Some("Child 1".to_string()),
2017 spec: Some("Child spec 1".to_string()),
2018 priority: None,
2019 children: None,
2020 depends_on: None,
2021 id: None,
2022 status: None,
2023 active_form: None,
2024 parent_id: None,
2025 ..Default::default()
2026 },
2027 TaskTree {
2028 name: Some("Child 2".to_string()),
2029 spec: Some("Child spec 2".to_string()),
2030 priority: Some(PriorityValue::Low),
2031 children: None,
2032 depends_on: None,
2033 id: None,
2034 status: None,
2035 active_form: None,
2036 parent_id: None,
2037 ..Default::default()
2038 },
2039 ]),
2040 depends_on: None,
2041 id: None,
2042 status: None,
2043 active_form: None,
2044 parent_id: None,
2045 ..Default::default()
2046 }],
2047 };
2048
2049 let executor = PlanExecutor::new(&ctx.pool);
2050
2051 let result1 = executor.execute(&request).await.unwrap();
2053 assert!(result1.success, "First execution should succeed");
2054 assert_eq!(result1.created_count, 3, "Should create 3 tasks");
2055 assert_eq!(result1.updated_count, 0, "Should not update any tasks");
2056 assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
2057
2058 let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
2060 let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
2061 let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
2062
2063 let result2 = executor.execute(&request).await.unwrap();
2065 assert!(result2.success, "Second execution should succeed");
2066 assert_eq!(result2.created_count, 0, "Should not create any new tasks");
2067 assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
2068 assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
2069
2070 let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
2072 let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
2073 let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
2074
2075 assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
2076 assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
2077 assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
2078
2079 let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
2081 .bind(parent_id_2)
2082 .fetch_one(&ctx.pool)
2083 .await
2084 .unwrap();
2085
2086 assert_eq!(parent.0, "Initial spec");
2087 assert_eq!(parent.1, 2); let modified_request = PlanRequest {
2091 tasks: vec![TaskTree {
2092 name: Some("Idempotent Task".to_string()),
2093 spec: Some("Updated spec".to_string()), priority: Some(PriorityValue::Critical), children: Some(vec![
2096 TaskTree {
2097 name: Some("Child 1".to_string()),
2098 spec: Some("Updated child spec 1".to_string()), priority: None,
2100 children: None,
2101 depends_on: None,
2102 id: None,
2103 status: None,
2104 active_form: None,
2105 parent_id: None,
2106 ..Default::default()
2107 },
2108 TaskTree {
2109 name: Some("Child 2".to_string()),
2110 spec: Some("Child spec 2".to_string()), priority: Some(PriorityValue::Low),
2112 children: None,
2113 depends_on: None,
2114 id: None,
2115 status: None,
2116 active_form: None,
2117 parent_id: None,
2118 ..Default::default()
2119 },
2120 ]),
2121 depends_on: None,
2122 id: None,
2123 status: None,
2124 active_form: None,
2125 parent_id: None,
2126 ..Default::default()
2127 }],
2128 };
2129
2130 let result3 = executor.execute(&modified_request).await.unwrap();
2131 assert!(result3.success, "Third execution should succeed");
2132 assert_eq!(result3.created_count, 0, "Should not create any new tasks");
2133 assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
2134
2135 let updated_parent: (String, i64) =
2137 sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
2138 .bind(parent_id_2)
2139 .fetch_one(&ctx.pool)
2140 .await
2141 .unwrap();
2142
2143 assert_eq!(updated_parent.0, "Updated spec");
2144 assert_eq!(updated_parent.1, 1); let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
2147 .bind(child1_id_2)
2148 .fetch_one(&ctx.pool)
2149 .await
2150 .unwrap();
2151
2152 assert_eq!(updated_child1.0, "Updated child spec 1");
2153 }
2154
2155 #[tokio::test]
2156 async fn test_plan_executor_dependencies() {
2157 use crate::test_utils::test_helpers::TestContext;
2158
2159 let ctx = TestContext::new().await;
2160
2161 let request = PlanRequest {
2163 tasks: vec![
2164 TaskTree {
2165 name: Some("Foundation".to_string()),
2166 spec: Some("Base layer".to_string()),
2167 priority: Some(PriorityValue::Critical),
2168 children: None,
2169 depends_on: None,
2170 id: None,
2171 status: None,
2172 active_form: None,
2173 parent_id: None,
2174 ..Default::default()
2175 },
2176 TaskTree {
2177 name: Some("Layer 1".to_string()),
2178 spec: Some("Depends on Foundation".to_string()),
2179 priority: Some(PriorityValue::High),
2180 children: None,
2181 depends_on: Some(vec!["Foundation".to_string()]),
2182 id: None,
2183 status: None,
2184 active_form: None,
2185 parent_id: None,
2186 ..Default::default()
2187 },
2188 TaskTree {
2189 name: Some("Layer 2".to_string()),
2190 spec: Some("Depends on Layer 1".to_string()),
2191 priority: None,
2192 children: None,
2193 depends_on: Some(vec!["Layer 1".to_string()]),
2194 id: None,
2195 status: None,
2196 active_form: None,
2197 parent_id: None,
2198 ..Default::default()
2199 },
2200 TaskTree {
2201 name: Some("Integration".to_string()),
2202 spec: Some("Depends on both Foundation and Layer 2".to_string()),
2203 priority: None,
2204 children: None,
2205 depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
2206 id: None,
2207 status: None,
2208 active_form: None,
2209 parent_id: None,
2210 ..Default::default()
2211 },
2212 ],
2213 };
2214
2215 let executor = PlanExecutor::new(&ctx.pool);
2216 let result = executor.execute(&request).await.unwrap();
2217
2218 assert!(result.success, "Plan execution should succeed");
2219 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2220 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2221
2222 let foundation_id = *result.task_id_map.get("Foundation").unwrap();
2224 let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
2225 let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
2226 let integration_id = *result.task_id_map.get("Integration").unwrap();
2227
2228 let deps1: Vec<(i64,)> =
2230 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
2231 .bind(layer1_id)
2232 .fetch_all(&ctx.pool)
2233 .await
2234 .unwrap();
2235
2236 assert_eq!(deps1.len(), 1);
2237 assert_eq!(deps1[0].0, foundation_id);
2238
2239 let deps2: Vec<(i64,)> =
2241 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
2242 .bind(layer2_id)
2243 .fetch_all(&ctx.pool)
2244 .await
2245 .unwrap();
2246
2247 assert_eq!(deps2.len(), 1);
2248 assert_eq!(deps2[0].0, layer1_id);
2249
2250 let deps3: Vec<(i64,)> =
2252 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
2253 .bind(integration_id)
2254 .fetch_all(&ctx.pool)
2255 .await
2256 .unwrap();
2257
2258 assert_eq!(deps3.len(), 2);
2259 let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
2260 blocking_ids.sort();
2261
2262 let mut expected_ids = vec![foundation_id, layer2_id];
2263 expected_ids.sort();
2264
2265 assert_eq!(blocking_ids, expected_ids);
2266 }
2267
2268 #[tokio::test]
2269 async fn test_plan_executor_invalid_dependency() {
2270 use crate::test_utils::test_helpers::TestContext;
2271
2272 let ctx = TestContext::new().await;
2273
2274 let request = PlanRequest {
2276 tasks: vec![TaskTree {
2277 name: Some("Task A".to_string()),
2278 spec: Some("Depends on non-existent task".to_string()),
2279 priority: None,
2280 children: None,
2281 depends_on: Some(vec!["NonExistent".to_string()]),
2282 id: None,
2283 status: None,
2284 active_form: None,
2285 parent_id: None,
2286 ..Default::default()
2287 }],
2288 };
2289
2290 let executor = PlanExecutor::new(&ctx.pool);
2291 let result = executor.execute(&request).await.unwrap();
2292
2293 assert!(!result.success, "Plan execution should fail");
2294 assert!(result.error.is_some(), "Should have error message");
2295 let error = result.error.unwrap();
2296 assert!(
2297 error.contains("NonExistent"),
2298 "Error should mention the missing dependency: {}",
2299 error
2300 );
2301 }
2302
2303 #[tokio::test]
2304 async fn test_plan_executor_simple_cycle() {
2305 use crate::test_utils::test_helpers::TestContext;
2306
2307 let ctx = TestContext::new().await;
2308
2309 let request = PlanRequest {
2311 tasks: vec![
2312 TaskTree {
2313 name: Some("Task A".to_string()),
2314 spec: Some("Depends on B".to_string()),
2315 priority: None,
2316 children: None,
2317 depends_on: Some(vec!["Task B".to_string()]),
2318 id: None,
2319 status: None,
2320 active_form: None,
2321 parent_id: None,
2322 ..Default::default()
2323 },
2324 TaskTree {
2325 name: Some("Task B".to_string()),
2326 spec: Some("Depends on A".to_string()),
2327 priority: None,
2328 children: None,
2329 depends_on: Some(vec!["Task A".to_string()]),
2330 id: None,
2331 status: None,
2332 active_form: None,
2333 parent_id: None,
2334 ..Default::default()
2335 },
2336 ],
2337 };
2338
2339 let executor = PlanExecutor::new(&ctx.pool);
2340 let result = executor.execute(&request).await.unwrap();
2341
2342 assert!(!result.success, "Plan execution should fail");
2343 assert!(result.error.is_some(), "Should have error message");
2344 let error = result.error.unwrap();
2345 assert!(
2346 error.contains("Circular dependency"),
2347 "Error should mention circular dependency: {}",
2348 error
2349 );
2350 assert!(
2351 error.contains("Task A") && error.contains("Task B"),
2352 "Error should mention both tasks in the cycle: {}",
2353 error
2354 );
2355 }
2356
2357 #[tokio::test]
2358 async fn test_plan_executor_complex_cycle() {
2359 use crate::test_utils::test_helpers::TestContext;
2360
2361 let ctx = TestContext::new().await;
2362
2363 let request = PlanRequest {
2365 tasks: vec![
2366 TaskTree {
2367 name: Some("Task A".to_string()),
2368 spec: Some("Depends on B".to_string()),
2369 priority: None,
2370 children: None,
2371 depends_on: Some(vec!["Task B".to_string()]),
2372 id: None,
2373 status: None,
2374 active_form: None,
2375 parent_id: None,
2376 ..Default::default()
2377 },
2378 TaskTree {
2379 name: Some("Task B".to_string()),
2380 spec: Some("Depends on C".to_string()),
2381 priority: None,
2382 children: None,
2383 depends_on: Some(vec!["Task C".to_string()]),
2384 id: None,
2385 status: None,
2386 active_form: None,
2387 parent_id: None,
2388 ..Default::default()
2389 },
2390 TaskTree {
2391 name: Some("Task C".to_string()),
2392 spec: Some("Depends on A".to_string()),
2393 priority: None,
2394 children: None,
2395 depends_on: Some(vec!["Task A".to_string()]),
2396 id: None,
2397 status: None,
2398 active_form: None,
2399 parent_id: None,
2400 ..Default::default()
2401 },
2402 ],
2403 };
2404
2405 let executor = PlanExecutor::new(&ctx.pool);
2406 let result = executor.execute(&request).await.unwrap();
2407
2408 assert!(!result.success, "Plan execution should fail");
2409 assert!(result.error.is_some(), "Should have error message");
2410 let error = result.error.unwrap();
2411 assert!(
2412 error.contains("Circular dependency"),
2413 "Error should mention circular dependency: {}",
2414 error
2415 );
2416 assert!(
2417 error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
2418 "Error should mention all tasks in the cycle: {}",
2419 error
2420 );
2421 }
2422
2423 #[tokio::test]
2424 async fn test_plan_executor_valid_dag() {
2425 use crate::test_utils::test_helpers::TestContext;
2426
2427 let ctx = TestContext::new().await;
2428
2429 let request = PlanRequest {
2436 tasks: vec![
2437 TaskTree {
2438 name: Some("Task A".to_string()),
2439 spec: Some("Root task".to_string()),
2440 priority: None,
2441 children: None,
2442 depends_on: None,
2443 id: None,
2444 status: None,
2445 active_form: None,
2446 parent_id: None,
2447 ..Default::default()
2448 },
2449 TaskTree {
2450 name: Some("Task B".to_string()),
2451 spec: Some("Depends on A".to_string()),
2452 priority: None,
2453 children: None,
2454 depends_on: Some(vec!["Task A".to_string()]),
2455 id: None,
2456 status: None,
2457 active_form: None,
2458 parent_id: None,
2459 ..Default::default()
2460 },
2461 TaskTree {
2462 name: Some("Task C".to_string()),
2463 spec: Some("Depends on A".to_string()),
2464 priority: None,
2465 children: None,
2466 depends_on: Some(vec!["Task A".to_string()]),
2467 id: None,
2468 status: None,
2469 active_form: None,
2470 parent_id: None,
2471 ..Default::default()
2472 },
2473 TaskTree {
2474 name: Some("Task D".to_string()),
2475 spec: Some("Depends on B and C".to_string()),
2476 priority: None,
2477 children: None,
2478 depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
2479 id: None,
2480 status: None,
2481 active_form: None,
2482 parent_id: None,
2483 ..Default::default()
2484 },
2485 ],
2486 };
2487
2488 let executor = PlanExecutor::new(&ctx.pool);
2489 let result = executor.execute(&request).await.unwrap();
2490
2491 assert!(
2492 result.success,
2493 "Plan execution should succeed for valid DAG"
2494 );
2495 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2496 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2497 }
2498
2499 #[tokio::test]
2500 async fn test_plan_executor_self_dependency() {
2501 use crate::test_utils::test_helpers::TestContext;
2502
2503 let ctx = TestContext::new().await;
2504
2505 let request = PlanRequest {
2507 tasks: vec![TaskTree {
2508 name: Some("Task A".to_string()),
2509 spec: Some("Depends on itself".to_string()),
2510 priority: None,
2511 children: None,
2512 depends_on: Some(vec!["Task A".to_string()]),
2513 id: None,
2514 status: None,
2515 active_form: None,
2516 parent_id: None,
2517 ..Default::default()
2518 }],
2519 };
2520
2521 let executor = PlanExecutor::new(&ctx.pool);
2522 let result = executor.execute(&request).await.unwrap();
2523
2524 assert!(
2525 !result.success,
2526 "Plan execution should fail for self-dependency"
2527 );
2528 assert!(result.error.is_some(), "Should have error message");
2529 let error = result.error.unwrap();
2530 assert!(
2531 error.contains("Circular dependency"),
2532 "Error should mention circular dependency: {}",
2533 error
2534 );
2535 }
2536
2537 #[tokio::test]
2539 async fn test_find_tasks_by_names_empty() {
2540 use crate::test_utils::test_helpers::TestContext;
2541
2542 let ctx = TestContext::new().await;
2543 let executor = PlanExecutor::new(&ctx.pool);
2544
2545 let result = executor.find_tasks_by_names(&[]).await.unwrap();
2546 assert!(result.is_empty(), "Empty input should return empty map");
2547 }
2548
2549 #[tokio::test]
2550 async fn test_find_tasks_by_names_partial() {
2551 use crate::test_utils::test_helpers::TestContext;
2552
2553 let ctx = TestContext::new().await;
2554 let executor = PlanExecutor::new(&ctx.pool);
2555
2556 let request = PlanRequest {
2558 tasks: vec![
2559 TaskTree {
2560 name: Some("Task A".to_string()),
2561 spec: None,
2562 priority: None,
2563 children: None,
2564 depends_on: None,
2565 id: None,
2566 status: None,
2567 active_form: None,
2568 parent_id: None,
2569 ..Default::default()
2570 },
2571 TaskTree {
2572 name: Some("Task B".to_string()),
2573 spec: None,
2574 priority: None,
2575 children: None,
2576 depends_on: None,
2577 id: None,
2578 status: None,
2579 active_form: None,
2580 parent_id: None,
2581 ..Default::default()
2582 },
2583 ],
2584 };
2585 executor.execute(&request).await.unwrap();
2586
2587 let names = vec![
2589 "Task A".to_string(),
2590 "Task B".to_string(),
2591 "Task C".to_string(),
2592 ];
2593 let result = executor.find_tasks_by_names(&names).await.unwrap();
2594
2595 assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
2596 assert!(result.contains_key("Task A"));
2597 assert!(result.contains_key("Task B"));
2598 assert!(!result.contains_key("Task C"));
2599 }
2600
2601 #[tokio::test]
2603 async fn test_plan_1000_tasks_performance() {
2604 use crate::test_utils::test_helpers::TestContext;
2605
2606 let ctx = TestContext::new().await;
2607 let executor = PlanExecutor::new(&ctx.pool);
2608
2609 let mut tasks = Vec::new();
2611 for i in 0..1000 {
2612 tasks.push(TaskTree {
2613 name: Some(format!("Task {}", i)),
2614 spec: Some(format!("Spec for task {}", i)),
2615 priority: Some(PriorityValue::Medium),
2616 children: None,
2617 depends_on: None,
2618 id: None,
2619 status: None,
2620 active_form: None,
2621 parent_id: None,
2622 ..Default::default()
2623 });
2624 }
2625
2626 let request = PlanRequest { tasks };
2627
2628 let start = std::time::Instant::now();
2629 let result = executor.execute(&request).await.unwrap();
2630 let duration = start.elapsed();
2631
2632 assert!(result.success);
2633 assert_eq!(result.created_count, 1000);
2634 assert!(
2635 duration.as_secs() < 10,
2636 "Should complete 1000 tasks in under 10 seconds, took {:?}",
2637 duration
2638 );
2639
2640 println!("✅ Created 1000 tasks in {:?}", duration);
2641 }
2642
2643 #[tokio::test]
2644 async fn test_plan_deep_nesting_20_levels() {
2645 use crate::test_utils::test_helpers::TestContext;
2646
2647 let ctx = TestContext::new().await;
2648 let executor = PlanExecutor::new(&ctx.pool);
2649
2650 fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
2652 TaskTree {
2653 name: Some(format!("Level {}", current)),
2654 spec: Some(format!("Task at depth {}", current)),
2655 priority: Some(PriorityValue::Low),
2656 children: if current < depth {
2657 Some(vec![build_deep_tree(depth, current + 1)])
2658 } else {
2659 None
2660 },
2661 depends_on: None,
2662 id: None,
2663 status: None,
2664 active_form: None,
2665 parent_id: None,
2666 ..Default::default()
2667 }
2668 }
2669
2670 let request = PlanRequest {
2671 tasks: vec![build_deep_tree(20, 1)],
2672 };
2673
2674 let start = std::time::Instant::now();
2675 let result = executor.execute(&request).await.unwrap();
2676 let duration = start.elapsed();
2677
2678 assert!(result.success);
2679 assert_eq!(
2680 result.created_count, 20,
2681 "Should create 20 tasks (1 per level)"
2682 );
2683 assert!(
2684 duration.as_secs() < 5,
2685 "Should handle 20-level nesting in under 5 seconds, took {:?}",
2686 duration
2687 );
2688
2689 println!("✅ Created 20-level deep tree in {:?}", duration);
2690 }
2691
2692 #[test]
2693 fn test_flatten_preserves_all_fields() {
2694 let tasks = vec![TaskTree {
2695 name: Some("Full Task".to_string()),
2696 spec: Some("Detailed spec".to_string()),
2697 priority: Some(PriorityValue::Critical),
2698 children: None,
2699 depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2700 id: Some(42),
2701 status: None,
2702 active_form: None,
2703 parent_id: None,
2704 ..Default::default()
2705 }];
2706
2707 let flat = flatten_task_tree(&tasks);
2708 assert_eq!(flat.len(), 1);
2709
2710 let task = &flat[0];
2711 assert_eq!(task.name, Some("Full Task".to_string()));
2712 assert_eq!(task.spec, Some("Detailed spec".to_string()));
2713 assert_eq!(task.priority, Some(PriorityValue::Critical));
2714 assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2715 assert_eq!(task.id, Some(42));
2716 }
2717}
2718
2719#[cfg(test)]
2720mod status_alias_tests {
2721 use super::*;
2722
2723 #[test]
2725 fn test_from_db_str_canonical() {
2726 assert_eq!(TaskStatus::from_db_str("todo"), Some(TaskStatus::Todo));
2727 assert_eq!(TaskStatus::from_db_str("doing"), Some(TaskStatus::Doing));
2728 assert_eq!(TaskStatus::from_db_str("done"), Some(TaskStatus::Done));
2729 }
2730
2731 #[test]
2733 fn test_from_db_str_aliases() {
2734 assert_eq!(TaskStatus::from_db_str("pending"), Some(TaskStatus::Todo));
2735 assert_eq!(
2736 TaskStatus::from_db_str("in_progress"),
2737 Some(TaskStatus::Doing)
2738 );
2739 assert_eq!(TaskStatus::from_db_str("completed"), Some(TaskStatus::Done));
2740 }
2741
2742 #[test]
2743 fn test_from_db_str_unknown_returns_none() {
2744 assert_eq!(TaskStatus::from_db_str("running"), None);
2745 assert_eq!(TaskStatus::from_db_str(""), None);
2746 assert_eq!(TaskStatus::from_db_str("TODO"), None); }
2748
2749 #[test]
2751 fn test_as_db_str_canonical_output() {
2752 assert_eq!(TaskStatus::Todo.as_db_str(), "todo");
2753 assert_eq!(TaskStatus::Doing.as_db_str(), "doing");
2754 assert_eq!(TaskStatus::Done.as_db_str(), "done");
2755 }
2756
2757 #[test]
2759 fn test_serde_deserialize_canonical() {
2760 let s: TaskStatus = serde_json::from_str(r#""todo""#).unwrap();
2761 assert_eq!(s, TaskStatus::Todo);
2762 let s: TaskStatus = serde_json::from_str(r#""doing""#).unwrap();
2763 assert_eq!(s, TaskStatus::Doing);
2764 let s: TaskStatus = serde_json::from_str(r#""done""#).unwrap();
2765 assert_eq!(s, TaskStatus::Done);
2766 }
2767
2768 #[test]
2769 fn test_serde_deserialize_aliases() {
2770 let s: TaskStatus = serde_json::from_str(r#""pending""#).unwrap();
2771 assert_eq!(s, TaskStatus::Todo);
2772 let s: TaskStatus = serde_json::from_str(r#""in_progress""#).unwrap();
2773 assert_eq!(s, TaskStatus::Doing);
2774 let s: TaskStatus = serde_json::from_str(r#""completed""#).unwrap();
2775 assert_eq!(s, TaskStatus::Done);
2776 }
2777
2778 #[test]
2779 fn test_serde_serialize_always_canonical() {
2780 assert_eq!(
2783 serde_json::to_string(&TaskStatus::Todo).unwrap(),
2784 r#""todo""#
2785 );
2786 assert_eq!(
2787 serde_json::to_string(&TaskStatus::Doing).unwrap(),
2788 r#""doing""#
2789 );
2790 assert_eq!(
2791 serde_json::to_string(&TaskStatus::Done).unwrap(),
2792 r#""done""#
2793 );
2794 }
2795
2796 #[test]
2799 fn test_task_tree_status_alias_roundtrip() {
2800 let json = r#"{"name":"T","status":"pending"}"#;
2801 let task: TaskTree = serde_json::from_str(json).unwrap();
2802 assert_eq!(task.status, Some(TaskStatus::Todo));
2803 let out = serde_json::to_string(&task.status.unwrap()).unwrap();
2805 assert_eq!(out, r#""todo""#);
2806 }
2807}
2808
2809#[cfg(test)]
2810mod dataflow_tests {
2811 use super::*;
2812 use crate::tasks::TaskManager;
2813 use crate::test_utils::test_helpers::TestContext;
2814
2815 #[tokio::test]
2816 async fn test_complete_dataflow_status_and_active_form() {
2817 let ctx = TestContext::new().await;
2819
2820 let request = PlanRequest {
2822 tasks: vec![TaskTree {
2823 name: Some("Test Active Form Task".to_string()),
2824 spec: Some("Testing complete dataflow".to_string()),
2825 priority: Some(PriorityValue::High),
2826 children: None,
2827 depends_on: None,
2828 id: None,
2829 status: Some(TaskStatus::Doing),
2830 active_form: Some("Testing complete dataflow now".to_string()),
2831 parent_id: None,
2832 ..Default::default()
2833 }],
2834 };
2835
2836 let executor = PlanExecutor::new(&ctx.pool);
2837 let result = executor.execute(&request).await.unwrap();
2838
2839 assert!(result.success);
2840 assert_eq!(result.created_count, 1);
2841
2842 let task_mgr = TaskManager::new(&ctx.pool);
2844 let result = task_mgr
2845 .find_tasks(None, None, None, None, None)
2846 .await
2847 .unwrap();
2848
2849 assert_eq!(result.tasks.len(), 1);
2850 let task = &result.tasks[0];
2851
2852 assert_eq!(task.name, "Test Active Form Task");
2854 assert_eq!(task.status, "doing"); assert_eq!(
2856 task.active_form,
2857 Some("Testing complete dataflow now".to_string())
2858 );
2859
2860 let json = serde_json::to_value(task).unwrap();
2862 assert_eq!(json["name"], "Test Active Form Task");
2863 assert_eq!(json["status"], "doing");
2864 assert_eq!(json["active_form"], "Testing complete dataflow now");
2865
2866 println!("✅ 完整数据流验证成功!");
2867 println!(" Plan工具写入 -> Task读取 -> JSON序列化 -> MCP输出");
2868 println!(" active_form: {:?}", task.active_form);
2869 }
2870}
2871
2872#[cfg(test)]
2873mod parent_id_tests {
2874 use super::*;
2875 use crate::test_utils::test_helpers::TestContext;
2876
2877 #[test]
2878 fn test_parent_id_json_deserialization_absent() {
2879 let json = r#"{"name": "Test Task"}"#;
2881 let task: TaskTree = serde_json::from_str(json).unwrap();
2882 assert_eq!(task.parent_id, None);
2883 }
2884
2885 #[test]
2886 fn test_parent_id_json_deserialization_null() {
2887 let json = r#"{"name": "Test Task", "parent_id": null}"#;
2889 let task: TaskTree = serde_json::from_str(json).unwrap();
2890 assert_eq!(task.parent_id, Some(None));
2891 }
2892
2893 #[test]
2894 fn test_parent_id_json_deserialization_number() {
2895 let json = r#"{"name": "Test Task", "parent_id": 42}"#;
2897 let task: TaskTree = serde_json::from_str(json).unwrap();
2898 assert_eq!(task.parent_id, Some(Some(42)));
2899 }
2900
2901 #[test]
2902 fn test_flatten_propagates_parent_id() {
2903 let tasks = vec![TaskTree {
2904 name: Some("Task with explicit parent".to_string()),
2905 spec: None,
2906 priority: None,
2907 children: None,
2908 depends_on: None,
2909 id: None,
2910 status: None,
2911 active_form: None,
2912 parent_id: Some(Some(99)),
2913 ..Default::default()
2914 }];
2915
2916 let flat = flatten_task_tree(&tasks);
2917 assert_eq!(flat.len(), 1);
2918 assert_eq!(flat[0].explicit_parent_id, Some(Some(99)));
2919 }
2920
2921 #[test]
2922 fn test_flatten_propagates_null_parent_id() {
2923 let tasks = vec![TaskTree {
2924 name: Some("Explicit root task".to_string()),
2925 spec: None,
2926 priority: None,
2927 children: None,
2928 depends_on: None,
2929 id: None,
2930 status: None,
2931 active_form: None,
2932 parent_id: Some(None), ..Default::default()
2934 }];
2935
2936 let flat = flatten_task_tree(&tasks);
2937 assert_eq!(flat.len(), 1);
2938 assert_eq!(flat[0].explicit_parent_id, Some(None));
2939 }
2940
2941 #[tokio::test]
2942 async fn test_explicit_parent_id_sets_parent() {
2943 let ctx = TestContext::new().await;
2944
2945 let request1 = PlanRequest {
2947 tasks: vec![TaskTree {
2948 name: Some("Parent Task".to_string()),
2949 spec: Some("This is the parent".to_string()),
2950 priority: None,
2951 children: None,
2952 depends_on: None,
2953 id: None,
2954 status: Some(TaskStatus::Doing),
2955 active_form: None,
2956 parent_id: None,
2957 ..Default::default()
2958 }],
2959 };
2960
2961 let executor = PlanExecutor::new(&ctx.pool);
2962 let result1 = executor.execute(&request1).await.unwrap();
2963 assert!(result1.success);
2964 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
2965
2966 let request2 = PlanRequest {
2968 tasks: vec![TaskTree {
2969 name: Some("Child Task".to_string()),
2970 spec: Some("This uses explicit parent_id".to_string()),
2971 priority: None,
2972 children: None,
2973 depends_on: None,
2974 id: None,
2975 status: None,
2976 active_form: None,
2977 parent_id: Some(Some(parent_id)),
2978 ..Default::default()
2979 }],
2980 };
2981
2982 let result2 = executor.execute(&request2).await.unwrap();
2983 assert!(result2.success);
2984 let child_id = *result2.task_id_map.get("Child Task").unwrap();
2985
2986 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2988 .bind(child_id)
2989 .fetch_one(&ctx.pool)
2990 .await
2991 .unwrap();
2992 assert_eq!(row.0, Some(parent_id));
2993 }
2994
2995 #[tokio::test]
2996 async fn test_explicit_null_parent_id_creates_root() {
2997 let ctx = TestContext::new().await;
2998
2999 let request = PlanRequest {
3002 tasks: vec![TaskTree {
3003 name: Some("Explicit Root Task".to_string()),
3004 spec: Some("Should be root despite default parent".to_string()),
3005 priority: None,
3006 children: None,
3007 depends_on: None,
3008 id: None,
3009 status: Some(TaskStatus::Doing),
3010 active_form: None,
3011 parent_id: Some(None), ..Default::default()
3013 }],
3014 };
3015
3016 let parent_request = PlanRequest {
3019 tasks: vec![TaskTree {
3020 name: Some("Default Parent".to_string()),
3021 spec: None,
3022 priority: None,
3023 children: None,
3024 depends_on: None,
3025 id: None,
3026 status: None,
3027 active_form: None,
3028 parent_id: None,
3029 ..Default::default()
3030 }],
3031 };
3032 let executor = PlanExecutor::new(&ctx.pool);
3033 let parent_result = executor.execute(&parent_request).await.unwrap();
3034 let default_parent_id = *parent_result.task_id_map.get("Default Parent").unwrap();
3035
3036 let executor_with_default =
3038 PlanExecutor::new(&ctx.pool).with_default_parent(default_parent_id);
3039 let result = executor_with_default.execute(&request).await.unwrap();
3040 assert!(result.success);
3041 let task_id = *result.task_id_map.get("Explicit Root Task").unwrap();
3042
3043 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3045 .bind(task_id)
3046 .fetch_one(&ctx.pool)
3047 .await
3048 .unwrap();
3049 assert_eq!(
3050 row.0, None,
3051 "Task with explicit null parent_id should be root"
3052 );
3053 }
3054
3055 #[tokio::test]
3056 async fn test_children_nesting_takes_precedence_over_parent_id() {
3057 let ctx = TestContext::new().await;
3058
3059 let request = PlanRequest {
3061 tasks: vec![TaskTree {
3062 name: Some("Parent via Nesting".to_string()),
3063 spec: Some("Test parent spec".to_string()),
3064 priority: None,
3065 children: Some(vec![TaskTree {
3066 name: Some("Child via Nesting".to_string()),
3067 spec: None,
3068 priority: None,
3069 children: None,
3070 depends_on: None,
3071 id: None,
3072 status: None,
3073 active_form: None,
3074 parent_id: Some(Some(999)), ..Default::default()
3076 }]),
3077 depends_on: None,
3078 id: None,
3079 status: Some(TaskStatus::Doing),
3080 active_form: None,
3081 parent_id: None,
3082 ..Default::default()
3083 }],
3084 };
3085
3086 let executor = PlanExecutor::new(&ctx.pool);
3087 let result = executor.execute(&request).await.unwrap();
3088 assert!(result.success);
3089
3090 let parent_id = *result.task_id_map.get("Parent via Nesting").unwrap();
3091 let child_id = *result.task_id_map.get("Child via Nesting").unwrap();
3092
3093 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3095 .bind(child_id)
3096 .fetch_one(&ctx.pool)
3097 .await
3098 .unwrap();
3099 assert_eq!(
3100 row.0,
3101 Some(parent_id),
3102 "Children nesting should take precedence"
3103 );
3104 }
3105
3106 #[tokio::test]
3107 async fn test_modify_existing_task_parent() {
3108 let ctx = TestContext::new().await;
3109 let executor = PlanExecutor::new(&ctx.pool);
3110
3111 let request1 = PlanRequest {
3113 tasks: vec![
3114 TaskTree {
3115 name: Some("Task A".to_string()),
3116 spec: Some("Task A spec".to_string()),
3117 priority: None,
3118 children: None,
3119 depends_on: None,
3120 id: None,
3121 status: Some(TaskStatus::Doing),
3122 active_form: None,
3123 parent_id: None,
3124 ..Default::default()
3125 },
3126 TaskTree {
3127 name: Some("Task B".to_string()),
3128 spec: None,
3129 priority: None,
3130 children: None,
3131 depends_on: None,
3132 id: None,
3133 status: None,
3134 active_form: None,
3135 parent_id: None,
3136 ..Default::default()
3137 },
3138 ],
3139 };
3140
3141 let result1 = executor.execute(&request1).await.unwrap();
3142 assert!(result1.success);
3143 let task_a_id = *result1.task_id_map.get("Task A").unwrap();
3144 let task_b_id = *result1.task_id_map.get("Task B").unwrap();
3145
3146 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3148 .bind(task_b_id)
3149 .fetch_one(&ctx.pool)
3150 .await
3151 .unwrap();
3152 assert_eq!(row.0, None, "Task B should initially be root");
3153
3154 let request2 = PlanRequest {
3156 tasks: vec![TaskTree {
3157 name: Some("Task B".to_string()), spec: None,
3159 priority: None,
3160 children: None,
3161 depends_on: None,
3162 id: None,
3163 status: None,
3164 active_form: None,
3165 parent_id: Some(Some(task_a_id)), ..Default::default()
3167 }],
3168 };
3169
3170 let result2 = executor.execute(&request2).await.unwrap();
3171 assert!(result2.success);
3172 assert_eq!(result2.updated_count, 1, "Should update existing task");
3173
3174 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3176 .bind(task_b_id)
3177 .fetch_one(&ctx.pool)
3178 .await
3179 .unwrap();
3180 assert_eq!(
3181 row.0,
3182 Some(task_a_id),
3183 "Task B should now be child of Task A"
3184 );
3185 }
3186
3187 #[tokio::test]
3188 async fn test_plan_done_with_incomplete_children_fails() {
3189 let ctx = TestContext::new().await;
3190 let executor = PlanExecutor::new(&ctx.pool);
3191
3192 let request1 = PlanRequest {
3194 tasks: vec![TaskTree {
3195 name: Some("Parent Task".to_string()),
3196 spec: Some("Parent spec".to_string()),
3197 priority: None,
3198 children: Some(vec![TaskTree {
3199 name: Some("Child Task".to_string()),
3200 spec: None,
3201 priority: None,
3202 children: None,
3203 depends_on: None,
3204 id: None,
3205 status: Some(TaskStatus::Todo), active_form: None,
3207 parent_id: None,
3208 ..Default::default()
3209 }]),
3210 depends_on: None,
3211 id: None,
3212 status: Some(TaskStatus::Doing),
3213 active_form: None,
3214 parent_id: None,
3215 ..Default::default()
3216 }],
3217 };
3218
3219 let result1 = executor.execute(&request1).await.unwrap();
3220 assert!(result1.success);
3221
3222 let request2 = PlanRequest {
3224 tasks: vec![TaskTree {
3225 name: Some("Parent Task".to_string()),
3226 spec: None,
3227 priority: None,
3228 children: None,
3229 depends_on: None,
3230 id: None,
3231 status: Some(TaskStatus::Done), active_form: None,
3233 parent_id: None,
3234 ..Default::default()
3235 }],
3236 };
3237
3238 let result2 = executor.execute(&request2).await.unwrap();
3239 assert!(!result2.success, "Should fail when child is incomplete");
3240 assert!(
3241 result2
3242 .error
3243 .as_ref()
3244 .unwrap()
3245 .contains("Uncompleted children"),
3246 "Error should mention uncompleted children: {:?}",
3247 result2.error
3248 );
3249 }
3250
3251 #[tokio::test]
3252 async fn test_plan_done_with_completed_children_succeeds() {
3253 let ctx = TestContext::new().await;
3254 let executor = PlanExecutor::new(&ctx.pool);
3255
3256 let request1 = PlanRequest {
3258 tasks: vec![TaskTree {
3259 name: Some("Parent Task".to_string()),
3260 spec: Some("Parent spec".to_string()),
3261 priority: None,
3262 children: Some(vec![TaskTree {
3263 name: Some("Child Task".to_string()),
3264 spec: None,
3265 priority: None,
3266 children: None,
3267 depends_on: None,
3268 id: None,
3269 status: Some(TaskStatus::Todo),
3270 active_form: None,
3271 parent_id: None,
3272 ..Default::default()
3273 }]),
3274 depends_on: None,
3275 id: None,
3276 status: Some(TaskStatus::Doing),
3277 active_form: None,
3278 parent_id: None,
3279 ..Default::default()
3280 }],
3281 };
3282
3283 let result1 = executor.execute(&request1).await.unwrap();
3284 assert!(result1.success);
3285
3286 let request2 = PlanRequest {
3288 tasks: vec![TaskTree {
3289 name: Some("Child Task".to_string()),
3290 spec: None,
3291 priority: None,
3292 children: None,
3293 depends_on: None,
3294 id: None,
3295 status: Some(TaskStatus::Done),
3296 active_form: None,
3297 parent_id: None,
3298 ..Default::default()
3299 }],
3300 };
3301
3302 let result2 = executor.execute(&request2).await.unwrap();
3303 assert!(result2.success);
3304
3305 let request3 = PlanRequest {
3307 tasks: vec![TaskTree {
3308 name: Some("Parent Task".to_string()),
3309 spec: None,
3310 priority: None,
3311 children: None,
3312 depends_on: None,
3313 id: None,
3314 status: Some(TaskStatus::Done),
3315 active_form: None,
3316 parent_id: None,
3317 ..Default::default()
3318 }],
3319 };
3320
3321 let result3 = executor.execute(&request3).await.unwrap();
3322 assert!(result3.success, "Should succeed when child is complete");
3323 }
3324}
3325
3326#[cfg(test)]
3327mod delete_tests {
3328 use super::*;
3329 use crate::test_utils::test_helpers::TestContext;
3330
3331 #[tokio::test]
3332 async fn test_delete_task_by_id_only() {
3333 let ctx = TestContext::new().await;
3334 let executor = PlanExecutor::new(&ctx.pool);
3335
3336 let request1 = PlanRequest {
3338 tasks: vec![TaskTree {
3339 name: Some("Task to delete".to_string()),
3340 spec: Some("This will be deleted".to_string()),
3341 priority: None,
3342 children: None,
3343 depends_on: None,
3344 id: None,
3345 status: None,
3346 active_form: None,
3347 parent_id: None,
3348 ..Default::default()
3349 }],
3350 };
3351
3352 let result1 = executor.execute(&request1).await.unwrap();
3353 assert!(result1.success);
3354 let task_id = *result1.task_id_map.get("Task to delete").unwrap();
3355
3356 let exists: (i64,) =
3358 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
3359 .bind(task_id)
3360 .fetch_one(&ctx.pool)
3361 .await
3362 .unwrap();
3363 assert_eq!(exists.0, 1, "Task should exist");
3364
3365 let request2 = PlanRequest {
3367 tasks: vec![TaskTree {
3368 name: None, spec: None,
3370 priority: None,
3371 children: None,
3372 depends_on: None,
3373 id: Some(task_id),
3374 status: None,
3375 active_form: None,
3376 parent_id: None,
3377 delete: Some(true),
3378 }],
3379 };
3380
3381 let result2 = executor.execute(&request2).await.unwrap();
3382 assert!(result2.success, "Delete should succeed");
3383 assert_eq!(result2.deleted_count, 1, "Should delete 1 task");
3384 assert_eq!(result2.created_count, 0);
3385 assert_eq!(result2.updated_count, 0);
3386
3387 let exists: (i64,) =
3389 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
3390 .bind(task_id)
3391 .fetch_one(&ctx.pool)
3392 .await
3393 .unwrap();
3394 assert_eq!(exists.0, 0, "Task should be deleted");
3395 }
3396
3397 #[tokio::test]
3398 async fn test_delete_requires_id() {
3399 let ctx = TestContext::new().await;
3400 let executor = PlanExecutor::new(&ctx.pool);
3401
3402 let request = PlanRequest {
3404 tasks: vec![TaskTree {
3405 name: Some("Task name without id".to_string()),
3406 spec: None,
3407 priority: None,
3408 children: None,
3409 depends_on: None,
3410 id: None, status: None,
3412 active_form: None,
3413 parent_id: None,
3414 delete: Some(true),
3415 }],
3416 };
3417
3418 let result = executor.execute(&request).await.unwrap();
3419 assert!(!result.success, "Delete without id should fail");
3420 assert!(
3421 result.error.as_ref().unwrap().contains("id"),
3422 "Error should mention 'id' requirement"
3423 );
3424 }
3425
3426 #[tokio::test]
3427 async fn test_delete_with_json_syntax() {
3428 let ctx = TestContext::new().await;
3429 let executor = PlanExecutor::new(&ctx.pool);
3430
3431 let request1 = PlanRequest {
3433 tasks: vec![TaskTree {
3434 name: Some("JSON delete test".to_string()),
3435 spec: None,
3436 priority: None,
3437 children: None,
3438 depends_on: None,
3439 id: None,
3440 status: None,
3441 active_form: None,
3442 parent_id: None,
3443 ..Default::default()
3444 }],
3445 };
3446
3447 let result1 = executor.execute(&request1).await.unwrap();
3448 assert!(result1.success);
3449 let task_id = *result1.task_id_map.get("JSON delete test").unwrap();
3450
3451 let json = format!(r#"{{"tasks": [{{"id": {}, "delete": true}}]}}"#, task_id);
3453 let request2: PlanRequest = serde_json::from_str(&json).unwrap();
3454
3455 let result2 = executor.execute(&request2).await.unwrap();
3456 assert!(result2.success, "Delete via JSON should succeed");
3457 assert_eq!(result2.deleted_count, 1);
3458
3459 let exists: (i64,) =
3461 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
3462 .bind(task_id)
3463 .fetch_one(&ctx.pool)
3464 .await
3465 .unwrap();
3466 assert_eq!(exists.0, 0, "Task should be deleted");
3467 }
3468
3469 #[tokio::test]
3470 async fn test_mixed_create_update_delete_in_one_request() {
3471 let ctx = TestContext::new().await;
3472 let executor = PlanExecutor::new(&ctx.pool);
3473
3474 let request1 = PlanRequest {
3476 tasks: vec![
3477 TaskTree {
3478 name: Some("To Update".to_string()),
3479 spec: Some("Original spec".to_string()),
3480 priority: None,
3481 children: None,
3482 depends_on: None,
3483 id: None,
3484 status: None,
3485 active_form: None,
3486 parent_id: None,
3487 ..Default::default()
3488 },
3489 TaskTree {
3490 name: Some("To Delete".to_string()),
3491 spec: None,
3492 priority: None,
3493 children: None,
3494 depends_on: None,
3495 id: None,
3496 status: None,
3497 active_form: None,
3498 parent_id: None,
3499 ..Default::default()
3500 },
3501 ],
3502 };
3503
3504 let result1 = executor.execute(&request1).await.unwrap();
3505 assert!(result1.success);
3506 let delete_id = *result1.task_id_map.get("To Delete").unwrap();
3507
3508 let request2 = PlanRequest {
3510 tasks: vec![
3511 TaskTree {
3513 name: Some("Newly Created".to_string()),
3514 spec: Some("Brand new".to_string()),
3515 priority: None,
3516 children: None,
3517 depends_on: None,
3518 id: None,
3519 status: None,
3520 active_form: None,
3521 parent_id: None,
3522 ..Default::default()
3523 },
3524 TaskTree {
3526 name: Some("To Update".to_string()),
3527 spec: Some("Updated spec".to_string()),
3528 priority: None,
3529 children: None,
3530 depends_on: None,
3531 id: None,
3532 status: None,
3533 active_form: None,
3534 parent_id: None,
3535 ..Default::default()
3536 },
3537 TaskTree {
3539 name: None,
3540 spec: None,
3541 priority: None,
3542 children: None,
3543 depends_on: None,
3544 id: Some(delete_id),
3545 status: None,
3546 active_form: None,
3547 parent_id: None,
3548 delete: Some(true),
3549 },
3550 ],
3551 };
3552
3553 let result2 = executor.execute(&request2).await.unwrap();
3554 assert!(result2.success);
3555 assert_eq!(result2.created_count, 1, "Should create 1 task");
3556 assert_eq!(result2.updated_count, 1, "Should update 1 task");
3557 assert_eq!(result2.deleted_count, 1, "Should delete 1 task");
3558
3559 let exists: (i64,) =
3561 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
3562 .bind(delete_id)
3563 .fetch_one(&ctx.pool)
3564 .await
3565 .unwrap();
3566 assert_eq!(exists.0, 0, "Deleted task should not exist");
3567 }
3568
3569 #[tokio::test]
3570 async fn test_delete_nonexistent_id_returns_warning() {
3571 let ctx = TestContext::new().await;
3572 let executor = PlanExecutor::new(&ctx.pool);
3573
3574 let request = PlanRequest {
3576 tasks: vec![TaskTree {
3577 name: None,
3578 spec: None,
3579 priority: None,
3580 children: None,
3581 depends_on: None,
3582 id: Some(99999), status: None,
3584 active_form: None,
3585 parent_id: None,
3586 delete: Some(true),
3587 }],
3588 };
3589
3590 let result = executor.execute(&request).await.unwrap();
3591
3592 assert!(
3594 result.success,
3595 "Delete of non-existent ID should still succeed"
3596 );
3597 assert_eq!(
3598 result.deleted_count, 0,
3599 "Should not count non-existent task as deleted"
3600 );
3601 assert!(
3602 !result.warnings.is_empty(),
3603 "Should have warning about non-existent task"
3604 );
3605 assert!(
3606 result.warnings[0].contains("not found"),
3607 "Warning should mention task not found: {:?}",
3608 result.warnings
3609 );
3610 }
3611
3612 #[tokio::test]
3613 async fn test_cascade_delete_reports_descendants() {
3614 let ctx = TestContext::new().await;
3615 let executor = PlanExecutor::new(&ctx.pool);
3616
3617 let request1 = PlanRequest {
3619 tasks: vec![TaskTree {
3620 name: Some("Parent Task".to_string()),
3621 spec: None,
3622 priority: None,
3623 children: Some(vec![
3624 TaskTree {
3625 name: Some("Child 1".to_string()),
3626 spec: None,
3627 priority: None,
3628 children: None,
3629 depends_on: None,
3630 id: None,
3631 status: None,
3632 active_form: None,
3633 parent_id: None,
3634 ..Default::default()
3635 },
3636 TaskTree {
3637 name: Some("Child 2".to_string()),
3638 spec: None,
3639 priority: None,
3640 children: None,
3641 depends_on: None,
3642 id: None,
3643 status: None,
3644 active_form: None,
3645 parent_id: None,
3646 ..Default::default()
3647 },
3648 ]),
3649 depends_on: None,
3650 id: None,
3651 status: None,
3652 active_form: None,
3653 parent_id: None,
3654 ..Default::default()
3655 }],
3656 };
3657
3658 let result1 = executor.execute(&request1).await.unwrap();
3659 assert!(result1.success);
3660 assert_eq!(
3661 result1.created_count, 3,
3662 "Should create parent + 2 children"
3663 );
3664 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
3665
3666 let request2 = PlanRequest {
3668 tasks: vec![TaskTree {
3669 name: None,
3670 spec: None,
3671 priority: None,
3672 children: None,
3673 depends_on: None,
3674 id: Some(parent_id),
3675 status: None,
3676 active_form: None,
3677 parent_id: None,
3678 delete: Some(true),
3679 }],
3680 };
3681
3682 let result2 = executor.execute(&request2).await.unwrap();
3683
3684 assert!(result2.success, "Cascade delete should succeed");
3685 assert_eq!(
3686 result2.deleted_count, 1,
3687 "deleted_count should only count direct deletes"
3688 );
3689 assert_eq!(
3690 result2.cascade_deleted_count, 2,
3691 "Should report 2 cascade-deleted children"
3692 );
3693 assert!(
3694 result2.warnings.iter().any(|w| w.contains("descendant")),
3695 "Should have warning about cascade-deleted descendants: {:?}",
3696 result2.warnings
3697 );
3698
3699 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE deleted_at IS NULL")
3701 .fetch_one(&ctx.pool)
3702 .await
3703 .unwrap();
3704 assert_eq!(count.0, 0, "All tasks should be deleted");
3705 }
3706
3707 #[tokio::test]
3708 async fn test_cascade_delete_deep_hierarchy() {
3709 let ctx = TestContext::new().await;
3710 let executor = PlanExecutor::new(&ctx.pool);
3711
3712 let request1 = PlanRequest {
3714 tasks: vec![TaskTree {
3715 name: Some("Root".to_string()),
3716 spec: None,
3717 priority: None,
3718 children: Some(vec![TaskTree {
3719 name: Some("Level1".to_string()),
3720 spec: None,
3721 priority: None,
3722 children: Some(vec![TaskTree {
3723 name: Some("Level2".to_string()),
3724 spec: None,
3725 priority: None,
3726 children: Some(vec![TaskTree {
3727 name: Some("Level3".to_string()),
3728 spec: None,
3729 priority: None,
3730 children: None,
3731 depends_on: None,
3732 id: None,
3733 status: None,
3734 active_form: None,
3735 parent_id: None,
3736 ..Default::default()
3737 }]),
3738 depends_on: None,
3739 id: None,
3740 status: None,
3741 active_form: None,
3742 parent_id: None,
3743 ..Default::default()
3744 }]),
3745 depends_on: None,
3746 id: None,
3747 status: None,
3748 active_form: None,
3749 parent_id: None,
3750 ..Default::default()
3751 }]),
3752 depends_on: None,
3753 id: None,
3754 status: None,
3755 active_form: None,
3756 parent_id: None,
3757 ..Default::default()
3758 }],
3759 };
3760
3761 let result1 = executor.execute(&request1).await.unwrap();
3762 assert!(result1.success);
3763 assert_eq!(result1.created_count, 4);
3764 let root_id = *result1.task_id_map.get("Root").unwrap();
3765
3766 let request2 = PlanRequest {
3768 tasks: vec![TaskTree {
3769 name: None,
3770 spec: None,
3771 priority: None,
3772 children: None,
3773 depends_on: None,
3774 id: Some(root_id),
3775 status: None,
3776 active_form: None,
3777 parent_id: None,
3778 delete: Some(true),
3779 }],
3780 };
3781
3782 let result2 = executor.execute(&request2).await.unwrap();
3783
3784 assert!(result2.success);
3785 assert_eq!(
3786 result2.deleted_count, 1,
3787 "Only root counted as direct delete"
3788 );
3789 assert_eq!(
3790 result2.cascade_deleted_count, 3,
3791 "Should cascade-delete 3 descendants"
3792 );
3793 }
3794
3795 #[tokio::test]
3796 async fn test_delete_multiple_ids_with_mixed_results() {
3797 let ctx = TestContext::new().await;
3798 let executor = PlanExecutor::new(&ctx.pool);
3799
3800 let request1 = PlanRequest {
3802 tasks: vec![TaskTree {
3803 name: Some("Existing Task".to_string()),
3804 spec: None,
3805 priority: None,
3806 children: None,
3807 depends_on: None,
3808 id: None,
3809 status: None,
3810 active_form: None,
3811 parent_id: None,
3812 ..Default::default()
3813 }],
3814 };
3815
3816 let result1 = executor.execute(&request1).await.unwrap();
3817 let existing_id = *result1.task_id_map.get("Existing Task").unwrap();
3818
3819 let request2 = PlanRequest {
3821 tasks: vec![
3822 TaskTree {
3823 name: None,
3824 spec: None,
3825 priority: None,
3826 children: None,
3827 depends_on: None,
3828 id: Some(existing_id),
3829 status: None,
3830 active_form: None,
3831 parent_id: None,
3832 delete: Some(true),
3833 },
3834 TaskTree {
3835 name: None,
3836 spec: None,
3837 priority: None,
3838 children: None,
3839 depends_on: None,
3840 id: Some(88888), status: None,
3842 active_form: None,
3843 parent_id: None,
3844 delete: Some(true),
3845 },
3846 ],
3847 };
3848
3849 let result2 = executor.execute(&request2).await.unwrap();
3850
3851 assert!(result2.success, "Mixed delete should still succeed");
3852 assert_eq!(result2.deleted_count, 1, "Only one task actually deleted");
3853 assert!(
3854 result2
3855 .warnings
3856 .iter()
3857 .any(|w| w.contains("88888") && w.contains("not found")),
3858 "Should warn about non-existent ID 88888: {:?}",
3859 result2.warnings
3860 );
3861 }
3862
3863 #[tokio::test]
3865 async fn test_delete_focused_task_returns_error() {
3866 let ctx = TestContext::new().await;
3867 let executor = PlanExecutor::new(&ctx.pool);
3868
3869 let request1 = PlanRequest {
3874 tasks: vec![TaskTree {
3875 name: Some("Focused Task".to_string()),
3876 spec: Some("## Goal\nTest focus deletion".to_string()),
3877 priority: None,
3878 children: None,
3879 depends_on: None,
3880 id: None,
3881 status: Some(TaskStatus::Doing),
3882 active_form: None,
3883 parent_id: None,
3884 ..Default::default()
3885 }],
3886 };
3887
3888 let result1 = executor.execute(&request1).await.unwrap();
3889 assert!(result1.success, "Create focused task should succeed");
3890 let task_id = *result1.task_id_map.get("Focused Task").unwrap();
3891
3892 let focus_check: Option<(i64,)> =
3894 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = '-1'")
3895 .fetch_optional(&ctx.pool)
3896 .await
3897 .unwrap();
3898 assert_eq!(
3899 focus_check.map(|r| r.0),
3900 Some(task_id),
3901 "Task should be the session's current focus"
3902 );
3903
3904 let request2 = PlanRequest {
3906 tasks: vec![TaskTree {
3907 name: None,
3908 spec: None,
3909 priority: None,
3910 children: None,
3911 depends_on: None,
3912 id: Some(task_id),
3913 status: None,
3914 active_form: None,
3915 parent_id: None,
3916 delete: Some(true),
3917 }],
3918 };
3919
3920 let result2 = executor.execute(&request2).await.unwrap();
3921
3922 assert!(!result2.success, "Delete focused task should fail");
3924 let error = result2.error.as_ref().unwrap();
3925 assert!(
3926 error.contains("focus"),
3927 "Error should mention focus: {}",
3928 error
3929 );
3930 assert_eq!(result2.deleted_count, 0, "Nothing should be deleted");
3931
3932 let exists: (i64,) =
3934 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
3935 .bind(task_id)
3936 .fetch_one(&ctx.pool)
3937 .await
3938 .unwrap();
3939 assert_eq!(exists.0, 1, "Focused task should NOT be deleted");
3940 }
3941
3942 #[tokio::test]
3944 async fn test_delete_duplicate_id_in_batch() {
3945 let ctx = TestContext::new().await;
3946 let executor = PlanExecutor::new(&ctx.pool);
3947
3948 let request1 = PlanRequest {
3950 tasks: vec![TaskTree {
3951 name: Some("Duplicate Delete Target".to_string()),
3952 spec: None,
3953 priority: None,
3954 children: None,
3955 depends_on: None,
3956 id: None,
3957 status: None,
3958 active_form: None,
3959 parent_id: None,
3960 ..Default::default()
3961 }],
3962 };
3963
3964 let result1 = executor.execute(&request1).await.unwrap();
3965 assert!(result1.success);
3966 let task_id = *result1.task_id_map.get("Duplicate Delete Target").unwrap();
3967
3968 let request2 = PlanRequest {
3970 tasks: vec![
3971 TaskTree {
3972 name: None,
3973 spec: None,
3974 priority: None,
3975 children: None,
3976 depends_on: None,
3977 id: Some(task_id),
3978 status: None,
3979 active_form: None,
3980 parent_id: None,
3981 delete: Some(true),
3982 },
3983 TaskTree {
3984 name: None,
3985 spec: None,
3986 priority: None,
3987 children: None,
3988 depends_on: None,
3989 id: Some(task_id), status: None,
3991 active_form: None,
3992 parent_id: None,
3993 delete: Some(true),
3994 },
3995 ],
3996 };
3997
3998 let result2 = executor.execute(&request2).await.unwrap();
3999
4000 assert!(result2.success, "Duplicate delete should still succeed");
4002 assert_eq!(
4003 result2.deleted_count, 1,
4004 "Only the first delete should count"
4005 );
4006
4007 let not_found_warnings: Vec<_> = result2
4009 .warnings
4010 .iter()
4011 .filter(|w| w.contains("not found"))
4012 .collect();
4013 assert_eq!(
4014 not_found_warnings.len(),
4015 1,
4016 "Should have exactly one 'not found' warning for the duplicate: {:?}",
4017 result2.warnings
4018 );
4019
4020 let exists: (i64,) =
4022 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
4023 .bind(task_id)
4024 .fetch_one(&ctx.pool)
4025 .await
4026 .unwrap();
4027 assert_eq!(exists.0, 0, "Task should be deleted");
4028 }
4029
4030 #[tokio::test]
4033 async fn test_delete_parent_blocked_when_child_is_focused() {
4034 let ctx = TestContext::new().await;
4035 let executor = PlanExecutor::new(&ctx.pool);
4036
4037 let request1 = PlanRequest {
4039 tasks: vec![TaskTree {
4040 name: Some("Parent Task".to_string()),
4041 spec: None,
4042 priority: None,
4043 children: Some(vec![TaskTree {
4044 name: Some("Child Task".to_string()),
4045 spec: Some("## Goal\nChild is focused".to_string()),
4046 priority: None,
4047 children: None,
4048 depends_on: None,
4049 id: None,
4050 status: Some(TaskStatus::Doing), active_form: None,
4052 parent_id: None,
4053 ..Default::default()
4054 }]),
4055 depends_on: None,
4056 id: None,
4057 status: None,
4058 active_form: None,
4059 parent_id: None,
4060 ..Default::default()
4061 }],
4062 };
4063
4064 let result1 = executor.execute(&request1).await.unwrap();
4065 assert!(result1.success, "Create hierarchy should succeed");
4066 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
4067 let child_id = *result1.task_id_map.get("Child Task").unwrap();
4068
4069 let focus_check: Option<(i64,)> =
4071 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = '-1'")
4072 .fetch_optional(&ctx.pool)
4073 .await
4074 .unwrap();
4075 assert_eq!(
4076 focus_check.map(|r| r.0),
4077 Some(child_id),
4078 "Child should be the session's focus"
4079 );
4080
4081 let request2 = PlanRequest {
4083 tasks: vec![TaskTree {
4084 name: None,
4085 spec: None,
4086 priority: None,
4087 children: None,
4088 depends_on: None,
4089 id: Some(parent_id),
4090 status: None,
4091 active_form: None,
4092 parent_id: None,
4093 delete: Some(true),
4094 }],
4095 };
4096
4097 let result2 = executor.execute(&request2).await.unwrap();
4098
4099 assert!(
4101 !result2.success,
4102 "Delete parent should fail when child is focused"
4103 );
4104 let error = result2.error.as_ref().unwrap();
4105 assert!(
4106 error.contains("cascade"),
4107 "Error should mention cascade: {}",
4108 error
4109 );
4110 assert!(
4111 error.contains(&child_id.to_string()),
4112 "Error should mention child task ID {}: {}",
4113 child_id,
4114 error
4115 );
4116
4117 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
4119 .fetch_one(&ctx.pool)
4120 .await
4121 .unwrap();
4122 assert_eq!(count.0, 2, "Both tasks should still exist");
4123 }
4124
4125 #[tokio::test]
4128 async fn test_batch_delete_blocked_when_subtree_contains_focus() {
4129 let ctx = TestContext::new().await;
4130 let executor = PlanExecutor::new(&ctx.pool);
4131
4132 let request1 = PlanRequest {
4134 tasks: vec![TaskTree {
4135 name: Some("BatchParent".to_string()),
4136 spec: None,
4137 priority: None,
4138 children: Some(vec![TaskTree {
4139 name: Some("BatchChild".to_string()),
4140 spec: Some("## Goal\nFocused child".to_string()),
4141 priority: None,
4142 children: None,
4143 depends_on: None,
4144 id: None,
4145 status: Some(TaskStatus::Doing),
4146 active_form: None,
4147 parent_id: None,
4148 ..Default::default()
4149 }]),
4150 depends_on: None,
4151 id: None,
4152 status: None,
4153 active_form: None,
4154 parent_id: None,
4155 ..Default::default()
4156 }],
4157 };
4158
4159 let result1 = executor.execute(&request1).await.unwrap();
4160 assert!(result1.success);
4161 let parent_id = *result1.task_id_map.get("BatchParent").unwrap();
4162 let child_id = *result1.task_id_map.get("BatchChild").unwrap();
4163
4164 let request2 = PlanRequest {
4167 tasks: vec![
4168 TaskTree {
4169 name: None,
4170 spec: None,
4171 priority: None,
4172 children: None,
4173 depends_on: None,
4174 id: Some(parent_id),
4175 status: None,
4176 active_form: None,
4177 parent_id: None,
4178 delete: Some(true),
4179 },
4180 TaskTree {
4181 name: None,
4182 spec: None,
4183 priority: None,
4184 children: None,
4185 depends_on: None,
4186 id: Some(child_id),
4187 status: None,
4188 active_form: None,
4189 parent_id: None,
4190 delete: Some(true),
4191 },
4192 ],
4193 };
4194
4195 let result2 = executor.execute(&request2).await.unwrap();
4196
4197 assert!(!result2.success, "Batch delete should fail");
4199 assert!(
4200 result2.error.as_ref().unwrap().contains("focus"),
4201 "Error should mention focus: {:?}",
4202 result2.error
4203 );
4204 assert_eq!(result2.deleted_count, 0, "Nothing should be deleted");
4205
4206 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
4208 .fetch_one(&ctx.pool)
4209 .await
4210 .unwrap();
4211 assert_eq!(count.0, 2, "Both tasks should still exist");
4212 }
4213
4214 #[tokio::test]
4216 async fn test_delete_blocked_when_deep_descendant_is_focused() {
4217 let ctx = TestContext::new().await;
4218 let executor = PlanExecutor::new(&ctx.pool);
4219
4220 let request1 = PlanRequest {
4222 tasks: vec![TaskTree {
4223 name: Some("Root".to_string()),
4224 spec: None,
4225 priority: None,
4226 children: Some(vec![TaskTree {
4227 name: Some("Level1".to_string()),
4228 spec: None,
4229 priority: None,
4230 children: Some(vec![TaskTree {
4231 name: Some("Level2".to_string()),
4232 spec: None,
4233 priority: None,
4234 children: Some(vec![TaskTree {
4235 name: Some("Level3".to_string()),
4236 spec: Some("## Goal\nDeep focused task".to_string()),
4237 priority: None,
4238 children: None,
4239 depends_on: None,
4240 id: None,
4241 status: Some(TaskStatus::Doing),
4242 active_form: None,
4243 parent_id: None,
4244 ..Default::default()
4245 }]),
4246 depends_on: None,
4247 id: None,
4248 status: None,
4249 active_form: None,
4250 parent_id: None,
4251 ..Default::default()
4252 }]),
4253 depends_on: None,
4254 id: None,
4255 status: None,
4256 active_form: None,
4257 parent_id: None,
4258 ..Default::default()
4259 }]),
4260 depends_on: None,
4261 id: None,
4262 status: None,
4263 active_form: None,
4264 parent_id: None,
4265 ..Default::default()
4266 }],
4267 };
4268
4269 let result1 = executor.execute(&request1).await.unwrap();
4270 assert!(result1.success);
4271 let root_id = *result1.task_id_map.get("Root").unwrap();
4272 let level3_id = *result1.task_id_map.get("Level3").unwrap();
4273
4274 let focus_check: Option<(i64,)> =
4276 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = '-1'")
4277 .fetch_optional(&ctx.pool)
4278 .await
4279 .unwrap();
4280 assert_eq!(focus_check.map(|r| r.0), Some(level3_id));
4281
4282 let request2 = PlanRequest {
4284 tasks: vec![TaskTree {
4285 name: None,
4286 spec: None,
4287 priority: None,
4288 children: None,
4289 depends_on: None,
4290 id: Some(root_id),
4291 status: None,
4292 active_form: None,
4293 parent_id: None,
4294 delete: Some(true),
4295 }],
4296 };
4297
4298 let result2 = executor.execute(&request2).await.unwrap();
4299
4300 assert!(
4301 !result2.success,
4302 "Delete root should fail when deep descendant is focused"
4303 );
4304 let error = result2.error.as_ref().unwrap();
4305 assert!(
4306 error.contains("cascade"),
4307 "Error should mention cascade: {}",
4308 error
4309 );
4310 assert!(
4311 error.contains(&level3_id.to_string()),
4312 "Error should mention Level3 ID {}: {}",
4313 level3_id,
4314 error
4315 );
4316
4317 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
4319 .fetch_one(&ctx.pool)
4320 .await
4321 .unwrap();
4322 assert_eq!(count.0, 4, "All tasks should still exist");
4323 }
4324
4325 #[tokio::test]
4328 async fn test_delete_nonexistent_task_subtree_check_succeeds() {
4329 let ctx = TestContext::new().await;
4330 let executor = PlanExecutor::new(&ctx.pool);
4331
4332 let request = PlanRequest {
4335 tasks: vec![TaskTree {
4336 name: None,
4337 spec: None,
4338 priority: None,
4339 children: None,
4340 depends_on: None,
4341 id: Some(99999), status: None,
4343 active_form: None,
4344 parent_id: None,
4345 delete: Some(true),
4346 }],
4347 };
4348
4349 let result = executor.execute(&request).await.unwrap();
4350
4351 assert!(result.success, "Delete of non-existent should succeed");
4353 assert_eq!(result.deleted_count, 0);
4354 assert!(
4355 result.warnings.iter().any(|w| w.contains("not found")),
4356 "Should have 'not found' warning: {:?}",
4357 result.warnings
4358 );
4359 }
4360
4361 #[tokio::test]
4364 async fn test_default_session_focus_also_protected() {
4365 let ctx = TestContext::new().await;
4366 let executor = PlanExecutor::new(&ctx.pool);
4367
4368 let request1 = PlanRequest {
4374 tasks: vec![TaskTree {
4375 name: Some("Default Session Task".to_string()),
4376 spec: Some("## Goal\nTest default session".to_string()),
4377 priority: None,
4378 children: None,
4379 depends_on: None,
4380 id: None,
4381 status: Some(TaskStatus::Doing),
4382 active_form: None,
4383 parent_id: None,
4384 ..Default::default()
4385 }],
4386 };
4387
4388 let result1 = executor.execute(&request1).await.unwrap();
4389 assert!(result1.success);
4390 let task_id = *result1.task_id_map.get("Default Session Task").unwrap();
4391
4392 let request2 = PlanRequest {
4394 tasks: vec![TaskTree {
4395 name: None,
4396 spec: None,
4397 priority: None,
4398 children: None,
4399 depends_on: None,
4400 id: Some(task_id),
4401 status: None,
4402 active_form: None,
4403 parent_id: None,
4404 delete: Some(true),
4405 }],
4406 };
4407
4408 let result2 = executor.execute(&request2).await.unwrap();
4409
4410 assert!(
4412 !result2.success,
4413 "Default session focus should be protected"
4414 );
4415 assert_eq!(result2.deleted_count, 0);
4416
4417 let error = result2.error.as_ref().unwrap();
4419 assert!(
4420 error.contains("-1"),
4421 "Error should mention default session '-1': {}",
4422 error
4423 );
4424
4425 let exists: (i64,) =
4427 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
4428 .bind(task_id)
4429 .fetch_one(&ctx.pool)
4430 .await
4431 .unwrap();
4432 assert_eq!(exists.0, 1, "Task should still exist");
4433 }
4434
4435 #[tokio::test]
4438 async fn test_cross_session_delete_blocked() {
4439 let ctx = TestContext::new().await;
4440 let executor = PlanExecutor::new(&ctx.pool);
4441
4442 let request1 = PlanRequest {
4444 tasks: vec![TaskTree {
4445 name: Some("Session A Focus".to_string()),
4446 spec: Some("## Goal\nSession A's task".to_string()),
4447 priority: None,
4448 children: None,
4449 depends_on: None,
4450 id: None,
4451 status: Some(TaskStatus::Doing),
4452 active_form: None,
4453 parent_id: None,
4454 ..Default::default()
4455 }],
4456 };
4457
4458 let result1 = executor.execute(&request1).await.unwrap();
4459 assert!(result1.success);
4460 let task_id = *result1.task_id_map.get("Session A Focus").unwrap();
4461
4462 let session_a = "session-A-cross-test";
4467 sqlx::query(
4468 "UPDATE sessions SET session_id = ? WHERE session_id = '-1' AND current_task_id = ?",
4469 )
4470 .bind(session_a)
4471 .bind(task_id)
4472 .execute(&ctx.pool)
4473 .await
4474 .unwrap();
4475
4476 let focus_a: Option<(i64,)> =
4478 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = ?")
4479 .bind(session_a)
4480 .fetch_optional(&ctx.pool)
4481 .await
4482 .unwrap();
4483 assert_eq!(
4484 focus_a.map(|r| r.0),
4485 Some(task_id),
4486 "session-A should have focus"
4487 );
4488
4489 let request2 = PlanRequest {
4492 tasks: vec![TaskTree {
4493 name: None,
4494 spec: None,
4495 priority: None,
4496 children: None,
4497 depends_on: None,
4498 id: Some(task_id),
4499 status: None,
4500 active_form: None,
4501 parent_id: None,
4502 delete: Some(true),
4503 }],
4504 };
4505
4506 let result2 = executor.execute(&request2).await.unwrap();
4507
4508 assert!(
4510 !result2.success,
4511 "Session B should NOT be able to delete Session A's focus"
4512 );
4513 assert_eq!(result2.deleted_count, 0);
4514
4515 let error = result2.error.as_ref().unwrap();
4517 assert!(
4518 error.contains(session_a),
4519 "Error should mention session '{}': {}",
4520 session_a,
4521 error
4522 );
4523
4524 let exists: (i64,) =
4526 sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ? AND deleted_at IS NULL")
4527 .bind(task_id)
4528 .fetch_one(&ctx.pool)
4529 .await
4530 .unwrap();
4531 assert_eq!(exists.0, 1, "Task should still exist");
4532 }
4533}