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 #[serde(skip_serializing_if = "Option::is_none")]
46 pub status: Option<TaskStatus>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
51 pub active_form: Option<String>,
52}
53
54#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum TaskStatus {
58 Todo,
59 Doing,
60 Done,
61}
62
63impl TaskStatus {
64 pub fn as_db_str(&self) -> &'static str {
66 match self {
67 TaskStatus::Todo => "todo",
68 TaskStatus::Doing => "doing",
69 TaskStatus::Done => "done",
70 }
71 }
72
73 pub fn from_db_str(s: &str) -> Option<Self> {
75 match s {
76 "todo" => Some(TaskStatus::Todo),
77 "doing" => Some(TaskStatus::Doing),
78 "done" => Some(TaskStatus::Done),
79 _ => None,
80 }
81 }
82
83 pub fn as_str(&self) -> &'static str {
85 match self {
86 TaskStatus::Todo => "todo",
87 TaskStatus::Doing => "doing",
88 TaskStatus::Done => "done",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
95#[serde(rename_all = "lowercase")]
96pub enum PriorityValue {
97 Critical,
98 High,
99 Medium,
100 Low,
101}
102
103impl PriorityValue {
104 pub fn to_int(&self) -> i32 {
106 match self {
107 PriorityValue::Critical => 1,
108 PriorityValue::High => 2,
109 PriorityValue::Medium => 3,
110 PriorityValue::Low => 4,
111 }
112 }
113
114 pub fn from_int(value: i32) -> Option<Self> {
116 match value {
117 1 => Some(PriorityValue::Critical),
118 2 => Some(PriorityValue::High),
119 3 => Some(PriorityValue::Medium),
120 4 => Some(PriorityValue::Low),
121 _ => None,
122 }
123 }
124
125 pub fn as_str(&self) -> &'static str {
127 match self {
128 PriorityValue::Critical => "critical",
129 PriorityValue::High => "high",
130 PriorityValue::Medium => "medium",
131 PriorityValue::Low => "low",
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138pub struct PlanResult {
139 pub success: bool,
141
142 pub task_id_map: HashMap<String, i64>,
144
145 pub created_count: usize,
147
148 pub updated_count: usize,
150
151 pub dependency_count: usize,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub error: Option<String>,
157}
158
159impl PlanResult {
160 pub fn success(
162 task_id_map: HashMap<String, i64>,
163 created_count: usize,
164 updated_count: usize,
165 dependency_count: usize,
166 ) -> Self {
167 Self {
168 success: true,
169 task_id_map,
170 created_count,
171 updated_count,
172 dependency_count,
173 error: None,
174 }
175 }
176
177 pub fn error(message: impl Into<String>) -> Self {
179 Self {
180 success: false,
181 task_id_map: HashMap::new(),
182 created_count: 0,
183 updated_count: 0,
184 dependency_count: 0,
185 error: Some(message.into()),
186 }
187 }
188}
189
190pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
196 let mut names = Vec::new();
197
198 for task in tasks {
199 names.push(task.name.clone());
200
201 if let Some(children) = &task.children {
202 names.extend(extract_all_names(children));
203 }
204 }
205
206 names
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct FlatTask {
212 pub name: String,
213 pub spec: Option<String>,
214 pub priority: Option<PriorityValue>,
215 pub parent_name: Option<String>,
216 pub depends_on: Vec<String>,
217 pub task_id: Option<i64>,
218 pub status: Option<TaskStatus>,
219 pub active_form: Option<String>,
220}
221
222pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
223 flatten_task_tree_recursive(tasks, None)
224}
225
226fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
227 let mut flat = Vec::new();
228
229 for task in tasks {
230 let flat_task = FlatTask {
231 name: task.name.clone(),
232 spec: task.spec.clone(),
233 priority: task.priority.clone(),
234 parent_name: parent_name.clone(),
235 depends_on: task.depends_on.clone().unwrap_or_default(),
236 task_id: task.task_id,
237 status: task.status.clone(),
238 active_form: task.active_form.clone(),
239 };
240
241 flat.push(flat_task);
242
243 if let Some(children) = &task.children {
245 flat.extend(flatten_task_tree_recursive(
246 children,
247 Some(task.name.clone()),
248 ));
249 }
250 }
251
252 flat
253}
254
255#[derive(Debug, Clone, PartialEq)]
257pub enum Operation {
258 Create(FlatTask),
259 Update { task_id: i64, task: FlatTask },
260}
261
262pub fn classify_operations(
271 flat_tasks: &[FlatTask],
272 existing_names: &HashMap<String, i64>,
273) -> Vec<Operation> {
274 let mut operations = Vec::new();
275
276 for task in flat_tasks {
277 let operation = if let Some(task_id) = task.task_id {
279 Operation::Update {
281 task_id,
282 task: task.clone(),
283 }
284 } else if let Some(&task_id) = existing_names.get(&task.name) {
285 Operation::Update {
287 task_id,
288 task: task.clone(),
289 }
290 } else {
291 Operation::Create(task.clone())
293 };
294
295 operations.push(operation);
296 }
297
298 operations
299}
300
301pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
303 let mut seen = HashMap::new();
304 let mut duplicates = Vec::new();
305
306 for name in extract_all_names(tasks) {
307 let count = seen.entry(name.clone()).or_insert(0);
308 *count += 1;
309 if *count == 2 {
310 duplicates.push(name);
312 }
313 }
314
315 duplicates
316}
317
318use crate::error::{IntentError, Result};
323use chrono::Utc;
324use sqlx::SqlitePool;
325
326pub struct PlanExecutor<'a> {
328 pool: &'a SqlitePool,
329}
330
331impl<'a> PlanExecutor<'a> {
332 pub fn new(pool: &'a SqlitePool) -> Self {
334 Self { pool }
335 }
336
337 pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
339 let duplicates = find_duplicate_names(&request.tasks);
341 if !duplicates.is_empty() {
342 return Ok(PlanResult::error(format!(
343 "Duplicate task names in request: {:?}",
344 duplicates
345 )));
346 }
347
348 let all_names = extract_all_names(&request.tasks);
350
351 let existing = self.find_tasks_by_names(&all_names).await?;
353
354 let flat_tasks = flatten_task_tree(&request.tasks);
356
357 if let Err(e) = self.validate_dependencies(&flat_tasks) {
359 return Ok(PlanResult::error(e.to_string()));
360 }
361
362 if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
364 return Ok(PlanResult::error(e.to_string()));
365 }
366
367 if let Err(e) = self.validate_batch_single_doing(&flat_tasks) {
369 return Ok(PlanResult::error(e.to_string()));
370 }
371
372 let mut tx = self.pool.begin().await?;
374
375 let mut task_id_map = HashMap::new();
377 let mut created_count = 0;
378 let mut updated_count = 0;
379
380 for task in &flat_tasks {
381 if let Some(&existing_id) = existing.get(&task.name) {
382 self.update_task_in_tx(&mut tx, existing_id, task).await?;
384 task_id_map.insert(task.name.clone(), existing_id);
385 updated_count += 1;
386 } else {
387 let id = self.create_task_in_tx(&mut tx, task).await?;
389 task_id_map.insert(task.name.clone(), id);
390 created_count += 1;
391 }
392 }
393
394 self.build_parent_child_relations(&mut tx, &flat_tasks, &task_id_map)
396 .await?;
397
398 let dep_count = self
400 .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
401 .await?;
402
403 tx.commit().await?;
405
406 let doing_task = flat_tasks
409 .iter()
410 .find(|task| matches!(task.status, Some(TaskStatus::Doing)));
411
412 if let Some(doing_task) = doing_task {
413 if let Some(&task_id) = task_id_map.get(&doing_task.name) {
415 use crate::tasks::TaskManager;
417 let task_mgr = TaskManager::new(self.pool);
418 task_mgr.start_task(task_id, false).await?;
419 }
420 }
421
422 Ok(PlanResult::success(
424 task_id_map,
425 created_count,
426 updated_count,
427 dep_count,
428 ))
429 }
430
431 async fn find_tasks_by_names(&self, names: &[String]) -> Result<HashMap<String, i64>> {
433 if names.is_empty() {
434 return Ok(HashMap::new());
435 }
436
437 let mut map = HashMap::new();
438
439 let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
442 let query = format!(
443 "SELECT id, name FROM tasks WHERE name IN ({})",
444 placeholders
445 );
446
447 let mut query_builder = sqlx::query(&query);
448 for name in names {
449 query_builder = query_builder.bind(name);
450 }
451
452 let rows = query_builder.fetch_all(self.pool).await?;
453
454 for row in rows {
455 let id: i64 = row.get("id");
456 let name: String = row.get("name");
457 map.insert(name, id);
458 }
459
460 Ok(map)
461 }
462
463 async fn create_task_in_tx(
465 &self,
466 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
467 task: &FlatTask,
468 ) -> Result<i64> {
469 let now = Utc::now();
470 let priority = task.priority.as_ref().map(|p| p.to_int()).unwrap_or(3); let status_str = match &task.status {
474 Some(status) => status.as_db_str(),
475 None => "todo",
476 };
477
478 let result = sqlx::query(
479 r#"
480 INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at)
481 VALUES (?, ?, ?, ?, ?, ?)
482 "#,
483 )
484 .bind(&task.name)
485 .bind(&task.spec)
486 .bind(priority)
487 .bind(status_str)
488 .bind(&task.active_form)
489 .bind(now)
490 .execute(&mut **tx)
491 .await?;
492
493 Ok(result.last_insert_rowid())
494 }
495
496 async fn update_task_in_tx(
499 &self,
500 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
501 task_id: i64,
502 task: &FlatTask,
503 ) -> Result<()> {
504 if let Some(spec) = &task.spec {
506 sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
507 .bind(spec)
508 .bind(task_id)
509 .execute(&mut **tx)
510 .await?;
511 }
512
513 if let Some(priority) = &task.priority {
515 sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
516 .bind(priority.to_int())
517 .bind(task_id)
518 .execute(&mut **tx)
519 .await?;
520 }
521
522 if let Some(status) = &task.status {
524 sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
525 .bind(status.as_db_str())
526 .bind(task_id)
527 .execute(&mut **tx)
528 .await?;
529 }
530
531 if let Some(active_form) = &task.active_form {
533 sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
534 .bind(active_form)
535 .bind(task_id)
536 .execute(&mut **tx)
537 .await?;
538 }
539
540 Ok(())
545 }
546
547 async fn build_parent_child_relations(
549 &self,
550 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
551 flat_tasks: &[FlatTask],
552 task_id_map: &HashMap<String, i64>,
553 ) -> Result<()> {
554 for task in flat_tasks {
555 if let Some(parent_name) = &task.parent_name {
556 let task_id = task_id_map.get(&task.name).ok_or_else(|| {
557 IntentError::InvalidInput(format!("Task not found: {}", task.name))
558 })?;
559
560 let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
561 IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
562 })?;
563
564 sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
565 .bind(parent_id)
566 .bind(task_id)
567 .execute(&mut **tx)
568 .await?;
569 }
570 }
571
572 Ok(())
573 }
574
575 async fn build_dependencies(
577 &self,
578 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
579 flat_tasks: &[FlatTask],
580 task_id_map: &HashMap<String, i64>,
581 ) -> Result<usize> {
582 let mut count = 0;
583
584 for task in flat_tasks {
585 if !task.depends_on.is_empty() {
586 let blocked_id = task_id_map.get(&task.name).ok_or_else(|| {
587 IntentError::InvalidInput(format!("Task not found: {}", task.name))
588 })?;
589
590 for dep_name in &task.depends_on {
591 let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
592 IntentError::InvalidInput(format!(
593 "Dependency '{}' not found for task '{}'",
594 dep_name, task.name
595 ))
596 })?;
597
598 sqlx::query(
599 "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
600 )
601 .bind(blocking_id)
602 .bind(blocked_id)
603 .execute(&mut **tx)
604 .await?;
605
606 count += 1;
607 }
608 }
609 }
610
611 Ok(count)
612 }
613
614 fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
616 let task_names: std::collections::HashSet<_> =
617 flat_tasks.iter().map(|t| t.name.as_str()).collect();
618
619 for task in flat_tasks {
620 for dep_name in &task.depends_on {
621 if !task_names.contains(dep_name.as_str()) {
622 return Err(IntentError::InvalidInput(format!(
623 "Task '{}' depends on '{}', but '{}' is not in the plan",
624 task.name, dep_name, dep_name
625 )));
626 }
627 }
628 }
629
630 Ok(())
631 }
632
633 fn validate_batch_single_doing(&self, flat_tasks: &[FlatTask]) -> Result<()> {
637 let doing_tasks: Vec<&FlatTask> = flat_tasks
639 .iter()
640 .filter(|task| matches!(task.status, Some(TaskStatus::Doing)))
641 .collect();
642
643 if doing_tasks.len() > 1 {
645 let names: Vec<&str> = doing_tasks.iter().map(|t| t.name.as_str()).collect();
646 return Err(IntentError::InvalidInput(format!(
647 "Batch single doing constraint violated: only one task per batch can have status='doing'. Found: {}",
648 names.join(", ")
649 )));
650 }
651
652 Ok(())
653 }
654
655 fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
657 if flat_tasks.is_empty() {
658 return Ok(());
659 }
660
661 let name_to_idx: HashMap<&str, usize> = flat_tasks
663 .iter()
664 .enumerate()
665 .map(|(i, t)| (t.name.as_str(), i))
666 .collect();
667
668 let mut graph: Vec<Vec<usize>> = vec![Vec::new(); flat_tasks.len()];
670 for (idx, task) in flat_tasks.iter().enumerate() {
671 for dep_name in &task.depends_on {
672 if let Some(&dep_idx) = name_to_idx.get(dep_name.as_str()) {
673 graph[idx].push(dep_idx);
674 }
675 }
676 }
677
678 for task in flat_tasks {
680 if task.depends_on.contains(&task.name) {
681 return Err(IntentError::InvalidInput(format!(
682 "Circular dependency detected: task '{}' depends on itself",
683 task.name
684 )));
685 }
686 }
687
688 let sccs = self.tarjan_scc(&graph);
690
691 for scc in sccs {
693 if scc.len() > 1 {
694 let cycle_names: Vec<&str> = scc
696 .iter()
697 .map(|&idx| flat_tasks[idx].name.as_str())
698 .collect();
699
700 return Err(IntentError::InvalidInput(format!(
701 "Circular dependency detected: {}",
702 cycle_names.join(" → ")
703 )));
704 }
705 }
706
707 Ok(())
708 }
709
710 fn tarjan_scc(&self, graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
713 let n = graph.len();
714 let mut index = 0;
715 let mut stack = Vec::new();
716 let mut indices = vec![None; n];
717 let mut lowlinks = vec![0; n];
718 let mut on_stack = vec![false; n];
719 let mut sccs = Vec::new();
720
721 #[allow(clippy::too_many_arguments)]
722 fn strongconnect(
723 v: usize,
724 graph: &[Vec<usize>],
725 index: &mut usize,
726 stack: &mut Vec<usize>,
727 indices: &mut [Option<usize>],
728 lowlinks: &mut [usize],
729 on_stack: &mut [bool],
730 sccs: &mut Vec<Vec<usize>>,
731 ) {
732 indices[v] = Some(*index);
734 lowlinks[v] = *index;
735 *index += 1;
736 stack.push(v);
737 on_stack[v] = true;
738
739 for &w in &graph[v] {
741 if indices[w].is_none() {
742 strongconnect(w, graph, index, stack, indices, lowlinks, on_stack, sccs);
744 lowlinks[v] = lowlinks[v].min(lowlinks[w]);
745 } else if on_stack[w] {
746 lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
748 }
749 }
750
751 if lowlinks[v] == indices[v].unwrap() {
753 let mut scc = Vec::new();
754 loop {
755 let w = stack.pop().unwrap();
756 on_stack[w] = false;
757 scc.push(w);
758 if w == v {
759 break;
760 }
761 }
762 sccs.push(scc);
763 }
764 }
765
766 for v in 0..n {
768 if indices[v].is_none() {
769 strongconnect(
770 v,
771 graph,
772 &mut index,
773 &mut stack,
774 &mut indices,
775 &mut lowlinks,
776 &mut on_stack,
777 &mut sccs,
778 );
779 }
780 }
781
782 sccs
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn test_priority_value_to_int() {
792 assert_eq!(PriorityValue::Critical.to_int(), 1);
793 assert_eq!(PriorityValue::High.to_int(), 2);
794 assert_eq!(PriorityValue::Medium.to_int(), 3);
795 assert_eq!(PriorityValue::Low.to_int(), 4);
796 }
797
798 #[test]
799 fn test_priority_value_from_int() {
800 assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
801 assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
802 assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
803 assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
804 assert_eq!(PriorityValue::from_int(999), None);
805 }
806
807 #[test]
808 fn test_priority_value_as_str() {
809 assert_eq!(PriorityValue::Critical.as_str(), "critical");
810 assert_eq!(PriorityValue::High.as_str(), "high");
811 assert_eq!(PriorityValue::Medium.as_str(), "medium");
812 assert_eq!(PriorityValue::Low.as_str(), "low");
813 }
814
815 #[test]
816 fn test_plan_request_deserialization_minimal() {
817 let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
818 let request: PlanRequest = serde_json::from_str(json).unwrap();
819
820 assert_eq!(request.tasks.len(), 1);
821 assert_eq!(request.tasks[0].name, "Test Task");
822 assert_eq!(request.tasks[0].spec, None);
823 assert_eq!(request.tasks[0].priority, None);
824 assert_eq!(request.tasks[0].children, None);
825 assert_eq!(request.tasks[0].depends_on, None);
826 assert_eq!(request.tasks[0].task_id, None);
827 }
828
829 #[test]
830 fn test_plan_request_deserialization_full() {
831 let json = r#"{
832 "tasks": [{
833 "name": "Parent Task",
834 "spec": "Parent spec",
835 "priority": "high",
836 "children": [{
837 "name": "Child Task",
838 "spec": "Child spec"
839 }],
840 "depends_on": ["Other Task"],
841 "task_id": 42
842 }]
843 }"#;
844
845 let request: PlanRequest = serde_json::from_str(json).unwrap();
846
847 assert_eq!(request.tasks.len(), 1);
848 let parent = &request.tasks[0];
849 assert_eq!(parent.name, "Parent Task");
850 assert_eq!(parent.spec, Some("Parent spec".to_string()));
851 assert_eq!(parent.priority, Some(PriorityValue::High));
852 assert_eq!(parent.task_id, Some(42));
853
854 let children = parent.children.as_ref().unwrap();
855 assert_eq!(children.len(), 1);
856 assert_eq!(children[0].name, "Child Task");
857
858 let depends = parent.depends_on.as_ref().unwrap();
859 assert_eq!(depends.len(), 1);
860 assert_eq!(depends[0], "Other Task");
861 }
862
863 #[test]
864 fn test_plan_request_serialization() {
865 let request = PlanRequest {
866 tasks: vec![TaskTree {
867 name: "Test Task".to_string(),
868 spec: Some("Test spec".to_string()),
869 priority: Some(PriorityValue::Medium),
870 children: None,
871 depends_on: None,
872 task_id: None,
873 status: None,
874 active_form: None,
875 }],
876 };
877
878 let json = serde_json::to_string(&request).unwrap();
879 assert!(json.contains("\"name\":\"Test Task\""));
880 assert!(json.contains("\"spec\":\"Test spec\""));
881 assert!(json.contains("\"priority\":\"medium\""));
882 }
883
884 #[test]
885 fn test_plan_result_success() {
886 let mut map = HashMap::new();
887 map.insert("Task 1".to_string(), 1);
888 map.insert("Task 2".to_string(), 2);
889
890 let result = PlanResult::success(map.clone(), 2, 0, 1);
891
892 assert!(result.success);
893 assert_eq!(result.task_id_map, map);
894 assert_eq!(result.created_count, 2);
895 assert_eq!(result.updated_count, 0);
896 assert_eq!(result.dependency_count, 1);
897 assert_eq!(result.error, None);
898 }
899
900 #[test]
901 fn test_plan_result_error() {
902 let result = PlanResult::error("Test error");
903
904 assert!(!result.success);
905 assert_eq!(result.task_id_map.len(), 0);
906 assert_eq!(result.created_count, 0);
907 assert_eq!(result.updated_count, 0);
908 assert_eq!(result.dependency_count, 0);
909 assert_eq!(result.error, Some("Test error".to_string()));
910 }
911
912 #[test]
913 fn test_task_tree_nested() {
914 let tree = TaskTree {
915 name: "Parent".to_string(),
916 spec: None,
917 priority: None,
918 children: Some(vec![
919 TaskTree {
920 name: "Child 1".to_string(),
921 spec: None,
922 priority: None,
923 children: None,
924 depends_on: None,
925 task_id: None,
926 status: None,
927 active_form: None,
928 },
929 TaskTree {
930 name: "Child 2".to_string(),
931 spec: None,
932 priority: Some(PriorityValue::High),
933 children: None,
934 depends_on: None,
935 task_id: None,
936 status: None,
937 active_form: None,
938 },
939 ]),
940 depends_on: None,
941 task_id: None,
942 status: None,
943 active_form: None,
944 };
945
946 let json = serde_json::to_string_pretty(&tree).unwrap();
947 let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
948
949 assert_eq!(tree, deserialized);
950 assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
951 }
952
953 #[test]
954 fn test_priority_value_case_insensitive_deserialization() {
955 let json = r#"{"name": "Test", "priority": "high"}"#;
957 let task: TaskTree = serde_json::from_str(json).unwrap();
958 assert_eq!(task.priority, Some(PriorityValue::High));
959
960 }
963
964 #[test]
965 fn test_extract_all_names_simple() {
966 let tasks = vec![
967 TaskTree {
968 name: "Task 1".to_string(),
969 spec: None,
970 priority: None,
971 children: None,
972 depends_on: None,
973 task_id: None,
974 status: None,
975 active_form: None,
976 },
977 TaskTree {
978 name: "Task 2".to_string(),
979 spec: None,
980 priority: None,
981 children: None,
982 depends_on: None,
983 task_id: None,
984 status: None,
985 active_form: None,
986 },
987 ];
988
989 let names = extract_all_names(&tasks);
990 assert_eq!(names, vec!["Task 1", "Task 2"]);
991 }
992
993 #[test]
994 fn test_extract_all_names_nested() {
995 let tasks = vec![TaskTree {
996 name: "Parent".to_string(),
997 spec: None,
998 priority: None,
999 children: Some(vec![
1000 TaskTree {
1001 name: "Child 1".to_string(),
1002 spec: None,
1003 priority: None,
1004 children: None,
1005 depends_on: None,
1006 task_id: None,
1007 status: None,
1008 active_form: None,
1009 },
1010 TaskTree {
1011 name: "Child 2".to_string(),
1012 spec: None,
1013 priority: None,
1014 children: Some(vec![TaskTree {
1015 name: "Grandchild".to_string(),
1016 spec: None,
1017 priority: None,
1018 children: None,
1019 depends_on: None,
1020 task_id: None,
1021 status: None,
1022 active_form: None,
1023 }]),
1024 depends_on: None,
1025 task_id: None,
1026 status: None,
1027 active_form: None,
1028 },
1029 ]),
1030 depends_on: None,
1031 task_id: None,
1032 status: None,
1033 active_form: None,
1034 }];
1035
1036 let names = extract_all_names(&tasks);
1037 assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
1038 }
1039
1040 #[test]
1041 fn test_flatten_task_tree_simple() {
1042 let tasks = vec![TaskTree {
1043 name: "Task 1".to_string(),
1044 spec: Some("Spec 1".to_string()),
1045 priority: Some(PriorityValue::High),
1046 children: None,
1047 depends_on: Some(vec!["Task 0".to_string()]),
1048 task_id: None,
1049 status: None,
1050 active_form: None,
1051 }];
1052
1053 let flat = flatten_task_tree(&tasks);
1054 assert_eq!(flat.len(), 1);
1055 assert_eq!(flat[0].name, "Task 1");
1056 assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
1057 assert_eq!(flat[0].priority, Some(PriorityValue::High));
1058 assert_eq!(flat[0].parent_name, None);
1059 assert_eq!(flat[0].depends_on, vec!["Task 0"]);
1060 }
1061
1062 #[test]
1063 fn test_flatten_task_tree_nested() {
1064 let tasks = vec![TaskTree {
1065 name: "Parent".to_string(),
1066 spec: None,
1067 priority: None,
1068 children: Some(vec![
1069 TaskTree {
1070 name: "Child 1".to_string(),
1071 spec: None,
1072 priority: None,
1073 children: None,
1074 depends_on: None,
1075 task_id: None,
1076 status: None,
1077 active_form: None,
1078 },
1079 TaskTree {
1080 name: "Child 2".to_string(),
1081 spec: None,
1082 priority: None,
1083 children: None,
1084 depends_on: None,
1085 task_id: None,
1086 status: None,
1087 active_form: None,
1088 },
1089 ]),
1090 depends_on: None,
1091 task_id: None,
1092 status: None,
1093 active_form: None,
1094 }];
1095
1096 let flat = flatten_task_tree(&tasks);
1097 assert_eq!(flat.len(), 3);
1098
1099 assert_eq!(flat[0].name, "Parent");
1101 assert_eq!(flat[0].parent_name, None);
1102
1103 assert_eq!(flat[1].name, "Child 1");
1105 assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
1106
1107 assert_eq!(flat[2].name, "Child 2");
1108 assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
1109 }
1110
1111 #[test]
1112 fn test_classify_operations_all_create() {
1113 let flat_tasks = vec![
1114 FlatTask {
1115 name: "Task 1".to_string(),
1116 spec: None,
1117 priority: None,
1118 parent_name: None,
1119 depends_on: vec![],
1120 task_id: None,
1121 status: None,
1122 active_form: None,
1123 },
1124 FlatTask {
1125 name: "Task 2".to_string(),
1126 spec: None,
1127 priority: None,
1128 parent_name: None,
1129 depends_on: vec![],
1130 task_id: None,
1131 status: None,
1132 active_form: None,
1133 },
1134 ];
1135
1136 let existing = HashMap::new();
1137 let operations = classify_operations(&flat_tasks, &existing);
1138
1139 assert_eq!(operations.len(), 2);
1140 assert!(matches!(operations[0], Operation::Create(_)));
1141 assert!(matches!(operations[1], Operation::Create(_)));
1142 }
1143
1144 #[test]
1145 fn test_classify_operations_all_update() {
1146 let flat_tasks = vec![
1147 FlatTask {
1148 name: "Task 1".to_string(),
1149 spec: None,
1150 priority: None,
1151 parent_name: None,
1152 depends_on: vec![],
1153 task_id: None,
1154 status: None,
1155 active_form: None,
1156 },
1157 FlatTask {
1158 name: "Task 2".to_string(),
1159 spec: None,
1160 priority: None,
1161 parent_name: None,
1162 depends_on: vec![],
1163 task_id: None,
1164 status: None,
1165 active_form: None,
1166 },
1167 ];
1168
1169 let mut existing = HashMap::new();
1170 existing.insert("Task 1".to_string(), 1);
1171 existing.insert("Task 2".to_string(), 2);
1172
1173 let operations = classify_operations(&flat_tasks, &existing);
1174
1175 assert_eq!(operations.len(), 2);
1176 assert!(matches!(
1177 operations[0],
1178 Operation::Update { task_id: 1, .. }
1179 ));
1180 assert!(matches!(
1181 operations[1],
1182 Operation::Update { task_id: 2, .. }
1183 ));
1184 }
1185
1186 #[test]
1187 fn test_classify_operations_mixed() {
1188 let flat_tasks = vec![
1189 FlatTask {
1190 name: "Existing Task".to_string(),
1191 spec: None,
1192 priority: None,
1193 parent_name: None,
1194 depends_on: vec![],
1195 task_id: None,
1196 status: None,
1197 active_form: None,
1198 },
1199 FlatTask {
1200 name: "New Task".to_string(),
1201 spec: None,
1202 priority: None,
1203 parent_name: None,
1204 depends_on: vec![],
1205 task_id: None,
1206 status: None,
1207 active_form: None,
1208 },
1209 ];
1210
1211 let mut existing = HashMap::new();
1212 existing.insert("Existing Task".to_string(), 42);
1213
1214 let operations = classify_operations(&flat_tasks, &existing);
1215
1216 assert_eq!(operations.len(), 2);
1217 assert!(matches!(
1218 operations[0],
1219 Operation::Update { task_id: 42, .. }
1220 ));
1221 assert!(matches!(operations[1], Operation::Create(_)));
1222 }
1223
1224 #[test]
1225 fn test_classify_operations_explicit_task_id() {
1226 let flat_tasks = vec![FlatTask {
1227 name: "Task".to_string(),
1228 spec: None,
1229 priority: None,
1230 parent_name: None,
1231 depends_on: vec![],
1232 task_id: Some(99), status: None,
1234 active_form: None,
1235 }];
1236
1237 let existing = HashMap::new(); let operations = classify_operations(&flat_tasks, &existing);
1240
1241 assert_eq!(operations.len(), 1);
1243 assert!(matches!(
1244 operations[0],
1245 Operation::Update { task_id: 99, .. }
1246 ));
1247 }
1248
1249 #[test]
1250 fn test_find_duplicate_names_no_duplicates() {
1251 let tasks = vec![
1252 TaskTree {
1253 name: "Task 1".to_string(),
1254 spec: None,
1255 priority: None,
1256 children: None,
1257 depends_on: None,
1258 task_id: None,
1259 status: None,
1260 active_form: None,
1261 },
1262 TaskTree {
1263 name: "Task 2".to_string(),
1264 spec: None,
1265 priority: None,
1266 children: None,
1267 depends_on: None,
1268 task_id: None,
1269 status: None,
1270 active_form: None,
1271 },
1272 ];
1273
1274 let duplicates = find_duplicate_names(&tasks);
1275 assert_eq!(duplicates.len(), 0);
1276 }
1277
1278 #[test]
1279 fn test_find_duplicate_names_with_duplicates() {
1280 let tasks = vec![
1281 TaskTree {
1282 name: "Duplicate".to_string(),
1283 spec: None,
1284 priority: None,
1285 children: None,
1286 depends_on: None,
1287 task_id: None,
1288 status: None,
1289 active_form: None,
1290 },
1291 TaskTree {
1292 name: "Unique".to_string(),
1293 spec: None,
1294 priority: None,
1295 children: None,
1296 depends_on: None,
1297 task_id: None,
1298 status: None,
1299 active_form: None,
1300 },
1301 TaskTree {
1302 name: "Duplicate".to_string(),
1303 spec: None,
1304 priority: None,
1305 children: None,
1306 depends_on: None,
1307 task_id: None,
1308 status: None,
1309 active_form: None,
1310 },
1311 ];
1312
1313 let duplicates = find_duplicate_names(&tasks);
1314 assert_eq!(duplicates.len(), 1);
1315 assert_eq!(duplicates[0], "Duplicate");
1316 }
1317
1318 #[test]
1319 fn test_find_duplicate_names_nested() {
1320 let tasks = vec![TaskTree {
1321 name: "Parent".to_string(),
1322 spec: None,
1323 priority: None,
1324 children: Some(vec![TaskTree {
1325 name: "Parent".to_string(), spec: None,
1327 priority: None,
1328 children: None,
1329 depends_on: None,
1330 task_id: None,
1331 status: None,
1332 active_form: None,
1333 }]),
1334 depends_on: None,
1335 task_id: None,
1336 status: None,
1337 active_form: None,
1338 }];
1339
1340 let duplicates = find_duplicate_names(&tasks);
1341 assert_eq!(duplicates.len(), 1);
1342 assert_eq!(duplicates[0], "Parent");
1343 }
1344
1345 #[test]
1346 fn test_flatten_task_tree_empty() {
1347 let tasks: Vec<TaskTree> = vec![];
1348 let flat = flatten_task_tree(&tasks);
1349 assert_eq!(flat.len(), 0);
1350 }
1351
1352 #[test]
1353 fn test_flatten_task_tree_deep_nesting() {
1354 let tasks = vec![TaskTree {
1356 name: "Root".to_string(),
1357 spec: None,
1358 priority: None,
1359 children: Some(vec![TaskTree {
1360 name: "Level1".to_string(),
1361 spec: None,
1362 priority: None,
1363 children: Some(vec![TaskTree {
1364 name: "Level2".to_string(),
1365 spec: None,
1366 priority: None,
1367 children: Some(vec![TaskTree {
1368 name: "Level3".to_string(),
1369 spec: None,
1370 priority: None,
1371 children: None,
1372 depends_on: None,
1373 task_id: None,
1374 status: None,
1375 active_form: None,
1376 }]),
1377 depends_on: None,
1378 task_id: None,
1379 status: None,
1380 active_form: None,
1381 }]),
1382 depends_on: None,
1383 task_id: None,
1384 status: None,
1385 active_form: None,
1386 }]),
1387 depends_on: None,
1388 task_id: None,
1389 status: None,
1390 active_form: None,
1391 }];
1392
1393 let flat = flatten_task_tree(&tasks);
1394 assert_eq!(flat.len(), 4);
1395
1396 assert_eq!(flat[0].name, "Root");
1398 assert_eq!(flat[0].parent_name, None);
1399
1400 assert_eq!(flat[1].name, "Level1");
1401 assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1402
1403 assert_eq!(flat[2].name, "Level2");
1404 assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1405
1406 assert_eq!(flat[3].name, "Level3");
1407 assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1408 }
1409
1410 #[test]
1411 fn test_flatten_task_tree_many_siblings() {
1412 let children: Vec<TaskTree> = (0..10)
1413 .map(|i| TaskTree {
1414 name: format!("Child {}", i),
1415 spec: None,
1416 priority: None,
1417 children: None,
1418 depends_on: None,
1419 task_id: None,
1420 status: None,
1421 active_form: None,
1422 })
1423 .collect();
1424
1425 let tasks = vec![TaskTree {
1426 name: "Parent".to_string(),
1427 spec: None,
1428 priority: None,
1429 children: Some(children),
1430 depends_on: None,
1431 task_id: None,
1432 status: None,
1433 active_form: None,
1434 }];
1435
1436 let flat = flatten_task_tree(&tasks);
1437 assert_eq!(flat.len(), 11); for child in flat.iter().skip(1).take(10) {
1441 assert_eq!(child.parent_name, Some("Parent".to_string()));
1442 }
1443 }
1444
1445 #[test]
1446 fn test_flatten_task_tree_complex_mixed() {
1447 let tasks = vec![
1449 TaskTree {
1450 name: "Task 1".to_string(),
1451 spec: None,
1452 priority: None,
1453 children: Some(vec![
1454 TaskTree {
1455 name: "Task 1.1".to_string(),
1456 spec: None,
1457 priority: None,
1458 children: None,
1459 depends_on: None,
1460 task_id: None,
1461 status: None,
1462 active_form: None,
1463 },
1464 TaskTree {
1465 name: "Task 1.2".to_string(),
1466 spec: None,
1467 priority: None,
1468 children: Some(vec![TaskTree {
1469 name: "Task 1.2.1".to_string(),
1470 spec: None,
1471 priority: None,
1472 children: None,
1473 depends_on: None,
1474 task_id: None,
1475 status: None,
1476 active_form: None,
1477 }]),
1478 depends_on: None,
1479 task_id: None,
1480 status: None,
1481 active_form: None,
1482 },
1483 ]),
1484 depends_on: None,
1485 task_id: None,
1486 status: None,
1487 active_form: None,
1488 },
1489 TaskTree {
1490 name: "Task 2".to_string(),
1491 spec: None,
1492 priority: None,
1493 children: None,
1494 depends_on: Some(vec!["Task 1".to_string()]),
1495 task_id: None,
1496 status: None,
1497 active_form: None,
1498 },
1499 ];
1500
1501 let flat = flatten_task_tree(&tasks);
1502 assert_eq!(flat.len(), 5);
1503
1504 assert_eq!(flat[0].name, "Task 1");
1506 assert_eq!(flat[0].parent_name, None);
1507
1508 assert_eq!(flat[1].name, "Task 1.1");
1509 assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
1510
1511 assert_eq!(flat[2].name, "Task 1.2");
1512 assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
1513
1514 assert_eq!(flat[3].name, "Task 1.2.1");
1515 assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
1516
1517 assert_eq!(flat[4].name, "Task 2");
1518 assert_eq!(flat[4].parent_name, None);
1519 assert_eq!(flat[4].depends_on, vec!["Task 1"]);
1520 }
1521
1522 #[tokio::test]
1523 async fn test_plan_executor_integration() {
1524 use crate::test_utils::test_helpers::TestContext;
1525
1526 let ctx = TestContext::new().await;
1527
1528 let request = PlanRequest {
1530 tasks: vec![TaskTree {
1531 name: "Integration Test Plan".to_string(),
1532 spec: Some("Test plan execution end-to-end".to_string()),
1533 priority: Some(PriorityValue::High),
1534 children: Some(vec![
1535 TaskTree {
1536 name: "Subtask A".to_string(),
1537 spec: Some("First subtask".to_string()),
1538 priority: None,
1539 children: None,
1540 depends_on: None,
1541 task_id: None,
1542 status: None,
1543 active_form: None,
1544 },
1545 TaskTree {
1546 name: "Subtask B".to_string(),
1547 spec: Some("Second subtask depends on A".to_string()),
1548 priority: None,
1549 children: None,
1550 depends_on: Some(vec!["Subtask A".to_string()]),
1551 task_id: None,
1552 status: None,
1553 active_form: None,
1554 },
1555 ]),
1556 depends_on: None,
1557 task_id: None,
1558 status: None,
1559 active_form: None,
1560 }],
1561 };
1562
1563 let executor = PlanExecutor::new(&ctx.pool);
1565 let result = executor.execute(&request).await.unwrap();
1566
1567 assert!(result.success, "Plan execution should succeed");
1569 assert_eq!(result.created_count, 3, "Should create 3 tasks");
1570 assert_eq!(result.updated_count, 0, "Should not update any tasks");
1571 assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
1572 assert!(result.error.is_none(), "Should have no error");
1573
1574 assert_eq!(result.task_id_map.len(), 3);
1576 assert!(result.task_id_map.contains_key("Integration Test Plan"));
1577 assert!(result.task_id_map.contains_key("Subtask A"));
1578 assert!(result.task_id_map.contains_key("Subtask B"));
1579
1580 let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
1582 let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
1583 let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
1584
1585 let parent: (String, String, i64, Option<i64>) =
1587 sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
1588 .bind(parent_id)
1589 .fetch_one(&ctx.pool)
1590 .await
1591 .unwrap();
1592
1593 assert_eq!(parent.0, "Integration Test Plan");
1594 assert_eq!(parent.1, "Test plan execution end-to-end");
1595 assert_eq!(parent.2, 2); assert_eq!(parent.3, None); let subtask_a: (String, Option<i64>) =
1600 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
1601 .bind(subtask_a_id)
1602 .fetch_one(&ctx.pool)
1603 .await
1604 .unwrap();
1605
1606 assert_eq!(subtask_a.0, "Subtask A");
1607 assert_eq!(subtask_a.1, Some(parent_id)); let dep: (i64, i64) = sqlx::query_as(
1611 "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
1612 )
1613 .bind(subtask_b_id)
1614 .fetch_one(&ctx.pool)
1615 .await
1616 .unwrap();
1617
1618 assert_eq!(dep.0, subtask_a_id); assert_eq!(dep.1, subtask_b_id); }
1621
1622 #[tokio::test]
1623 async fn test_plan_executor_idempotency() {
1624 use crate::test_utils::test_helpers::TestContext;
1625
1626 let ctx = TestContext::new().await;
1627
1628 let request = PlanRequest {
1630 tasks: vec![TaskTree {
1631 name: "Idempotent Task".to_string(),
1632 spec: Some("Initial spec".to_string()),
1633 priority: Some(PriorityValue::High),
1634 children: Some(vec![
1635 TaskTree {
1636 name: "Child 1".to_string(),
1637 spec: Some("Child spec 1".to_string()),
1638 priority: None,
1639 children: None,
1640 depends_on: None,
1641 task_id: None,
1642 status: None,
1643 active_form: None,
1644 },
1645 TaskTree {
1646 name: "Child 2".to_string(),
1647 spec: Some("Child spec 2".to_string()),
1648 priority: Some(PriorityValue::Low),
1649 children: None,
1650 depends_on: None,
1651 task_id: None,
1652 status: None,
1653 active_form: None,
1654 },
1655 ]),
1656 depends_on: None,
1657 task_id: None,
1658 status: None,
1659 active_form: None,
1660 }],
1661 };
1662
1663 let executor = PlanExecutor::new(&ctx.pool);
1664
1665 let result1 = executor.execute(&request).await.unwrap();
1667 assert!(result1.success, "First execution should succeed");
1668 assert_eq!(result1.created_count, 3, "Should create 3 tasks");
1669 assert_eq!(result1.updated_count, 0, "Should not update any tasks");
1670 assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
1671
1672 let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
1674 let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
1675 let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
1676
1677 let result2 = executor.execute(&request).await.unwrap();
1679 assert!(result2.success, "Second execution should succeed");
1680 assert_eq!(result2.created_count, 0, "Should not create any new tasks");
1681 assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
1682 assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
1683
1684 let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
1686 let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
1687 let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
1688
1689 assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
1690 assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
1691 assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
1692
1693 let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1695 .bind(parent_id_2)
1696 .fetch_one(&ctx.pool)
1697 .await
1698 .unwrap();
1699
1700 assert_eq!(parent.0, "Initial spec");
1701 assert_eq!(parent.1, 2); let modified_request = PlanRequest {
1705 tasks: vec![TaskTree {
1706 name: "Idempotent Task".to_string(),
1707 spec: Some("Updated spec".to_string()), priority: Some(PriorityValue::Critical), children: Some(vec![
1710 TaskTree {
1711 name: "Child 1".to_string(),
1712 spec: Some("Updated child spec 1".to_string()), priority: None,
1714 children: None,
1715 depends_on: None,
1716 task_id: None,
1717 status: None,
1718 active_form: None,
1719 },
1720 TaskTree {
1721 name: "Child 2".to_string(),
1722 spec: Some("Child spec 2".to_string()), priority: Some(PriorityValue::Low),
1724 children: None,
1725 depends_on: None,
1726 task_id: None,
1727 status: None,
1728 active_form: None,
1729 },
1730 ]),
1731 depends_on: None,
1732 task_id: None,
1733 status: None,
1734 active_form: None,
1735 }],
1736 };
1737
1738 let result3 = executor.execute(&modified_request).await.unwrap();
1739 assert!(result3.success, "Third execution should succeed");
1740 assert_eq!(result3.created_count, 0, "Should not create any new tasks");
1741 assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
1742
1743 let updated_parent: (String, i64) =
1745 sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1746 .bind(parent_id_2)
1747 .fetch_one(&ctx.pool)
1748 .await
1749 .unwrap();
1750
1751 assert_eq!(updated_parent.0, "Updated spec");
1752 assert_eq!(updated_parent.1, 1); let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
1755 .bind(child1_id_2)
1756 .fetch_one(&ctx.pool)
1757 .await
1758 .unwrap();
1759
1760 assert_eq!(updated_child1.0, "Updated child spec 1");
1761 }
1762
1763 #[tokio::test]
1764 async fn test_plan_executor_dependencies() {
1765 use crate::test_utils::test_helpers::TestContext;
1766
1767 let ctx = TestContext::new().await;
1768
1769 let request = PlanRequest {
1771 tasks: vec![
1772 TaskTree {
1773 name: "Foundation".to_string(),
1774 spec: Some("Base layer".to_string()),
1775 priority: Some(PriorityValue::Critical),
1776 children: None,
1777 depends_on: None,
1778 task_id: None,
1779 status: None,
1780 active_form: None,
1781 },
1782 TaskTree {
1783 name: "Layer 1".to_string(),
1784 spec: Some("Depends on Foundation".to_string()),
1785 priority: Some(PriorityValue::High),
1786 children: None,
1787 depends_on: Some(vec!["Foundation".to_string()]),
1788 task_id: None,
1789 status: None,
1790 active_form: None,
1791 },
1792 TaskTree {
1793 name: "Layer 2".to_string(),
1794 spec: Some("Depends on Layer 1".to_string()),
1795 priority: None,
1796 children: None,
1797 depends_on: Some(vec!["Layer 1".to_string()]),
1798 task_id: None,
1799 status: None,
1800 active_form: None,
1801 },
1802 TaskTree {
1803 name: "Integration".to_string(),
1804 spec: Some("Depends on both Foundation and Layer 2".to_string()),
1805 priority: None,
1806 children: None,
1807 depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
1808 task_id: None,
1809 status: None,
1810 active_form: None,
1811 },
1812 ],
1813 };
1814
1815 let executor = PlanExecutor::new(&ctx.pool);
1816 let result = executor.execute(&request).await.unwrap();
1817
1818 assert!(result.success, "Plan execution should succeed");
1819 assert_eq!(result.created_count, 4, "Should create 4 tasks");
1820 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
1821
1822 let foundation_id = *result.task_id_map.get("Foundation").unwrap();
1824 let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
1825 let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
1826 let integration_id = *result.task_id_map.get("Integration").unwrap();
1827
1828 let deps1: Vec<(i64,)> =
1830 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1831 .bind(layer1_id)
1832 .fetch_all(&ctx.pool)
1833 .await
1834 .unwrap();
1835
1836 assert_eq!(deps1.len(), 1);
1837 assert_eq!(deps1[0].0, foundation_id);
1838
1839 let deps2: Vec<(i64,)> =
1841 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1842 .bind(layer2_id)
1843 .fetch_all(&ctx.pool)
1844 .await
1845 .unwrap();
1846
1847 assert_eq!(deps2.len(), 1);
1848 assert_eq!(deps2[0].0, layer1_id);
1849
1850 let deps3: Vec<(i64,)> =
1852 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
1853 .bind(integration_id)
1854 .fetch_all(&ctx.pool)
1855 .await
1856 .unwrap();
1857
1858 assert_eq!(deps3.len(), 2);
1859 let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
1860 blocking_ids.sort();
1861
1862 let mut expected_ids = vec![foundation_id, layer2_id];
1863 expected_ids.sort();
1864
1865 assert_eq!(blocking_ids, expected_ids);
1866 }
1867
1868 #[tokio::test]
1869 async fn test_plan_executor_invalid_dependency() {
1870 use crate::test_utils::test_helpers::TestContext;
1871
1872 let ctx = TestContext::new().await;
1873
1874 let request = PlanRequest {
1876 tasks: vec![TaskTree {
1877 name: "Task A".to_string(),
1878 spec: Some("Depends on non-existent task".to_string()),
1879 priority: None,
1880 children: None,
1881 depends_on: Some(vec!["NonExistent".to_string()]),
1882 task_id: None,
1883 status: None,
1884 active_form: None,
1885 }],
1886 };
1887
1888 let executor = PlanExecutor::new(&ctx.pool);
1889 let result = executor.execute(&request).await.unwrap();
1890
1891 assert!(!result.success, "Plan execution should fail");
1892 assert!(result.error.is_some(), "Should have error message");
1893 let error = result.error.unwrap();
1894 assert!(
1895 error.contains("NonExistent"),
1896 "Error should mention the missing dependency: {}",
1897 error
1898 );
1899 }
1900
1901 #[tokio::test]
1902 async fn test_plan_executor_simple_cycle() {
1903 use crate::test_utils::test_helpers::TestContext;
1904
1905 let ctx = TestContext::new().await;
1906
1907 let request = PlanRequest {
1909 tasks: vec![
1910 TaskTree {
1911 name: "Task A".to_string(),
1912 spec: Some("Depends on B".to_string()),
1913 priority: None,
1914 children: None,
1915 depends_on: Some(vec!["Task B".to_string()]),
1916 task_id: None,
1917 status: None,
1918 active_form: None,
1919 },
1920 TaskTree {
1921 name: "Task B".to_string(),
1922 spec: Some("Depends on A".to_string()),
1923 priority: None,
1924 children: None,
1925 depends_on: Some(vec!["Task A".to_string()]),
1926 task_id: None,
1927 status: None,
1928 active_form: None,
1929 },
1930 ],
1931 };
1932
1933 let executor = PlanExecutor::new(&ctx.pool);
1934 let result = executor.execute(&request).await.unwrap();
1935
1936 assert!(!result.success, "Plan execution should fail");
1937 assert!(result.error.is_some(), "Should have error message");
1938 let error = result.error.unwrap();
1939 assert!(
1940 error.contains("Circular dependency"),
1941 "Error should mention circular dependency: {}",
1942 error
1943 );
1944 assert!(
1945 error.contains("Task A") && error.contains("Task B"),
1946 "Error should mention both tasks in the cycle: {}",
1947 error
1948 );
1949 }
1950
1951 #[tokio::test]
1952 async fn test_plan_executor_complex_cycle() {
1953 use crate::test_utils::test_helpers::TestContext;
1954
1955 let ctx = TestContext::new().await;
1956
1957 let request = PlanRequest {
1959 tasks: vec![
1960 TaskTree {
1961 name: "Task A".to_string(),
1962 spec: Some("Depends on B".to_string()),
1963 priority: None,
1964 children: None,
1965 depends_on: Some(vec!["Task B".to_string()]),
1966 task_id: None,
1967 status: None,
1968 active_form: None,
1969 },
1970 TaskTree {
1971 name: "Task B".to_string(),
1972 spec: Some("Depends on C".to_string()),
1973 priority: None,
1974 children: None,
1975 depends_on: Some(vec!["Task C".to_string()]),
1976 task_id: None,
1977 status: None,
1978 active_form: None,
1979 },
1980 TaskTree {
1981 name: "Task C".to_string(),
1982 spec: Some("Depends on A".to_string()),
1983 priority: None,
1984 children: None,
1985 depends_on: Some(vec!["Task A".to_string()]),
1986 task_id: None,
1987 status: None,
1988 active_form: None,
1989 },
1990 ],
1991 };
1992
1993 let executor = PlanExecutor::new(&ctx.pool);
1994 let result = executor.execute(&request).await.unwrap();
1995
1996 assert!(!result.success, "Plan execution should fail");
1997 assert!(result.error.is_some(), "Should have error message");
1998 let error = result.error.unwrap();
1999 assert!(
2000 error.contains("Circular dependency"),
2001 "Error should mention circular dependency: {}",
2002 error
2003 );
2004 assert!(
2005 error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
2006 "Error should mention all tasks in the cycle: {}",
2007 error
2008 );
2009 }
2010
2011 #[tokio::test]
2012 async fn test_plan_executor_valid_dag() {
2013 use crate::test_utils::test_helpers::TestContext;
2014
2015 let ctx = TestContext::new().await;
2016
2017 let request = PlanRequest {
2024 tasks: vec![
2025 TaskTree {
2026 name: "Task A".to_string(),
2027 spec: Some("Root task".to_string()),
2028 priority: None,
2029 children: None,
2030 depends_on: None,
2031 task_id: None,
2032 status: None,
2033 active_form: None,
2034 },
2035 TaskTree {
2036 name: "Task B".to_string(),
2037 spec: Some("Depends on A".to_string()),
2038 priority: None,
2039 children: None,
2040 depends_on: Some(vec!["Task A".to_string()]),
2041 task_id: None,
2042 status: None,
2043 active_form: None,
2044 },
2045 TaskTree {
2046 name: "Task C".to_string(),
2047 spec: Some("Depends on A".to_string()),
2048 priority: None,
2049 children: None,
2050 depends_on: Some(vec!["Task A".to_string()]),
2051 task_id: None,
2052 status: None,
2053 active_form: None,
2054 },
2055 TaskTree {
2056 name: "Task D".to_string(),
2057 spec: Some("Depends on B and C".to_string()),
2058 priority: None,
2059 children: None,
2060 depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
2061 task_id: None,
2062 status: None,
2063 active_form: None,
2064 },
2065 ],
2066 };
2067
2068 let executor = PlanExecutor::new(&ctx.pool);
2069 let result = executor.execute(&request).await.unwrap();
2070
2071 assert!(
2072 result.success,
2073 "Plan execution should succeed for valid DAG"
2074 );
2075 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2076 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2077 }
2078
2079 #[tokio::test]
2080 async fn test_plan_executor_self_dependency() {
2081 use crate::test_utils::test_helpers::TestContext;
2082
2083 let ctx = TestContext::new().await;
2084
2085 let request = PlanRequest {
2087 tasks: vec![TaskTree {
2088 name: "Task A".to_string(),
2089 spec: Some("Depends on itself".to_string()),
2090 priority: None,
2091 children: None,
2092 depends_on: Some(vec!["Task A".to_string()]),
2093 task_id: None,
2094 status: None,
2095 active_form: None,
2096 }],
2097 };
2098
2099 let executor = PlanExecutor::new(&ctx.pool);
2100 let result = executor.execute(&request).await.unwrap();
2101
2102 assert!(
2103 !result.success,
2104 "Plan execution should fail for self-dependency"
2105 );
2106 assert!(result.error.is_some(), "Should have error message");
2107 let error = result.error.unwrap();
2108 assert!(
2109 error.contains("Circular dependency"),
2110 "Error should mention circular dependency: {}",
2111 error
2112 );
2113 }
2114
2115 #[tokio::test]
2117 async fn test_find_tasks_by_names_empty() {
2118 use crate::test_utils::test_helpers::TestContext;
2119
2120 let ctx = TestContext::new().await;
2121 let executor = PlanExecutor::new(&ctx.pool);
2122
2123 let result = executor.find_tasks_by_names(&[]).await.unwrap();
2124 assert!(result.is_empty(), "Empty input should return empty map");
2125 }
2126
2127 #[tokio::test]
2128 async fn test_find_tasks_by_names_partial() {
2129 use crate::test_utils::test_helpers::TestContext;
2130
2131 let ctx = TestContext::new().await;
2132 let executor = PlanExecutor::new(&ctx.pool);
2133
2134 let request = PlanRequest {
2136 tasks: vec![
2137 TaskTree {
2138 name: "Task A".to_string(),
2139 spec: None,
2140 priority: None,
2141 children: None,
2142 depends_on: None,
2143 task_id: None,
2144 status: None,
2145 active_form: None,
2146 },
2147 TaskTree {
2148 name: "Task B".to_string(),
2149 spec: None,
2150 priority: None,
2151 children: None,
2152 depends_on: None,
2153 task_id: None,
2154 status: None,
2155 active_form: None,
2156 },
2157 ],
2158 };
2159 executor.execute(&request).await.unwrap();
2160
2161 let names = vec![
2163 "Task A".to_string(),
2164 "Task B".to_string(),
2165 "Task C".to_string(),
2166 ];
2167 let result = executor.find_tasks_by_names(&names).await.unwrap();
2168
2169 assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
2170 assert!(result.contains_key("Task A"));
2171 assert!(result.contains_key("Task B"));
2172 assert!(!result.contains_key("Task C"));
2173 }
2174
2175 #[tokio::test]
2177 async fn test_plan_1000_tasks_performance() {
2178 use crate::test_utils::test_helpers::TestContext;
2179
2180 let ctx = TestContext::new().await;
2181 let executor = PlanExecutor::new(&ctx.pool);
2182
2183 let mut tasks = Vec::new();
2185 for i in 0..1000 {
2186 tasks.push(TaskTree {
2187 name: format!("Task {}", i),
2188 spec: Some(format!("Spec for task {}", i)),
2189 priority: Some(PriorityValue::Medium),
2190 children: None,
2191 depends_on: None,
2192 task_id: None,
2193 status: None,
2194 active_form: None,
2195 });
2196 }
2197
2198 let request = PlanRequest { tasks };
2199
2200 let start = std::time::Instant::now();
2201 let result = executor.execute(&request).await.unwrap();
2202 let duration = start.elapsed();
2203
2204 assert!(result.success);
2205 assert_eq!(result.created_count, 1000);
2206 assert!(
2207 duration.as_secs() < 10,
2208 "Should complete 1000 tasks in under 10 seconds, took {:?}",
2209 duration
2210 );
2211
2212 println!("✅ Created 1000 tasks in {:?}", duration);
2213 }
2214
2215 #[tokio::test]
2216 async fn test_plan_deep_nesting_20_levels() {
2217 use crate::test_utils::test_helpers::TestContext;
2218
2219 let ctx = TestContext::new().await;
2220 let executor = PlanExecutor::new(&ctx.pool);
2221
2222 fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
2224 TaskTree {
2225 name: format!("Level {}", current),
2226 spec: Some(format!("Task at depth {}", current)),
2227 priority: Some(PriorityValue::Low),
2228 children: if current < depth {
2229 Some(vec![build_deep_tree(depth, current + 1)])
2230 } else {
2231 None
2232 },
2233 depends_on: None,
2234 task_id: None,
2235 status: None,
2236 active_form: None,
2237 }
2238 }
2239
2240 let request = PlanRequest {
2241 tasks: vec![build_deep_tree(20, 1)],
2242 };
2243
2244 let start = std::time::Instant::now();
2245 let result = executor.execute(&request).await.unwrap();
2246 let duration = start.elapsed();
2247
2248 assert!(result.success);
2249 assert_eq!(
2250 result.created_count, 20,
2251 "Should create 20 tasks (1 per level)"
2252 );
2253 assert!(
2254 duration.as_secs() < 5,
2255 "Should handle 20-level nesting in under 5 seconds, took {:?}",
2256 duration
2257 );
2258
2259 println!("✅ Created 20-level deep tree in {:?}", duration);
2260 }
2261
2262 #[test]
2263 fn test_flatten_preserves_all_fields() {
2264 let tasks = vec![TaskTree {
2265 name: "Full Task".to_string(),
2266 spec: Some("Detailed spec".to_string()),
2267 priority: Some(PriorityValue::Critical),
2268 children: None,
2269 depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2270 task_id: Some(42),
2271 status: None,
2272 active_form: None,
2273 }];
2274
2275 let flat = flatten_task_tree(&tasks);
2276 assert_eq!(flat.len(), 1);
2277
2278 let task = &flat[0];
2279 assert_eq!(task.name, "Full Task");
2280 assert_eq!(task.spec, Some("Detailed spec".to_string()));
2281 assert_eq!(task.priority, Some(PriorityValue::Critical));
2282 assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2283 assert_eq!(task.task_id, Some(42));
2284 }
2285}
2286
2287#[cfg(test)]
2288mod dataflow_tests {
2289 use super::*;
2290 use crate::tasks::TaskManager;
2291 use crate::test_utils::test_helpers::TestContext;
2292
2293 #[tokio::test]
2294 async fn test_complete_dataflow_status_and_active_form() {
2295 let ctx = TestContext::new().await;
2297
2298 let request = PlanRequest {
2300 tasks: vec![TaskTree {
2301 name: "Test Active Form Task".to_string(),
2302 spec: Some("Testing complete dataflow".to_string()),
2303 priority: Some(PriorityValue::High),
2304 children: None,
2305 depends_on: None,
2306 task_id: None,
2307 status: Some(TaskStatus::Doing),
2308 active_form: Some("Testing complete dataflow now".to_string()),
2309 }],
2310 };
2311
2312 let executor = PlanExecutor::new(&ctx.pool);
2313 let result = executor.execute(&request).await.unwrap();
2314
2315 assert!(result.success);
2316 assert_eq!(result.created_count, 1);
2317
2318 let task_mgr = TaskManager::new(&ctx.pool);
2320 let result = task_mgr
2321 .find_tasks(None, None, None, None, None)
2322 .await
2323 .unwrap();
2324
2325 assert_eq!(result.tasks.len(), 1);
2326 let task = &result.tasks[0];
2327
2328 assert_eq!(task.name, "Test Active Form Task");
2330 assert_eq!(task.status, "doing"); assert_eq!(
2332 task.active_form,
2333 Some("Testing complete dataflow now".to_string())
2334 );
2335
2336 let json = serde_json::to_value(task).unwrap();
2338 assert_eq!(json["name"], "Test Active Form Task");
2339 assert_eq!(json["status"], "doing");
2340 assert_eq!(json["active_form"], "Testing complete dataflow now");
2341
2342 println!("✅ 完整数据流验证成功!");
2343 println!(" Plan工具写入 -> Task读取 -> JSON序列化 -> MCP输出");
2344 println!(" active_form: {:?}", task.active_form);
2345 }
2346}