1use serde::{Deserialize, Serialize};
8use sqlx::Row;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
13pub struct PlanRequest {
14 pub tasks: Vec<TaskTree>,
16}
17
18#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
20pub struct TaskTree {
21 pub name: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub spec: Option<String>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub priority: Option<PriorityValue>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub children: Option<Vec<TaskTree>>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub depends_on: Option<Vec<String>>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub task_id: Option<i64>,
43}
44
45#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
47#[serde(rename_all = "lowercase")]
48pub enum PriorityValue {
49 Critical,
50 High,
51 Medium,
52 Low,
53}
54
55impl PriorityValue {
56 pub fn to_int(&self) -> i32 {
58 match self {
59 PriorityValue::Critical => 1,
60 PriorityValue::High => 2,
61 PriorityValue::Medium => 3,
62 PriorityValue::Low => 4,
63 }
64 }
65
66 pub fn from_int(value: i32) -> Option<Self> {
68 match value {
69 1 => Some(PriorityValue::Critical),
70 2 => Some(PriorityValue::High),
71 3 => Some(PriorityValue::Medium),
72 4 => Some(PriorityValue::Low),
73 _ => None,
74 }
75 }
76
77 pub fn as_str(&self) -> &'static str {
79 match self {
80 PriorityValue::Critical => "critical",
81 PriorityValue::High => "high",
82 PriorityValue::Medium => "medium",
83 PriorityValue::Low => "low",
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub struct PlanResult {
91 pub success: bool,
93
94 pub task_id_map: HashMap<String, i64>,
96
97 pub created_count: usize,
99
100 pub updated_count: usize,
102
103 pub dependency_count: usize,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub error: Option<String>,
109}
110
111impl PlanResult {
112 pub fn success(
114 task_id_map: HashMap<String, i64>,
115 created_count: usize,
116 updated_count: usize,
117 dependency_count: usize,
118 ) -> Self {
119 Self {
120 success: true,
121 task_id_map,
122 created_count,
123 updated_count,
124 dependency_count,
125 error: None,
126 }
127 }
128
129 pub fn error(message: impl Into<String>) -> Self {
131 Self {
132 success: false,
133 task_id_map: HashMap::new(),
134 created_count: 0,
135 updated_count: 0,
136 dependency_count: 0,
137 error: Some(message.into()),
138 }
139 }
140}
141
142pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
148 let mut names = Vec::new();
149
150 for task in tasks {
151 names.push(task.name.clone());
152
153 if let Some(children) = &task.children {
154 names.extend(extract_all_names(children));
155 }
156 }
157
158 names
159}
160
161#[derive(Debug, Clone, PartialEq)]
163pub struct FlatTask {
164 pub name: String,
165 pub spec: Option<String>,
166 pub priority: Option<PriorityValue>,
167 pub parent_name: Option<String>,
168 pub depends_on: Vec<String>,
169 pub task_id: Option<i64>,
170}
171
172pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
173 flatten_task_tree_recursive(tasks, None)
174}
175
176fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
177 let mut flat = Vec::new();
178
179 for task in tasks {
180 let flat_task = FlatTask {
181 name: task.name.clone(),
182 spec: task.spec.clone(),
183 priority: task.priority.clone(),
184 parent_name: parent_name.clone(),
185 depends_on: task.depends_on.clone().unwrap_or_default(),
186 task_id: task.task_id,
187 };
188
189 flat.push(flat_task);
190
191 if let Some(children) = &task.children {
193 flat.extend(flatten_task_tree_recursive(
194 children,
195 Some(task.name.clone()),
196 ));
197 }
198 }
199
200 flat
201}
202
203#[derive(Debug, Clone, PartialEq)]
205pub enum Operation {
206 Create(FlatTask),
207 Update { task_id: i64, task: FlatTask },
208}
209
210pub fn classify_operations(
219 flat_tasks: &[FlatTask],
220 existing_names: &HashMap<String, i64>,
221) -> Vec<Operation> {
222 let mut operations = Vec::new();
223
224 for task in flat_tasks {
225 let operation = if let Some(task_id) = task.task_id {
227 Operation::Update {
229 task_id,
230 task: task.clone(),
231 }
232 } else if let Some(&task_id) = existing_names.get(&task.name) {
233 Operation::Update {
235 task_id,
236 task: task.clone(),
237 }
238 } else {
239 Operation::Create(task.clone())
241 };
242
243 operations.push(operation);
244 }
245
246 operations
247}
248
249pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
251 let mut seen = HashMap::new();
252 let mut duplicates = Vec::new();
253
254 for name in extract_all_names(tasks) {
255 let count = seen.entry(name.clone()).or_insert(0);
256 *count += 1;
257 if *count == 2 {
258 duplicates.push(name);
260 }
261 }
262
263 duplicates
264}
265
266use crate::error::{IntentError, Result};
271use chrono::Utc;
272use sqlx::SqlitePool;
273
274pub struct PlanExecutor<'a> {
276 pool: &'a SqlitePool,
277}
278
279impl<'a> PlanExecutor<'a> {
280 pub fn new(pool: &'a SqlitePool) -> Self {
282 Self { pool }
283 }
284
285 pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
287 let duplicates = find_duplicate_names(&request.tasks);
289 if !duplicates.is_empty() {
290 return Ok(PlanResult::error(format!(
291 "Duplicate task names in request: {:?}",
292 duplicates
293 )));
294 }
295
296 let all_names = extract_all_names(&request.tasks);
298
299 let existing = self.find_tasks_by_names(&all_names).await?;
301
302 let flat_tasks = flatten_task_tree(&request.tasks);
304
305 if let Err(e) = self.validate_dependencies(&flat_tasks) {
307 return Ok(PlanResult::error(e.to_string()));
308 }
309
310 if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
312 return Ok(PlanResult::error(e.to_string()));
313 }
314
315 let mut tx = self.pool.begin().await?;
317
318 let mut task_id_map = HashMap::new();
320 let mut created_count = 0;
321 let mut updated_count = 0;
322
323 for task in &flat_tasks {
324 if let Some(&existing_id) = existing.get(&task.name) {
325 self.update_task_in_tx(&mut tx, existing_id, task).await?;
327 task_id_map.insert(task.name.clone(), existing_id);
328 updated_count += 1;
329 } else {
330 let id = self.create_task_in_tx(&mut tx, task).await?;
332 task_id_map.insert(task.name.clone(), id);
333 created_count += 1;
334 }
335 }
336
337 self.build_parent_child_relations(&mut tx, &flat_tasks, &task_id_map)
339 .await?;
340
341 let dep_count = self
343 .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
344 .await?;
345
346 tx.commit().await?;
348
349 Ok(PlanResult::success(
351 task_id_map,
352 created_count,
353 updated_count,
354 dep_count,
355 ))
356 }
357
358 async fn find_tasks_by_names(&self, names: &[String]) -> Result<HashMap<String, i64>> {
360 if names.is_empty() {
361 return Ok(HashMap::new());
362 }
363
364 let mut map = HashMap::new();
365
366 let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
369 let query = format!(
370 "SELECT id, name FROM tasks WHERE name IN ({})",
371 placeholders
372 );
373
374 let mut query_builder = sqlx::query(&query);
375 for name in names {
376 query_builder = query_builder.bind(name);
377 }
378
379 let rows = query_builder.fetch_all(self.pool).await?;
380
381 for row in rows {
382 let id: i64 = row.get("id");
383 let name: String = row.get("name");
384 map.insert(name, id);
385 }
386
387 Ok(map)
388 }
389
390 async fn create_task_in_tx(
392 &self,
393 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
394 task: &FlatTask,
395 ) -> Result<i64> {
396 let now = Utc::now();
397 let priority = task.priority.as_ref().map(|p| p.to_int()).unwrap_or(3); let result = sqlx::query(
400 r#"
401 INSERT INTO tasks (name, spec, priority, status, first_todo_at)
402 VALUES (?, ?, ?, 'todo', ?)
403 "#,
404 )
405 .bind(&task.name)
406 .bind(&task.spec)
407 .bind(priority)
408 .bind(now)
409 .execute(&mut **tx)
410 .await?;
411
412 Ok(result.last_insert_rowid())
413 }
414
415 async fn update_task_in_tx(
418 &self,
419 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
420 task_id: i64,
421 task: &FlatTask,
422 ) -> Result<()> {
423 if let Some(spec) = &task.spec {
425 sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
426 .bind(spec)
427 .bind(task_id)
428 .execute(&mut **tx)
429 .await?;
430 }
431
432 if let Some(priority) = &task.priority {
434 sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
435 .bind(priority.to_int())
436 .bind(task_id)
437 .execute(&mut **tx)
438 .await?;
439 }
440
441 Ok(())
447 }
448
449 async fn build_parent_child_relations(
451 &self,
452 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
453 flat_tasks: &[FlatTask],
454 task_id_map: &HashMap<String, i64>,
455 ) -> Result<()> {
456 for task in flat_tasks {
457 if let Some(parent_name) = &task.parent_name {
458 let task_id = task_id_map.get(&task.name).ok_or_else(|| {
459 IntentError::InvalidInput(format!("Task not found: {}", task.name))
460 })?;
461
462 let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
463 IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
464 })?;
465
466 sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
467 .bind(parent_id)
468 .bind(task_id)
469 .execute(&mut **tx)
470 .await?;
471 }
472 }
473
474 Ok(())
475 }
476
477 async fn build_dependencies(
479 &self,
480 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
481 flat_tasks: &[FlatTask],
482 task_id_map: &HashMap<String, i64>,
483 ) -> Result<usize> {
484 let mut count = 0;
485
486 for task in flat_tasks {
487 if !task.depends_on.is_empty() {
488 let blocked_id = task_id_map.get(&task.name).ok_or_else(|| {
489 IntentError::InvalidInput(format!("Task not found: {}", task.name))
490 })?;
491
492 for dep_name in &task.depends_on {
493 let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
494 IntentError::InvalidInput(format!(
495 "Dependency '{}' not found for task '{}'",
496 dep_name, task.name
497 ))
498 })?;
499
500 sqlx::query(
501 "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
502 )
503 .bind(blocking_id)
504 .bind(blocked_id)
505 .execute(&mut **tx)
506 .await?;
507
508 count += 1;
509 }
510 }
511 }
512
513 Ok(count)
514 }
515
516 fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
518 let task_names: std::collections::HashSet<_> =
519 flat_tasks.iter().map(|t| t.name.as_str()).collect();
520
521 for task in flat_tasks {
522 for dep_name in &task.depends_on {
523 if !task_names.contains(dep_name.as_str()) {
524 return Err(IntentError::InvalidInput(format!(
525 "Task '{}' depends on '{}', but '{}' is not in the plan",
526 task.name, dep_name, dep_name
527 )));
528 }
529 }
530 }
531
532 Ok(())
533 }
534
535 fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
537 if flat_tasks.is_empty() {
538 return Ok(());
539 }
540
541 let name_to_idx: HashMap<&str, usize> = flat_tasks
543 .iter()
544 .enumerate()
545 .map(|(i, t)| (t.name.as_str(), i))
546 .collect();
547
548 let mut graph: Vec<Vec<usize>> = vec![Vec::new(); flat_tasks.len()];
550 for (idx, task) in flat_tasks.iter().enumerate() {
551 for dep_name in &task.depends_on {
552 if let Some(&dep_idx) = name_to_idx.get(dep_name.as_str()) {
553 graph[idx].push(dep_idx);
554 }
555 }
556 }
557
558 for task in flat_tasks {
560 if task.depends_on.contains(&task.name) {
561 return Err(IntentError::InvalidInput(format!(
562 "Circular dependency detected: task '{}' depends on itself",
563 task.name
564 )));
565 }
566 }
567
568 let sccs = self.tarjan_scc(&graph);
570
571 for scc in sccs {
573 if scc.len() > 1 {
574 let cycle_names: Vec<&str> = scc
576 .iter()
577 .map(|&idx| flat_tasks[idx].name.as_str())
578 .collect();
579
580 return Err(IntentError::InvalidInput(format!(
581 "Circular dependency detected: {}",
582 cycle_names.join(" → ")
583 )));
584 }
585 }
586
587 Ok(())
588 }
589
590 fn tarjan_scc(&self, graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
593 let n = graph.len();
594 let mut index = 0;
595 let mut stack = Vec::new();
596 let mut indices = vec![None; n];
597 let mut lowlinks = vec![0; n];
598 let mut on_stack = vec![false; n];
599 let mut sccs = Vec::new();
600
601 #[allow(clippy::too_many_arguments)]
602 fn strongconnect(
603 v: usize,
604 graph: &[Vec<usize>],
605 index: &mut usize,
606 stack: &mut Vec<usize>,
607 indices: &mut [Option<usize>],
608 lowlinks: &mut [usize],
609 on_stack: &mut [bool],
610 sccs: &mut Vec<Vec<usize>>,
611 ) {
612 indices[v] = Some(*index);
614 lowlinks[v] = *index;
615 *index += 1;
616 stack.push(v);
617 on_stack[v] = true;
618
619 for &w in &graph[v] {
621 if indices[w].is_none() {
622 strongconnect(w, graph, index, stack, indices, lowlinks, on_stack, sccs);
624 lowlinks[v] = lowlinks[v].min(lowlinks[w]);
625 } else if on_stack[w] {
626 lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
628 }
629 }
630
631 if lowlinks[v] == indices[v].unwrap() {
633 let mut scc = Vec::new();
634 loop {
635 let w = stack.pop().unwrap();
636 on_stack[w] = false;
637 scc.push(w);
638 if w == v {
639 break;
640 }
641 }
642 sccs.push(scc);
643 }
644 }
645
646 for v in 0..n {
648 if indices[v].is_none() {
649 strongconnect(
650 v,
651 graph,
652 &mut index,
653 &mut stack,
654 &mut indices,
655 &mut lowlinks,
656 &mut on_stack,
657 &mut sccs,
658 );
659 }
660 }
661
662 sccs
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 #[test]
671 fn test_priority_value_to_int() {
672 assert_eq!(PriorityValue::Critical.to_int(), 1);
673 assert_eq!(PriorityValue::High.to_int(), 2);
674 assert_eq!(PriorityValue::Medium.to_int(), 3);
675 assert_eq!(PriorityValue::Low.to_int(), 4);
676 }
677
678 #[test]
679 fn test_priority_value_from_int() {
680 assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
681 assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
682 assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
683 assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
684 assert_eq!(PriorityValue::from_int(999), None);
685 }
686
687 #[test]
688 fn test_priority_value_as_str() {
689 assert_eq!(PriorityValue::Critical.as_str(), "critical");
690 assert_eq!(PriorityValue::High.as_str(), "high");
691 assert_eq!(PriorityValue::Medium.as_str(), "medium");
692 assert_eq!(PriorityValue::Low.as_str(), "low");
693 }
694
695 #[test]
696 fn test_plan_request_deserialization_minimal() {
697 let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
698 let request: PlanRequest = serde_json::from_str(json).unwrap();
699
700 assert_eq!(request.tasks.len(), 1);
701 assert_eq!(request.tasks[0].name, "Test Task");
702 assert_eq!(request.tasks[0].spec, None);
703 assert_eq!(request.tasks[0].priority, None);
704 assert_eq!(request.tasks[0].children, None);
705 assert_eq!(request.tasks[0].depends_on, None);
706 assert_eq!(request.tasks[0].task_id, None);
707 }
708
709 #[test]
710 fn test_plan_request_deserialization_full() {
711 let json = r#"{
712 "tasks": [{
713 "name": "Parent Task",
714 "spec": "Parent spec",
715 "priority": "high",
716 "children": [{
717 "name": "Child Task",
718 "spec": "Child spec"
719 }],
720 "depends_on": ["Other Task"],
721 "task_id": 42
722 }]
723 }"#;
724
725 let request: PlanRequest = serde_json::from_str(json).unwrap();
726
727 assert_eq!(request.tasks.len(), 1);
728 let parent = &request.tasks[0];
729 assert_eq!(parent.name, "Parent Task");
730 assert_eq!(parent.spec, Some("Parent spec".to_string()));
731 assert_eq!(parent.priority, Some(PriorityValue::High));
732 assert_eq!(parent.task_id, Some(42));
733
734 let children = parent.children.as_ref().unwrap();
735 assert_eq!(children.len(), 1);
736 assert_eq!(children[0].name, "Child Task");
737
738 let depends = parent.depends_on.as_ref().unwrap();
739 assert_eq!(depends.len(), 1);
740 assert_eq!(depends[0], "Other Task");
741 }
742
743 #[test]
744 fn test_plan_request_serialization() {
745 let request = PlanRequest {
746 tasks: vec![TaskTree {
747 name: "Test Task".to_string(),
748 spec: Some("Test spec".to_string()),
749 priority: Some(PriorityValue::Medium),
750 children: None,
751 depends_on: None,
752 task_id: None,
753 }],
754 };
755
756 let json = serde_json::to_string(&request).unwrap();
757 assert!(json.contains("\"name\":\"Test Task\""));
758 assert!(json.contains("\"spec\":\"Test spec\""));
759 assert!(json.contains("\"priority\":\"medium\""));
760 }
761
762 #[test]
763 fn test_plan_result_success() {
764 let mut map = HashMap::new();
765 map.insert("Task 1".to_string(), 1);
766 map.insert("Task 2".to_string(), 2);
767
768 let result = PlanResult::success(map.clone(), 2, 0, 1);
769
770 assert!(result.success);
771 assert_eq!(result.task_id_map, map);
772 assert_eq!(result.created_count, 2);
773 assert_eq!(result.updated_count, 0);
774 assert_eq!(result.dependency_count, 1);
775 assert_eq!(result.error, None);
776 }
777
778 #[test]
779 fn test_plan_result_error() {
780 let result = PlanResult::error("Test error");
781
782 assert!(!result.success);
783 assert_eq!(result.task_id_map.len(), 0);
784 assert_eq!(result.created_count, 0);
785 assert_eq!(result.updated_count, 0);
786 assert_eq!(result.dependency_count, 0);
787 assert_eq!(result.error, Some("Test error".to_string()));
788 }
789
790 #[test]
791 fn test_task_tree_nested() {
792 let tree = TaskTree {
793 name: "Parent".to_string(),
794 spec: None,
795 priority: None,
796 children: Some(vec![
797 TaskTree {
798 name: "Child 1".to_string(),
799 spec: None,
800 priority: None,
801 children: None,
802 depends_on: None,
803 task_id: None,
804 },
805 TaskTree {
806 name: "Child 2".to_string(),
807 spec: None,
808 priority: Some(PriorityValue::High),
809 children: None,
810 depends_on: None,
811 task_id: None,
812 },
813 ]),
814 depends_on: None,
815 task_id: None,
816 };
817
818 let json = serde_json::to_string_pretty(&tree).unwrap();
819 let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
820
821 assert_eq!(tree, deserialized);
822 assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
823 }
824
825 #[test]
826 fn test_priority_value_case_insensitive_deserialization() {
827 let json = r#"{"name": "Test", "priority": "high"}"#;
829 let task: TaskTree = serde_json::from_str(json).unwrap();
830 assert_eq!(task.priority, Some(PriorityValue::High));
831
832 }
835
836 #[test]
837 fn test_extract_all_names_simple() {
838 let tasks = vec![
839 TaskTree {
840 name: "Task 1".to_string(),
841 spec: None,
842 priority: None,
843 children: None,
844 depends_on: None,
845 task_id: None,
846 },
847 TaskTree {
848 name: "Task 2".to_string(),
849 spec: None,
850 priority: None,
851 children: None,
852 depends_on: None,
853 task_id: None,
854 },
855 ];
856
857 let names = extract_all_names(&tasks);
858 assert_eq!(names, vec!["Task 1", "Task 2"]);
859 }
860
861 #[test]
862 fn test_extract_all_names_nested() {
863 let tasks = vec![TaskTree {
864 name: "Parent".to_string(),
865 spec: None,
866 priority: None,
867 children: Some(vec![
868 TaskTree {
869 name: "Child 1".to_string(),
870 spec: None,
871 priority: None,
872 children: None,
873 depends_on: None,
874 task_id: None,
875 },
876 TaskTree {
877 name: "Child 2".to_string(),
878 spec: None,
879 priority: None,
880 children: Some(vec![TaskTree {
881 name: "Grandchild".to_string(),
882 spec: None,
883 priority: None,
884 children: None,
885 depends_on: None,
886 task_id: None,
887 }]),
888 depends_on: None,
889 task_id: None,
890 },
891 ]),
892 depends_on: None,
893 task_id: None,
894 }];
895
896 let names = extract_all_names(&tasks);
897 assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
898 }
899
900 #[test]
901 fn test_flatten_task_tree_simple() {
902 let tasks = vec![TaskTree {
903 name: "Task 1".to_string(),
904 spec: Some("Spec 1".to_string()),
905 priority: Some(PriorityValue::High),
906 children: None,
907 depends_on: Some(vec!["Task 0".to_string()]),
908 task_id: None,
909 }];
910
911 let flat = flatten_task_tree(&tasks);
912 assert_eq!(flat.len(), 1);
913 assert_eq!(flat[0].name, "Task 1");
914 assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
915 assert_eq!(flat[0].priority, Some(PriorityValue::High));
916 assert_eq!(flat[0].parent_name, None);
917 assert_eq!(flat[0].depends_on, vec!["Task 0"]);
918 }
919
920 #[test]
921 fn test_flatten_task_tree_nested() {
922 let tasks = vec![TaskTree {
923 name: "Parent".to_string(),
924 spec: None,
925 priority: None,
926 children: Some(vec![
927 TaskTree {
928 name: "Child 1".to_string(),
929 spec: None,
930 priority: None,
931 children: None,
932 depends_on: None,
933 task_id: None,
934 },
935 TaskTree {
936 name: "Child 2".to_string(),
937 spec: None,
938 priority: None,
939 children: None,
940 depends_on: None,
941 task_id: None,
942 },
943 ]),
944 depends_on: None,
945 task_id: None,
946 }];
947
948 let flat = flatten_task_tree(&tasks);
949 assert_eq!(flat.len(), 3);
950
951 assert_eq!(flat[0].name, "Parent");
953 assert_eq!(flat[0].parent_name, None);
954
955 assert_eq!(flat[1].name, "Child 1");
957 assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
958
959 assert_eq!(flat[2].name, "Child 2");
960 assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
961 }
962
963 #[test]
964 fn test_classify_operations_all_create() {
965 let flat_tasks = vec![
966 FlatTask {
967 name: "Task 1".to_string(),
968 spec: None,
969 priority: None,
970 parent_name: None,
971 depends_on: vec![],
972 task_id: None,
973 },
974 FlatTask {
975 name: "Task 2".to_string(),
976 spec: None,
977 priority: None,
978 parent_name: None,
979 depends_on: vec![],
980 task_id: None,
981 },
982 ];
983
984 let existing = HashMap::new();
985 let operations = classify_operations(&flat_tasks, &existing);
986
987 assert_eq!(operations.len(), 2);
988 assert!(matches!(operations[0], Operation::Create(_)));
989 assert!(matches!(operations[1], Operation::Create(_)));
990 }
991
992 #[test]
993 fn test_classify_operations_all_update() {
994 let flat_tasks = vec![
995 FlatTask {
996 name: "Task 1".to_string(),
997 spec: None,
998 priority: None,
999 parent_name: None,
1000 depends_on: vec![],
1001 task_id: None,
1002 },
1003 FlatTask {
1004 name: "Task 2".to_string(),
1005 spec: None,
1006 priority: None,
1007 parent_name: None,
1008 depends_on: vec![],
1009 task_id: None,
1010 },
1011 ];
1012
1013 let mut existing = HashMap::new();
1014 existing.insert("Task 1".to_string(), 1);
1015 existing.insert("Task 2".to_string(), 2);
1016
1017 let operations = classify_operations(&flat_tasks, &existing);
1018
1019 assert_eq!(operations.len(), 2);
1020 assert!(matches!(
1021 operations[0],
1022 Operation::Update { task_id: 1, .. }
1023 ));
1024 assert!(matches!(
1025 operations[1],
1026 Operation::Update { task_id: 2, .. }
1027 ));
1028 }
1029
1030 #[test]
1031 fn test_classify_operations_mixed() {
1032 let flat_tasks = vec![
1033 FlatTask {
1034 name: "Existing Task".to_string(),
1035 spec: None,
1036 priority: None,
1037 parent_name: None,
1038 depends_on: vec![],
1039 task_id: None,
1040 },
1041 FlatTask {
1042 name: "New Task".to_string(),
1043 spec: None,
1044 priority: None,
1045 parent_name: None,
1046 depends_on: vec![],
1047 task_id: None,
1048 },
1049 ];
1050
1051 let mut existing = HashMap::new();
1052 existing.insert("Existing Task".to_string(), 42);
1053
1054 let operations = classify_operations(&flat_tasks, &existing);
1055
1056 assert_eq!(operations.len(), 2);
1057 assert!(matches!(
1058 operations[0],
1059 Operation::Update { task_id: 42, .. }
1060 ));
1061 assert!(matches!(operations[1], Operation::Create(_)));
1062 }
1063
1064 #[test]
1065 fn test_classify_operations_explicit_task_id() {
1066 let flat_tasks = vec![FlatTask {
1067 name: "Task".to_string(),
1068 spec: None,
1069 priority: None,
1070 parent_name: None,
1071 depends_on: vec![],
1072 task_id: Some(99), }];
1074
1075 let existing = HashMap::new(); let operations = classify_operations(&flat_tasks, &existing);
1078
1079 assert_eq!(operations.len(), 1);
1081 assert!(matches!(
1082 operations[0],
1083 Operation::Update { task_id: 99, .. }
1084 ));
1085 }
1086
1087 #[test]
1088 fn test_find_duplicate_names_no_duplicates() {
1089 let tasks = vec![
1090 TaskTree {
1091 name: "Task 1".to_string(),
1092 spec: None,
1093 priority: None,
1094 children: None,
1095 depends_on: None,
1096 task_id: None,
1097 },
1098 TaskTree {
1099 name: "Task 2".to_string(),
1100 spec: None,
1101 priority: None,
1102 children: None,
1103 depends_on: None,
1104 task_id: None,
1105 },
1106 ];
1107
1108 let duplicates = find_duplicate_names(&tasks);
1109 assert_eq!(duplicates.len(), 0);
1110 }
1111
1112 #[test]
1113 fn test_find_duplicate_names_with_duplicates() {
1114 let tasks = vec![
1115 TaskTree {
1116 name: "Duplicate".to_string(),
1117 spec: None,
1118 priority: None,
1119 children: None,
1120 depends_on: None,
1121 task_id: None,
1122 },
1123 TaskTree {
1124 name: "Unique".to_string(),
1125 spec: None,
1126 priority: None,
1127 children: None,
1128 depends_on: None,
1129 task_id: None,
1130 },
1131 TaskTree {
1132 name: "Duplicate".to_string(),
1133 spec: None,
1134 priority: None,
1135 children: None,
1136 depends_on: None,
1137 task_id: None,
1138 },
1139 ];
1140
1141 let duplicates = find_duplicate_names(&tasks);
1142 assert_eq!(duplicates.len(), 1);
1143 assert_eq!(duplicates[0], "Duplicate");
1144 }
1145
1146 #[test]
1147 fn test_find_duplicate_names_nested() {
1148 let tasks = vec![TaskTree {
1149 name: "Parent".to_string(),
1150 spec: None,
1151 priority: None,
1152 children: Some(vec![TaskTree {
1153 name: "Parent".to_string(), spec: None,
1155 priority: None,
1156 children: None,
1157 depends_on: None,
1158 task_id: None,
1159 }]),
1160 depends_on: None,
1161 task_id: None,
1162 }];
1163
1164 let duplicates = find_duplicate_names(&tasks);
1165 assert_eq!(duplicates.len(), 1);
1166 assert_eq!(duplicates[0], "Parent");
1167 }
1168
1169 #[test]
1170 fn test_flatten_task_tree_empty() {
1171 let tasks: Vec<TaskTree> = vec![];
1172 let flat = flatten_task_tree(&tasks);
1173 assert_eq!(flat.len(), 0);
1174 }
1175
1176 #[test]
1177 fn test_flatten_task_tree_deep_nesting() {
1178 let tasks = vec![TaskTree {
1180 name: "Root".to_string(),
1181 spec: None,
1182 priority: None,
1183 children: Some(vec![TaskTree {
1184 name: "Level1".to_string(),
1185 spec: None,
1186 priority: None,
1187 children: Some(vec![TaskTree {
1188 name: "Level2".to_string(),
1189 spec: None,
1190 priority: None,
1191 children: Some(vec![TaskTree {
1192 name: "Level3".to_string(),
1193 spec: None,
1194 priority: None,
1195 children: None,
1196 depends_on: None,
1197 task_id: None,
1198 }]),
1199 depends_on: None,
1200 task_id: None,
1201 }]),
1202 depends_on: None,
1203 task_id: None,
1204 }]),
1205 depends_on: None,
1206 task_id: None,
1207 }];
1208
1209 let flat = flatten_task_tree(&tasks);
1210 assert_eq!(flat.len(), 4);
1211
1212 assert_eq!(flat[0].name, "Root");
1214 assert_eq!(flat[0].parent_name, None);
1215
1216 assert_eq!(flat[1].name, "Level1");
1217 assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1218
1219 assert_eq!(flat[2].name, "Level2");
1220 assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1221
1222 assert_eq!(flat[3].name, "Level3");
1223 assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1224 }
1225
1226 #[test]
1227 fn test_flatten_task_tree_many_siblings() {
1228 let children: Vec<TaskTree> = (0..10)
1229 .map(|i| TaskTree {
1230 name: format!("Child {}", i),
1231 spec: None,
1232 priority: None,
1233 children: None,
1234 depends_on: None,
1235 task_id: None,
1236 })
1237 .collect();
1238
1239 let tasks = vec![TaskTree {
1240 name: "Parent".to_string(),
1241 spec: None,
1242 priority: None,
1243 children: Some(children),
1244 depends_on: None,
1245 task_id: None,
1246 }];
1247
1248 let flat = flatten_task_tree(&tasks);
1249 assert_eq!(flat.len(), 11); for child in flat.iter().skip(1).take(10) {
1253 assert_eq!(child.parent_name, Some("Parent".to_string()));
1254 }
1255 }
1256
1257 #[test]
1258 fn test_flatten_task_tree_complex_mixed() {
1259 let tasks = vec![
1261 TaskTree {
1262 name: "Task 1".to_string(),
1263 spec: None,
1264 priority: None,
1265 children: Some(vec![
1266 TaskTree {
1267 name: "Task 1.1".to_string(),
1268 spec: None,
1269 priority: None,
1270 children: None,
1271 depends_on: None,
1272 task_id: None,
1273 },
1274 TaskTree {
1275 name: "Task 1.2".to_string(),
1276 spec: None,
1277 priority: None,
1278 children: Some(vec![TaskTree {
1279 name: "Task 1.2.1".to_string(),
1280 spec: None,
1281 priority: None,
1282 children: None,
1283 depends_on: None,
1284 task_id: None,
1285 }]),
1286 depends_on: None,
1287 task_id: None,
1288 },
1289 ]),
1290 depends_on: None,
1291 task_id: None,
1292 },
1293 TaskTree {
1294 name: "Task 2".to_string(),
1295 spec: None,
1296 priority: None,
1297 children: None,
1298 depends_on: Some(vec!["Task 1".to_string()]),
1299 task_id: None,
1300 },
1301 ];
1302
1303 let flat = flatten_task_tree(&tasks);
1304 assert_eq!(flat.len(), 5);
1305
1306 assert_eq!(flat[0].name, "Task 1");
1308 assert_eq!(flat[0].parent_name, None);
1309
1310 assert_eq!(flat[1].name, "Task 1.1");
1311 assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
1312
1313 assert_eq!(flat[2].name, "Task 1.2");
1314 assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
1315
1316 assert_eq!(flat[3].name, "Task 1.2.1");
1317 assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
1318
1319 assert_eq!(flat[4].name, "Task 2");
1320 assert_eq!(flat[4].parent_name, None);
1321 assert_eq!(flat[4].depends_on, vec!["Task 1"]);
1322 }
1323
1324 #[tokio::test]
1325 async fn test_plan_executor_integration() {
1326 use crate::test_utils::test_helpers::TestContext;
1327
1328 let ctx = TestContext::new().await;
1329
1330 let request = PlanRequest {
1332 tasks: vec![TaskTree {
1333 name: "Integration Test Plan".to_string(),
1334 spec: Some("Test plan execution end-to-end".to_string()),
1335 priority: Some(PriorityValue::High),
1336 children: Some(vec![
1337 TaskTree {
1338 name: "Subtask A".to_string(),
1339 spec: Some("First subtask".to_string()),
1340 priority: None,
1341 children: None,
1342 depends_on: None,
1343 task_id: None,
1344 },
1345 TaskTree {
1346 name: "Subtask B".to_string(),
1347 spec: Some("Second subtask depends on A".to_string()),
1348 priority: None,
1349 children: None,
1350 depends_on: Some(vec!["Subtask A".to_string()]),
1351 task_id: None,
1352 },
1353 ]),
1354 depends_on: None,
1355 task_id: None,
1356 }],
1357 };
1358
1359 let executor = PlanExecutor::new(&ctx.pool);
1361 let result = executor.execute(&request).await.unwrap();
1362
1363 assert!(result.success, "Plan execution should succeed");
1365 assert_eq!(result.created_count, 3, "Should create 3 tasks");
1366 assert_eq!(result.updated_count, 0, "Should not update any tasks");
1367 assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
1368 assert!(result.error.is_none(), "Should have no error");
1369
1370 assert_eq!(result.task_id_map.len(), 3);
1372 assert!(result.task_id_map.contains_key("Integration Test Plan"));
1373 assert!(result.task_id_map.contains_key("Subtask A"));
1374 assert!(result.task_id_map.contains_key("Subtask B"));
1375
1376 let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
1378 let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
1379 let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
1380
1381 let parent: (String, String, i64, Option<i64>) =
1383 sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
1384 .bind(parent_id)
1385 .fetch_one(&ctx.pool)
1386 .await
1387 .unwrap();
1388
1389 assert_eq!(parent.0, "Integration Test Plan");
1390 assert_eq!(parent.1, "Test plan execution end-to-end");
1391 assert_eq!(parent.2, 2); assert_eq!(parent.3, None); let subtask_a: (String, Option<i64>) =
1396 sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
1397 .bind(subtask_a_id)
1398 .fetch_one(&ctx.pool)
1399 .await
1400 .unwrap();
1401
1402 assert_eq!(subtask_a.0, "Subtask A");
1403 assert_eq!(subtask_a.1, Some(parent_id)); let dep: (i64, i64) = sqlx::query_as(
1407 "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
1408 )
1409 .bind(subtask_b_id)
1410 .fetch_one(&ctx.pool)
1411 .await
1412 .unwrap();
1413
1414 assert_eq!(dep.0, subtask_a_id); assert_eq!(dep.1, subtask_b_id); }
1417
1418 #[tokio::test]
1419 async fn test_plan_executor_idempotency() {
1420 use crate::test_utils::test_helpers::TestContext;
1421
1422 let ctx = TestContext::new().await;
1423
1424 let request = PlanRequest {
1426 tasks: vec![TaskTree {
1427 name: "Idempotent Task".to_string(),
1428 spec: Some("Initial spec".to_string()),
1429 priority: Some(PriorityValue::High),
1430 children: Some(vec![
1431 TaskTree {
1432 name: "Child 1".to_string(),
1433 spec: Some("Child spec 1".to_string()),
1434 priority: None,
1435 children: None,
1436 depends_on: None,
1437 task_id: None,
1438 },
1439 TaskTree {
1440 name: "Child 2".to_string(),
1441 spec: Some("Child spec 2".to_string()),
1442 priority: Some(PriorityValue::Low),
1443 children: None,
1444 depends_on: None,
1445 task_id: None,
1446 },
1447 ]),
1448 depends_on: None,
1449 task_id: None,
1450 }],
1451 };
1452
1453 let executor = PlanExecutor::new(&ctx.pool);
1454
1455 let result1 = executor.execute(&request).await.unwrap();
1457 assert!(result1.success, "First execution should succeed");
1458 assert_eq!(result1.created_count, 3, "Should create 3 tasks");
1459 assert_eq!(result1.updated_count, 0, "Should not update any tasks");
1460 assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
1461
1462 let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
1464 let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
1465 let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
1466
1467 let result2 = executor.execute(&request).await.unwrap();
1469 assert!(result2.success, "Second execution should succeed");
1470 assert_eq!(result2.created_count, 0, "Should not create any new tasks");
1471 assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
1472 assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
1473
1474 let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
1476 let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
1477 let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
1478
1479 assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
1480 assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
1481 assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
1482
1483 let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1485 .bind(parent_id_2)
1486 .fetch_one(&ctx.pool)
1487 .await
1488 .unwrap();
1489
1490 assert_eq!(parent.0, "Initial spec");
1491 assert_eq!(parent.1, 2); let modified_request = PlanRequest {
1495 tasks: vec![TaskTree {
1496 name: "Idempotent Task".to_string(),
1497 spec: Some("Updated spec".to_string()), priority: Some(PriorityValue::Critical), children: Some(vec![
1500 TaskTree {
1501 name: "Child 1".to_string(),
1502 spec: Some("Updated child spec 1".to_string()), priority: None,
1504 children: None,
1505 depends_on: None,
1506 task_id: None,
1507 },
1508 TaskTree {
1509 name: "Child 2".to_string(),
1510 spec: Some("Child spec 2".to_string()), priority: Some(PriorityValue::Low),
1512 children: None,
1513 depends_on: None,
1514 task_id: None,
1515 },
1516 ]),
1517 depends_on: None,
1518 task_id: None,
1519 }],
1520 };
1521
1522 let result3 = executor.execute(&modified_request).await.unwrap();
1523 assert!(result3.success, "Third execution should succeed");
1524 assert_eq!(result3.created_count, 0, "Should not create any new tasks");
1525 assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
1526
1527 let updated_parent: (String, i64) =
1529 sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1530 .bind(parent_id_2)
1531 .fetch_one(&ctx.pool)
1532 .await
1533 .unwrap();
1534
1535 assert_eq!(updated_parent.0, "Updated spec");
1536 assert_eq!(updated_parent.1, 1); let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
1539 .bind(child1_id_2)
1540 .fetch_one(&ctx.pool)
1541 .await
1542 .unwrap();
1543
1544 assert_eq!(updated_child1.0, "Updated child spec 1");
1545 }
1546
1547 #[tokio::test]
1548 async fn test_plan_executor_dependencies() {
1549 use crate::test_utils::test_helpers::TestContext;
1550
1551 let ctx = TestContext::new().await;
1552
1553 let request = PlanRequest {
1555 tasks: vec![
1556 TaskTree {
1557 name: "Foundation".to_string(),
1558 spec: Some("Base layer".to_string()),
1559 priority: Some(PriorityValue::Critical),
1560 children: None,
1561 depends_on: None,
1562 task_id: None,
1563 },
1564 TaskTree {
1565 name: "Layer 1".to_string(),
1566 spec: Some("Depends on Foundation".to_string()),
1567 priority: Some(PriorityValue::High),
1568 children: None,
1569 depends_on: Some(vec!["Foundation".to_string()]),
1570 task_id: None,
1571 },
1572 TaskTree {
1573 name: "Layer 2".to_string(),
1574 spec: Some("Depends on Layer 1".to_string()),
1575 priority: None,
1576 children: None,
1577 depends_on: Some(vec!["Layer 1".to_string()]),
1578 task_id: None,
1579 },
1580 TaskTree {
1581 name: "Integration".to_string(),
1582 spec: Some("Depends on both Foundation and Layer 2".to_string()),
1583 priority: None,
1584 children: None,
1585 depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
1586 task_id: None,
1587 },
1588 ],
1589 };
1590
1591 let executor = PlanExecutor::new(&ctx.pool);
1592 let result = executor.execute(&request).await.unwrap();
1593
1594 assert!(result.success, "Plan execution should succeed");
1595 assert_eq!(result.created_count, 4, "Should create 4 tasks");
1596 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
1597
1598 let foundation_id = *result.task_id_map.get("Foundation").unwrap();
1600 let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
1601 let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
1602 let integration_id = *result.task_id_map.get("Integration").unwrap();
1603
1604 let deps1: Vec<(i64,)> =
1606 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1607 .bind(layer1_id)
1608 .fetch_all(&ctx.pool)
1609 .await
1610 .unwrap();
1611
1612 assert_eq!(deps1.len(), 1);
1613 assert_eq!(deps1[0].0, foundation_id);
1614
1615 let deps2: Vec<(i64,)> =
1617 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1618 .bind(layer2_id)
1619 .fetch_all(&ctx.pool)
1620 .await
1621 .unwrap();
1622
1623 assert_eq!(deps2.len(), 1);
1624 assert_eq!(deps2[0].0, layer1_id);
1625
1626 let deps3: Vec<(i64,)> =
1628 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
1629 .bind(integration_id)
1630 .fetch_all(&ctx.pool)
1631 .await
1632 .unwrap();
1633
1634 assert_eq!(deps3.len(), 2);
1635 let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
1636 blocking_ids.sort();
1637
1638 let mut expected_ids = vec![foundation_id, layer2_id];
1639 expected_ids.sort();
1640
1641 assert_eq!(blocking_ids, expected_ids);
1642 }
1643
1644 #[tokio::test]
1645 async fn test_plan_executor_invalid_dependency() {
1646 use crate::test_utils::test_helpers::TestContext;
1647
1648 let ctx = TestContext::new().await;
1649
1650 let request = PlanRequest {
1652 tasks: vec![TaskTree {
1653 name: "Task A".to_string(),
1654 spec: Some("Depends on non-existent task".to_string()),
1655 priority: None,
1656 children: None,
1657 depends_on: Some(vec!["NonExistent".to_string()]),
1658 task_id: None,
1659 }],
1660 };
1661
1662 let executor = PlanExecutor::new(&ctx.pool);
1663 let result = executor.execute(&request).await.unwrap();
1664
1665 assert!(!result.success, "Plan execution should fail");
1666 assert!(result.error.is_some(), "Should have error message");
1667 let error = result.error.unwrap();
1668 assert!(
1669 error.contains("NonExistent"),
1670 "Error should mention the missing dependency: {}",
1671 error
1672 );
1673 }
1674
1675 #[tokio::test]
1676 async fn test_plan_executor_simple_cycle() {
1677 use crate::test_utils::test_helpers::TestContext;
1678
1679 let ctx = TestContext::new().await;
1680
1681 let request = PlanRequest {
1683 tasks: vec![
1684 TaskTree {
1685 name: "Task A".to_string(),
1686 spec: Some("Depends on B".to_string()),
1687 priority: None,
1688 children: None,
1689 depends_on: Some(vec!["Task B".to_string()]),
1690 task_id: None,
1691 },
1692 TaskTree {
1693 name: "Task B".to_string(),
1694 spec: Some("Depends on A".to_string()),
1695 priority: None,
1696 children: None,
1697 depends_on: Some(vec!["Task A".to_string()]),
1698 task_id: None,
1699 },
1700 ],
1701 };
1702
1703 let executor = PlanExecutor::new(&ctx.pool);
1704 let result = executor.execute(&request).await.unwrap();
1705
1706 assert!(!result.success, "Plan execution should fail");
1707 assert!(result.error.is_some(), "Should have error message");
1708 let error = result.error.unwrap();
1709 assert!(
1710 error.contains("Circular dependency"),
1711 "Error should mention circular dependency: {}",
1712 error
1713 );
1714 assert!(
1715 error.contains("Task A") && error.contains("Task B"),
1716 "Error should mention both tasks in the cycle: {}",
1717 error
1718 );
1719 }
1720
1721 #[tokio::test]
1722 async fn test_plan_executor_complex_cycle() {
1723 use crate::test_utils::test_helpers::TestContext;
1724
1725 let ctx = TestContext::new().await;
1726
1727 let request = PlanRequest {
1729 tasks: vec![
1730 TaskTree {
1731 name: "Task A".to_string(),
1732 spec: Some("Depends on B".to_string()),
1733 priority: None,
1734 children: None,
1735 depends_on: Some(vec!["Task B".to_string()]),
1736 task_id: None,
1737 },
1738 TaskTree {
1739 name: "Task B".to_string(),
1740 spec: Some("Depends on C".to_string()),
1741 priority: None,
1742 children: None,
1743 depends_on: Some(vec!["Task C".to_string()]),
1744 task_id: None,
1745 },
1746 TaskTree {
1747 name: "Task C".to_string(),
1748 spec: Some("Depends on A".to_string()),
1749 priority: None,
1750 children: None,
1751 depends_on: Some(vec!["Task A".to_string()]),
1752 task_id: None,
1753 },
1754 ],
1755 };
1756
1757 let executor = PlanExecutor::new(&ctx.pool);
1758 let result = executor.execute(&request).await.unwrap();
1759
1760 assert!(!result.success, "Plan execution should fail");
1761 assert!(result.error.is_some(), "Should have error message");
1762 let error = result.error.unwrap();
1763 assert!(
1764 error.contains("Circular dependency"),
1765 "Error should mention circular dependency: {}",
1766 error
1767 );
1768 assert!(
1769 error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
1770 "Error should mention all tasks in the cycle: {}",
1771 error
1772 );
1773 }
1774
1775 #[tokio::test]
1776 async fn test_plan_executor_valid_dag() {
1777 use crate::test_utils::test_helpers::TestContext;
1778
1779 let ctx = TestContext::new().await;
1780
1781 let request = PlanRequest {
1788 tasks: vec![
1789 TaskTree {
1790 name: "Task A".to_string(),
1791 spec: Some("Root task".to_string()),
1792 priority: None,
1793 children: None,
1794 depends_on: None,
1795 task_id: None,
1796 },
1797 TaskTree {
1798 name: "Task B".to_string(),
1799 spec: Some("Depends on A".to_string()),
1800 priority: None,
1801 children: None,
1802 depends_on: Some(vec!["Task A".to_string()]),
1803 task_id: None,
1804 },
1805 TaskTree {
1806 name: "Task C".to_string(),
1807 spec: Some("Depends on A".to_string()),
1808 priority: None,
1809 children: None,
1810 depends_on: Some(vec!["Task A".to_string()]),
1811 task_id: None,
1812 },
1813 TaskTree {
1814 name: "Task D".to_string(),
1815 spec: Some("Depends on B and C".to_string()),
1816 priority: None,
1817 children: None,
1818 depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
1819 task_id: None,
1820 },
1821 ],
1822 };
1823
1824 let executor = PlanExecutor::new(&ctx.pool);
1825 let result = executor.execute(&request).await.unwrap();
1826
1827 assert!(
1828 result.success,
1829 "Plan execution should succeed for valid DAG"
1830 );
1831 assert_eq!(result.created_count, 4, "Should create 4 tasks");
1832 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
1833 }
1834
1835 #[tokio::test]
1836 async fn test_plan_executor_self_dependency() {
1837 use crate::test_utils::test_helpers::TestContext;
1838
1839 let ctx = TestContext::new().await;
1840
1841 let request = PlanRequest {
1843 tasks: vec![TaskTree {
1844 name: "Task A".to_string(),
1845 spec: Some("Depends on itself".to_string()),
1846 priority: None,
1847 children: None,
1848 depends_on: Some(vec!["Task A".to_string()]),
1849 task_id: None,
1850 }],
1851 };
1852
1853 let executor = PlanExecutor::new(&ctx.pool);
1854 let result = executor.execute(&request).await.unwrap();
1855
1856 assert!(
1857 !result.success,
1858 "Plan execution should fail for self-dependency"
1859 );
1860 assert!(result.error.is_some(), "Should have error message");
1861 let error = result.error.unwrap();
1862 assert!(
1863 error.contains("Circular dependency"),
1864 "Error should mention circular dependency: {}",
1865 error
1866 );
1867 }
1868
1869 #[tokio::test]
1871 async fn test_find_tasks_by_names_empty() {
1872 use crate::test_utils::test_helpers::TestContext;
1873
1874 let ctx = TestContext::new().await;
1875 let executor = PlanExecutor::new(&ctx.pool);
1876
1877 let result = executor.find_tasks_by_names(&[]).await.unwrap();
1878 assert!(result.is_empty(), "Empty input should return empty map");
1879 }
1880
1881 #[tokio::test]
1882 async fn test_find_tasks_by_names_partial() {
1883 use crate::test_utils::test_helpers::TestContext;
1884
1885 let ctx = TestContext::new().await;
1886 let executor = PlanExecutor::new(&ctx.pool);
1887
1888 let request = PlanRequest {
1890 tasks: vec![
1891 TaskTree {
1892 name: "Task A".to_string(),
1893 spec: None,
1894 priority: None,
1895 children: None,
1896 depends_on: None,
1897 task_id: None,
1898 },
1899 TaskTree {
1900 name: "Task B".to_string(),
1901 spec: None,
1902 priority: None,
1903 children: None,
1904 depends_on: None,
1905 task_id: None,
1906 },
1907 ],
1908 };
1909 executor.execute(&request).await.unwrap();
1910
1911 let names = vec![
1913 "Task A".to_string(),
1914 "Task B".to_string(),
1915 "Task C".to_string(),
1916 ];
1917 let result = executor.find_tasks_by_names(&names).await.unwrap();
1918
1919 assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
1920 assert!(result.contains_key("Task A"));
1921 assert!(result.contains_key("Task B"));
1922 assert!(!result.contains_key("Task C"));
1923 }
1924
1925 #[tokio::test]
1927 async fn test_plan_1000_tasks_performance() {
1928 use crate::test_utils::test_helpers::TestContext;
1929
1930 let ctx = TestContext::new().await;
1931 let executor = PlanExecutor::new(&ctx.pool);
1932
1933 let mut tasks = Vec::new();
1935 for i in 0..1000 {
1936 tasks.push(TaskTree {
1937 name: format!("Task {}", i),
1938 spec: Some(format!("Spec for task {}", i)),
1939 priority: Some(PriorityValue::Medium),
1940 children: None,
1941 depends_on: None,
1942 task_id: None,
1943 });
1944 }
1945
1946 let request = PlanRequest { tasks };
1947
1948 let start = std::time::Instant::now();
1949 let result = executor.execute(&request).await.unwrap();
1950 let duration = start.elapsed();
1951
1952 assert!(result.success);
1953 assert_eq!(result.created_count, 1000);
1954 assert!(
1955 duration.as_secs() < 10,
1956 "Should complete 1000 tasks in under 10 seconds, took {:?}",
1957 duration
1958 );
1959
1960 println!("✅ Created 1000 tasks in {:?}", duration);
1961 }
1962
1963 #[tokio::test]
1964 async fn test_plan_deep_nesting_20_levels() {
1965 use crate::test_utils::test_helpers::TestContext;
1966
1967 let ctx = TestContext::new().await;
1968 let executor = PlanExecutor::new(&ctx.pool);
1969
1970 fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
1972 TaskTree {
1973 name: format!("Level {}", current),
1974 spec: Some(format!("Task at depth {}", current)),
1975 priority: Some(PriorityValue::Low),
1976 children: if current < depth {
1977 Some(vec![build_deep_tree(depth, current + 1)])
1978 } else {
1979 None
1980 },
1981 depends_on: None,
1982 task_id: None,
1983 }
1984 }
1985
1986 let request = PlanRequest {
1987 tasks: vec![build_deep_tree(20, 1)],
1988 };
1989
1990 let start = std::time::Instant::now();
1991 let result = executor.execute(&request).await.unwrap();
1992 let duration = start.elapsed();
1993
1994 assert!(result.success);
1995 assert_eq!(
1996 result.created_count, 20,
1997 "Should create 20 tasks (1 per level)"
1998 );
1999 assert!(
2000 duration.as_secs() < 5,
2001 "Should handle 20-level nesting in under 5 seconds, took {:?}",
2002 duration
2003 );
2004
2005 println!("✅ Created 20-level deep tree in {:?}", duration);
2006 }
2007
2008 #[test]
2009 fn test_flatten_preserves_all_fields() {
2010 let tasks = vec![TaskTree {
2011 name: "Full Task".to_string(),
2012 spec: Some("Detailed spec".to_string()),
2013 priority: Some(PriorityValue::Critical),
2014 children: None,
2015 depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2016 task_id: Some(42),
2017 }];
2018
2019 let flat = flatten_task_tree(&tasks);
2020 assert_eq!(flat.len(), 1);
2021
2022 let task = &flat[0];
2023 assert_eq!(task.name, "Full Task");
2024 assert_eq!(task.spec, Some("Detailed spec".to_string()));
2025 assert_eq!(task.priority, Some(PriorityValue::Critical));
2026 assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2027 assert_eq!(task.task_id, Some(42));
2028 }
2029}