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 #[serde(
58 default,
59 skip_serializing_if = "Option::is_none",
60 deserialize_with = "deserialize_parent_id"
61 )]
62 pub parent_id: Option<Option<i64>>,
63}
64
65fn deserialize_parent_id<'de, D>(
71 deserializer: D,
72) -> std::result::Result<Option<Option<i64>>, D::Error>
73where
74 D: serde::Deserializer<'de>,
75{
76 let inner: Option<i64> = Option::deserialize(deserializer)?;
83 Ok(Some(inner))
84}
85
86#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
88#[serde(rename_all = "snake_case")]
89pub enum TaskStatus {
90 Todo,
91 Doing,
92 Done,
93}
94
95impl TaskStatus {
96 pub fn as_db_str(&self) -> &'static str {
98 match self {
99 TaskStatus::Todo => "todo",
100 TaskStatus::Doing => "doing",
101 TaskStatus::Done => "done",
102 }
103 }
104
105 pub fn from_db_str(s: &str) -> Option<Self> {
107 match s {
108 "todo" => Some(TaskStatus::Todo),
109 "doing" => Some(TaskStatus::Doing),
110 "done" => Some(TaskStatus::Done),
111 _ => None,
112 }
113 }
114
115 pub fn as_str(&self) -> &'static str {
117 match self {
118 TaskStatus::Todo => "todo",
119 TaskStatus::Doing => "doing",
120 TaskStatus::Done => "done",
121 }
122 }
123}
124
125#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
127#[serde(rename_all = "lowercase")]
128pub enum PriorityValue {
129 Critical,
130 High,
131 Medium,
132 Low,
133}
134
135impl PriorityValue {
136 pub fn to_int(&self) -> i32 {
138 match self {
139 PriorityValue::Critical => 1,
140 PriorityValue::High => 2,
141 PriorityValue::Medium => 3,
142 PriorityValue::Low => 4,
143 }
144 }
145
146 pub fn from_int(value: i32) -> Option<Self> {
148 match value {
149 1 => Some(PriorityValue::Critical),
150 2 => Some(PriorityValue::High),
151 3 => Some(PriorityValue::Medium),
152 4 => Some(PriorityValue::Low),
153 _ => None,
154 }
155 }
156
157 pub fn as_str(&self) -> &'static str {
159 match self {
160 PriorityValue::Critical => "critical",
161 PriorityValue::High => "high",
162 PriorityValue::Medium => "medium",
163 PriorityValue::Low => "low",
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct PlanResult {
171 pub success: bool,
173
174 pub task_id_map: HashMap<String, i64>,
176
177 pub created_count: usize,
179
180 pub updated_count: usize,
182
183 pub dependency_count: usize,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
189 pub focused_task: Option<crate::db::models::TaskWithEvents>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub error: Option<String>,
194}
195
196impl PlanResult {
197 pub fn success(
199 task_id_map: HashMap<String, i64>,
200 created_count: usize,
201 updated_count: usize,
202 dependency_count: usize,
203 focused_task: Option<crate::db::models::TaskWithEvents>,
204 ) -> Self {
205 Self {
206 success: true,
207 task_id_map,
208 created_count,
209 updated_count,
210 dependency_count,
211 focused_task,
212 error: None,
213 }
214 }
215
216 pub fn error(message: impl Into<String>) -> Self {
218 Self {
219 success: false,
220 task_id_map: HashMap::new(),
221 created_count: 0,
222 updated_count: 0,
223 dependency_count: 0,
224 focused_task: None,
225 error: Some(message.into()),
226 }
227 }
228}
229
230pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
236 let mut names = Vec::new();
237
238 for task in tasks {
239 names.push(task.name.clone());
240
241 if let Some(children) = &task.children {
242 names.extend(extract_all_names(children));
243 }
244 }
245
246 names
247}
248
249#[derive(Debug, Clone, PartialEq)]
251pub struct FlatTask {
252 pub name: String,
253 pub spec: Option<String>,
254 pub priority: Option<PriorityValue>,
255 pub parent_name: Option<String>,
257 pub depends_on: Vec<String>,
258 pub task_id: Option<i64>,
259 pub status: Option<TaskStatus>,
260 pub active_form: Option<String>,
261 pub explicit_parent_id: Option<Option<i64>>,
266}
267
268pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
269 flatten_task_tree_recursive(tasks, None)
270}
271
272fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
273 let mut flat = Vec::new();
274
275 for task in tasks {
276 let flat_task = FlatTask {
277 name: task.name.clone(),
278 spec: task.spec.clone(),
279 priority: task.priority.clone(),
280 parent_name: parent_name.clone(),
281 depends_on: task.depends_on.clone().unwrap_or_default(),
282 task_id: task.task_id,
283 status: task.status.clone(),
284 active_form: task.active_form.clone(),
285 explicit_parent_id: task.parent_id,
286 };
287
288 flat.push(flat_task);
289
290 if let Some(children) = &task.children {
292 flat.extend(flatten_task_tree_recursive(
293 children,
294 Some(task.name.clone()),
295 ));
296 }
297 }
298
299 flat
300}
301
302#[derive(Debug, Clone, PartialEq)]
304pub enum Operation {
305 Create(FlatTask),
306 Update { task_id: i64, task: FlatTask },
307}
308
309pub fn classify_operations(
318 flat_tasks: &[FlatTask],
319 existing_names: &HashMap<String, i64>,
320) -> Vec<Operation> {
321 let mut operations = Vec::new();
322
323 for task in flat_tasks {
324 let operation = if let Some(task_id) = task.task_id {
326 Operation::Update {
328 task_id,
329 task: task.clone(),
330 }
331 } else if let Some(&task_id) = existing_names.get(&task.name) {
332 Operation::Update {
334 task_id,
335 task: task.clone(),
336 }
337 } else {
338 Operation::Create(task.clone())
340 };
341
342 operations.push(operation);
343 }
344
345 operations
346}
347
348pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
350 let mut seen = HashMap::new();
351 let mut duplicates = Vec::new();
352
353 for name in extract_all_names(tasks) {
354 let count = seen.entry(name.clone()).or_insert(0);
355 *count += 1;
356 if *count == 2 {
357 duplicates.push(name);
359 }
360 }
361
362 duplicates
363}
364
365use crate::error::{IntentError, Result};
370use sqlx::SqlitePool;
371
372pub struct PlanExecutor<'a> {
374 pool: &'a SqlitePool,
375 project_path: Option<String>,
376 default_parent_id: Option<i64>,
378}
379
380impl<'a> PlanExecutor<'a> {
381 pub fn new(pool: &'a SqlitePool) -> Self {
383 Self {
384 pool,
385 project_path: None,
386 default_parent_id: None,
387 }
388 }
389
390 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
392 Self {
393 pool,
394 project_path: Some(project_path),
395 default_parent_id: None,
396 }
397 }
398
399 pub fn with_default_parent(mut self, parent_id: i64) -> Self {
402 self.default_parent_id = Some(parent_id);
403 self
404 }
405
406 fn get_task_manager(&self) -> crate::tasks::TaskManager<'a> {
408 match &self.project_path {
409 Some(path) => crate::tasks::TaskManager::with_project_path(self.pool, path.clone()),
410 None => crate::tasks::TaskManager::new(self.pool),
411 }
412 }
413
414 #[tracing::instrument(skip(self, request), fields(task_count = request.tasks.len()))]
416 pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
417 let duplicates = find_duplicate_names(&request.tasks);
419 if !duplicates.is_empty() {
420 return Ok(PlanResult::error(format!(
421 "Duplicate task names in request: {:?}",
422 duplicates
423 )));
424 }
425
426 let all_names = extract_all_names(&request.tasks);
428
429 let existing = self.find_tasks_by_names(&all_names).await?;
431
432 let flat_tasks = flatten_task_tree(&request.tasks);
434
435 if let Err(e) = self.validate_dependencies(&flat_tasks) {
437 return Ok(PlanResult::error(e.to_string()));
438 }
439
440 if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
442 return Ok(PlanResult::error(e.to_string()));
443 }
444
445 if let Err(e) = self.validate_batch_single_doing(&flat_tasks) {
447 return Ok(PlanResult::error(e.to_string()));
448 }
449
450 let task_mgr = self.get_task_manager();
452
453 let mut tx = self.pool.begin().await?;
455
456 let mut task_id_map = HashMap::new();
458 let mut created_count = 0;
459 let mut updated_count = 0;
460 let mut newly_created_names: std::collections::HashSet<String> =
461 std::collections::HashSet::new();
462
463 for task in &flat_tasks {
464 if let Some(&existing_id) = existing.get(&task.name) {
465 task_mgr
467 .update_task_in_tx(
468 &mut tx,
469 existing_id,
470 task.spec.as_deref(),
471 task.priority.as_ref().map(|p| p.to_int()),
472 task.status.as_ref().map(|s| s.as_db_str()),
473 task.active_form.as_deref(),
474 )
475 .await?;
476 task_id_map.insert(task.name.clone(), existing_id);
477 updated_count += 1;
478 } else {
479 let id = task_mgr
481 .create_task_in_tx(
482 &mut tx,
483 &task.name,
484 task.spec.as_deref(),
485 task.priority.as_ref().map(|p| p.to_int()),
486 task.status.as_ref().map(|s| s.as_db_str()),
487 task.active_form.as_deref(),
488 "ai", )
490 .await?;
491 task_id_map.insert(task.name.clone(), id);
492 newly_created_names.insert(task.name.clone());
493 created_count += 1;
494 }
495 }
496
497 for task in &flat_tasks {
499 if let Some(parent_name) = &task.parent_name {
500 let task_id = task_id_map.get(&task.name).ok_or_else(|| {
501 IntentError::InvalidInput(format!("Task not found: {}", task.name))
502 })?;
503 let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
504 IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
505 })?;
506 task_mgr
507 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
508 .await?;
509 }
510 }
511
512 for task in &flat_tasks {
515 if task.parent_name.is_some() {
517 continue;
518 }
519
520 if let Some(explicit_parent) = &task.explicit_parent_id {
522 let task_id = task_id_map.get(&task.name).ok_or_else(|| {
523 IntentError::InvalidInput(format!("Task not found: {}", task.name))
524 })?;
525
526 match explicit_parent {
527 None => {
528 task_mgr.clear_parent_in_tx(&mut tx, *task_id).await?;
530 },
531 Some(parent_id) => {
532 task_mgr
535 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
536 .await?;
537 },
538 }
539 }
540 }
541
542 if let Some(default_parent) = self.default_parent_id {
544 for task in &flat_tasks {
545 if newly_created_names.contains(&task.name)
550 && task.parent_name.is_none()
551 && task.explicit_parent_id.is_none()
552 {
553 if let Some(&task_id) = task_id_map.get(&task.name) {
554 task_mgr
555 .set_parent_in_tx(&mut tx, task_id, default_parent)
556 .await?;
557 }
558 }
559 }
560 }
561
562 let dep_count = self
564 .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
565 .await?;
566
567 tx.commit().await?;
569
570 task_mgr.notify_batch_changed().await;
572
573 let doing_task = flat_tasks
576 .iter()
577 .find(|task| matches!(task.status, Some(TaskStatus::Doing)));
578
579 let focused_task_response = if let Some(doing_task) = doing_task {
580 if let Some(&task_id) = task_id_map.get(&doing_task.name) {
582 let response = task_mgr.start_task(task_id, true).await?;
584 Some(response)
585 } else {
586 None
587 }
588 } else {
589 None
590 };
591
592 Ok(PlanResult::success(
594 task_id_map,
595 created_count,
596 updated_count,
597 dep_count,
598 focused_task_response,
599 ))
600 }
601
602 async fn find_tasks_by_names(&self, names: &[String]) -> Result<HashMap<String, i64>> {
604 if names.is_empty() {
605 return Ok(HashMap::new());
606 }
607
608 let mut map = HashMap::new();
609
610 let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
613 let query = format!(
614 "SELECT id, name FROM tasks WHERE name IN ({})",
615 placeholders
616 );
617
618 let mut query_builder = sqlx::query(&query);
619 for name in names {
620 query_builder = query_builder.bind(name);
621 }
622
623 let rows = query_builder.fetch_all(self.pool).await?;
624
625 for row in rows {
626 let id: i64 = row.get("id");
627 let name: String = row.get("name");
628 map.insert(name, id);
629 }
630
631 Ok(map)
632 }
633
634 async fn build_dependencies(
636 &self,
637 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
638 flat_tasks: &[FlatTask],
639 task_id_map: &HashMap<String, i64>,
640 ) -> Result<usize> {
641 let mut count = 0;
642
643 for task in flat_tasks {
644 if !task.depends_on.is_empty() {
645 let blocked_id = task_id_map.get(&task.name).ok_or_else(|| {
646 IntentError::InvalidInput(format!("Task not found: {}", task.name))
647 })?;
648
649 for dep_name in &task.depends_on {
650 let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
651 IntentError::InvalidInput(format!(
652 "Dependency '{}' not found for task '{}'",
653 dep_name, task.name
654 ))
655 })?;
656
657 sqlx::query(
658 "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
659 )
660 .bind(blocking_id)
661 .bind(blocked_id)
662 .execute(&mut **tx)
663 .await?;
664
665 count += 1;
666 }
667 }
668 }
669
670 Ok(count)
671 }
672
673 fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
675 let task_names: std::collections::HashSet<_> =
676 flat_tasks.iter().map(|t| t.name.as_str()).collect();
677
678 for task in flat_tasks {
679 for dep_name in &task.depends_on {
680 if !task_names.contains(dep_name.as_str()) {
681 return Err(IntentError::InvalidInput(format!(
682 "Task '{}' depends on '{}', but '{}' is not in the plan",
683 task.name, dep_name, dep_name
684 )));
685 }
686 }
687 }
688
689 Ok(())
690 }
691
692 fn validate_batch_single_doing(&self, flat_tasks: &[FlatTask]) -> Result<()> {
696 let doing_tasks: Vec<&FlatTask> = flat_tasks
698 .iter()
699 .filter(|task| matches!(task.status, Some(TaskStatus::Doing)))
700 .collect();
701
702 if doing_tasks.len() > 1 {
704 let names: Vec<&str> = doing_tasks.iter().map(|t| t.name.as_str()).collect();
705 return Err(IntentError::InvalidInput(format!(
706 "Batch single doing constraint violated: only one task per batch can have status='doing'. Found: {}",
707 names.join(", ")
708 )));
709 }
710
711 Ok(())
712 }
713
714 fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
716 if flat_tasks.is_empty() {
717 return Ok(());
718 }
719
720 let name_to_idx: HashMap<&str, usize> = flat_tasks
722 .iter()
723 .enumerate()
724 .map(|(i, t)| (t.name.as_str(), i))
725 .collect();
726
727 let mut graph: Vec<Vec<usize>> = vec![Vec::new(); flat_tasks.len()];
729 for (idx, task) in flat_tasks.iter().enumerate() {
730 for dep_name in &task.depends_on {
731 if let Some(&dep_idx) = name_to_idx.get(dep_name.as_str()) {
732 graph[idx].push(dep_idx);
733 }
734 }
735 }
736
737 for task in flat_tasks {
739 if task.depends_on.contains(&task.name) {
740 return Err(IntentError::InvalidInput(format!(
741 "Circular dependency detected: task '{}' depends on itself",
742 task.name
743 )));
744 }
745 }
746
747 let sccs = self.tarjan_scc(&graph);
749
750 for scc in sccs {
752 if scc.len() > 1 {
753 let cycle_names: Vec<&str> = scc
755 .iter()
756 .map(|&idx| flat_tasks[idx].name.as_str())
757 .collect();
758
759 return Err(IntentError::InvalidInput(format!(
760 "Circular dependency detected: {}",
761 cycle_names.join(" → ")
762 )));
763 }
764 }
765
766 Ok(())
767 }
768
769 fn tarjan_scc(&self, graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
772 let n = graph.len();
773 let mut index = 0;
774 let mut stack = Vec::new();
775 let mut indices = vec![None; n];
776 let mut lowlinks = vec![0; n];
777 let mut on_stack = vec![false; n];
778 let mut sccs = Vec::new();
779
780 #[allow(clippy::too_many_arguments)]
781 fn strongconnect(
782 v: usize,
783 graph: &[Vec<usize>],
784 index: &mut usize,
785 stack: &mut Vec<usize>,
786 indices: &mut [Option<usize>],
787 lowlinks: &mut [usize],
788 on_stack: &mut [bool],
789 sccs: &mut Vec<Vec<usize>>,
790 ) {
791 indices[v] = Some(*index);
793 lowlinks[v] = *index;
794 *index += 1;
795 stack.push(v);
796 on_stack[v] = true;
797
798 for &w in &graph[v] {
800 if indices[w].is_none() {
801 strongconnect(w, graph, index, stack, indices, lowlinks, on_stack, sccs);
803 lowlinks[v] = lowlinks[v].min(lowlinks[w]);
804 } else if on_stack[w] {
805 lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
807 }
808 }
809
810 if lowlinks[v] == indices[v].unwrap() {
812 let mut scc = Vec::new();
813 loop {
814 let w = stack.pop().unwrap();
815 on_stack[w] = false;
816 scc.push(w);
817 if w == v {
818 break;
819 }
820 }
821 sccs.push(scc);
822 }
823 }
824
825 for v in 0..n {
827 if indices[v].is_none() {
828 strongconnect(
829 v,
830 graph,
831 &mut index,
832 &mut stack,
833 &mut indices,
834 &mut lowlinks,
835 &mut on_stack,
836 &mut sccs,
837 );
838 }
839 }
840
841 sccs
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848
849 #[test]
850 fn test_priority_value_to_int() {
851 assert_eq!(PriorityValue::Critical.to_int(), 1);
852 assert_eq!(PriorityValue::High.to_int(), 2);
853 assert_eq!(PriorityValue::Medium.to_int(), 3);
854 assert_eq!(PriorityValue::Low.to_int(), 4);
855 }
856
857 #[test]
858 fn test_priority_value_from_int() {
859 assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
860 assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
861 assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
862 assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
863 assert_eq!(PriorityValue::from_int(999), None);
864 }
865
866 #[test]
867 fn test_priority_value_as_str() {
868 assert_eq!(PriorityValue::Critical.as_str(), "critical");
869 assert_eq!(PriorityValue::High.as_str(), "high");
870 assert_eq!(PriorityValue::Medium.as_str(), "medium");
871 assert_eq!(PriorityValue::Low.as_str(), "low");
872 }
873
874 #[test]
875 fn test_plan_request_deserialization_minimal() {
876 let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
877 let request: PlanRequest = serde_json::from_str(json).unwrap();
878
879 assert_eq!(request.tasks.len(), 1);
880 assert_eq!(request.tasks[0].name, "Test Task");
881 assert_eq!(request.tasks[0].spec, None);
882 assert_eq!(request.tasks[0].priority, None);
883 assert_eq!(request.tasks[0].children, None);
884 assert_eq!(request.tasks[0].depends_on, None);
885 assert_eq!(request.tasks[0].task_id, None);
886 }
887
888 #[test]
889 fn test_plan_request_deserialization_full() {
890 let json = r#"{
891 "tasks": [{
892 "name": "Parent Task",
893 "spec": "Parent spec",
894 "priority": "high",
895 "children": [{
896 "name": "Child Task",
897 "spec": "Child spec"
898 }],
899 "depends_on": ["Other Task"],
900 "task_id": 42
901 }]
902 }"#;
903
904 let request: PlanRequest = serde_json::from_str(json).unwrap();
905
906 assert_eq!(request.tasks.len(), 1);
907 let parent = &request.tasks[0];
908 assert_eq!(parent.name, "Parent Task");
909 assert_eq!(parent.spec, Some("Parent spec".to_string()));
910 assert_eq!(parent.priority, Some(PriorityValue::High));
911 assert_eq!(parent.task_id, Some(42));
912
913 let children = parent.children.as_ref().unwrap();
914 assert_eq!(children.len(), 1);
915 assert_eq!(children[0].name, "Child Task");
916
917 let depends = parent.depends_on.as_ref().unwrap();
918 assert_eq!(depends.len(), 1);
919 assert_eq!(depends[0], "Other Task");
920 }
921
922 #[test]
923 fn test_plan_request_serialization() {
924 let request = PlanRequest {
925 tasks: vec![TaskTree {
926 name: "Test Task".to_string(),
927 spec: Some("Test spec".to_string()),
928 priority: Some(PriorityValue::Medium),
929 children: None,
930 depends_on: None,
931 task_id: None,
932 status: None,
933 active_form: None,
934 parent_id: None,
935 }],
936 };
937
938 let json = serde_json::to_string(&request).unwrap();
939 assert!(json.contains("\"name\":\"Test Task\""));
940 assert!(json.contains("\"spec\":\"Test spec\""));
941 assert!(json.contains("\"priority\":\"medium\""));
942 }
943
944 #[test]
945 fn test_plan_result_success() {
946 let mut map = HashMap::new();
947 map.insert("Task 1".to_string(), 1);
948 map.insert("Task 2".to_string(), 2);
949
950 let result = PlanResult::success(map.clone(), 2, 0, 1, None);
951
952 assert!(result.success);
953 assert_eq!(result.task_id_map, map);
954 assert_eq!(result.created_count, 2);
955 assert_eq!(result.updated_count, 0);
956 assert_eq!(result.dependency_count, 1);
957 assert_eq!(result.focused_task, None);
958 assert_eq!(result.error, None);
959 }
960
961 #[test]
962 fn test_plan_result_error() {
963 let result = PlanResult::error("Test error");
964
965 assert!(!result.success);
966 assert_eq!(result.task_id_map.len(), 0);
967 assert_eq!(result.created_count, 0);
968 assert_eq!(result.updated_count, 0);
969 assert_eq!(result.dependency_count, 0);
970 assert_eq!(result.error, Some("Test error".to_string()));
971 }
972
973 #[test]
974 fn test_task_tree_nested() {
975 let tree = TaskTree {
976 name: "Parent".to_string(),
977 spec: None,
978 priority: None,
979 children: Some(vec![
980 TaskTree {
981 name: "Child 1".to_string(),
982 spec: None,
983 priority: None,
984 children: None,
985 depends_on: None,
986 task_id: None,
987 status: None,
988 active_form: None,
989 parent_id: None,
990 },
991 TaskTree {
992 name: "Child 2".to_string(),
993 spec: None,
994 priority: Some(PriorityValue::High),
995 children: None,
996 depends_on: None,
997 task_id: None,
998 status: None,
999 active_form: None,
1000 parent_id: None,
1001 },
1002 ]),
1003 depends_on: None,
1004 task_id: None,
1005 status: None,
1006 active_form: None,
1007 parent_id: None,
1008 };
1009
1010 let json = serde_json::to_string_pretty(&tree).unwrap();
1011 let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
1012
1013 assert_eq!(tree, deserialized);
1014 assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
1015 }
1016
1017 #[test]
1018 fn test_priority_value_case_insensitive_deserialization() {
1019 let json = r#"{"name": "Test", "priority": "high"}"#;
1021 let task: TaskTree = serde_json::from_str(json).unwrap();
1022 assert_eq!(task.priority, Some(PriorityValue::High));
1023
1024 }
1027
1028 #[test]
1029 fn test_extract_all_names_simple() {
1030 let tasks = vec![
1031 TaskTree {
1032 name: "Task 1".to_string(),
1033 spec: None,
1034 priority: None,
1035 children: None,
1036 depends_on: None,
1037 task_id: None,
1038 status: None,
1039 active_form: None,
1040 parent_id: None,
1041 },
1042 TaskTree {
1043 name: "Task 2".to_string(),
1044 spec: None,
1045 priority: None,
1046 children: None,
1047 depends_on: None,
1048 task_id: None,
1049 status: None,
1050 active_form: None,
1051 parent_id: None,
1052 },
1053 ];
1054
1055 let names = extract_all_names(&tasks);
1056 assert_eq!(names, vec!["Task 1", "Task 2"]);
1057 }
1058
1059 #[test]
1060 fn test_extract_all_names_nested() {
1061 let tasks = vec![TaskTree {
1062 name: "Parent".to_string(),
1063 spec: None,
1064 priority: None,
1065 children: Some(vec![
1066 TaskTree {
1067 name: "Child 1".to_string(),
1068 spec: None,
1069 priority: None,
1070 children: None,
1071 depends_on: None,
1072 task_id: None,
1073 status: None,
1074 active_form: None,
1075 parent_id: None,
1076 },
1077 TaskTree {
1078 name: "Child 2".to_string(),
1079 spec: None,
1080 priority: None,
1081 children: Some(vec![TaskTree {
1082 name: "Grandchild".to_string(),
1083 spec: None,
1084 priority: None,
1085 children: None,
1086 depends_on: None,
1087 task_id: None,
1088 status: None,
1089 active_form: None,
1090 parent_id: None,
1091 }]),
1092 depends_on: None,
1093 task_id: None,
1094 status: None,
1095 active_form: None,
1096 parent_id: None,
1097 },
1098 ]),
1099 depends_on: None,
1100 task_id: None,
1101 status: None,
1102 active_form: None,
1103 parent_id: None,
1104 }];
1105
1106 let names = extract_all_names(&tasks);
1107 assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
1108 }
1109
1110 #[test]
1111 fn test_flatten_task_tree_simple() {
1112 let tasks = vec![TaskTree {
1113 name: "Task 1".to_string(),
1114 spec: Some("Spec 1".to_string()),
1115 priority: Some(PriorityValue::High),
1116 children: None,
1117 depends_on: Some(vec!["Task 0".to_string()]),
1118 task_id: None,
1119 status: None,
1120 active_form: None,
1121 parent_id: None,
1122 }];
1123
1124 let flat = flatten_task_tree(&tasks);
1125 assert_eq!(flat.len(), 1);
1126 assert_eq!(flat[0].name, "Task 1");
1127 assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
1128 assert_eq!(flat[0].priority, Some(PriorityValue::High));
1129 assert_eq!(flat[0].parent_name, None);
1130 assert_eq!(flat[0].depends_on, vec!["Task 0"]);
1131 }
1132
1133 #[test]
1134 fn test_flatten_task_tree_nested() {
1135 let tasks = vec![TaskTree {
1136 name: "Parent".to_string(),
1137 spec: None,
1138 priority: None,
1139 children: Some(vec![
1140 TaskTree {
1141 name: "Child 1".to_string(),
1142 spec: None,
1143 priority: None,
1144 children: None,
1145 depends_on: None,
1146 task_id: None,
1147 status: None,
1148 active_form: None,
1149 parent_id: None,
1150 },
1151 TaskTree {
1152 name: "Child 2".to_string(),
1153 spec: None,
1154 priority: None,
1155 children: None,
1156 depends_on: None,
1157 task_id: None,
1158 status: None,
1159 active_form: None,
1160 parent_id: None,
1161 },
1162 ]),
1163 depends_on: None,
1164 task_id: None,
1165 status: None,
1166 active_form: None,
1167 parent_id: None,
1168 }];
1169
1170 let flat = flatten_task_tree(&tasks);
1171 assert_eq!(flat.len(), 3);
1172
1173 assert_eq!(flat[0].name, "Parent");
1175 assert_eq!(flat[0].parent_name, None);
1176
1177 assert_eq!(flat[1].name, "Child 1");
1179 assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
1180
1181 assert_eq!(flat[2].name, "Child 2");
1182 assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
1183 }
1184
1185 #[test]
1186 fn test_classify_operations_all_create() {
1187 let flat_tasks = vec![
1188 FlatTask {
1189 name: "Task 1".to_string(),
1190 spec: None,
1191 priority: None,
1192 parent_name: None,
1193 depends_on: vec![],
1194 task_id: None,
1195 status: None,
1196 active_form: None,
1197 explicit_parent_id: None,
1198 },
1199 FlatTask {
1200 name: "Task 2".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 explicit_parent_id: None,
1209 },
1210 ];
1211
1212 let existing = HashMap::new();
1213 let operations = classify_operations(&flat_tasks, &existing);
1214
1215 assert_eq!(operations.len(), 2);
1216 assert!(matches!(operations[0], Operation::Create(_)));
1217 assert!(matches!(operations[1], Operation::Create(_)));
1218 }
1219
1220 #[test]
1221 fn test_classify_operations_all_update() {
1222 let flat_tasks = vec![
1223 FlatTask {
1224 name: "Task 1".to_string(),
1225 spec: None,
1226 priority: None,
1227 parent_name: None,
1228 depends_on: vec![],
1229 task_id: None,
1230 status: None,
1231 active_form: None,
1232 explicit_parent_id: None,
1233 },
1234 FlatTask {
1235 name: "Task 2".to_string(),
1236 spec: None,
1237 priority: None,
1238 parent_name: None,
1239 depends_on: vec![],
1240 task_id: None,
1241 status: None,
1242 active_form: None,
1243 explicit_parent_id: None,
1244 },
1245 ];
1246
1247 let mut existing = HashMap::new();
1248 existing.insert("Task 1".to_string(), 1);
1249 existing.insert("Task 2".to_string(), 2);
1250
1251 let operations = classify_operations(&flat_tasks, &existing);
1252
1253 assert_eq!(operations.len(), 2);
1254 assert!(matches!(
1255 operations[0],
1256 Operation::Update { task_id: 1, .. }
1257 ));
1258 assert!(matches!(
1259 operations[1],
1260 Operation::Update { task_id: 2, .. }
1261 ));
1262 }
1263
1264 #[test]
1265 fn test_classify_operations_mixed() {
1266 let flat_tasks = vec![
1267 FlatTask {
1268 name: "Existing Task".to_string(),
1269 spec: None,
1270 priority: None,
1271 parent_name: None,
1272 depends_on: vec![],
1273 task_id: None,
1274 status: None,
1275 active_form: None,
1276 explicit_parent_id: None,
1277 },
1278 FlatTask {
1279 name: "New Task".to_string(),
1280 spec: None,
1281 priority: None,
1282 parent_name: None,
1283 depends_on: vec![],
1284 task_id: None,
1285 status: None,
1286 active_form: None,
1287 explicit_parent_id: None,
1288 },
1289 ];
1290
1291 let mut existing = HashMap::new();
1292 existing.insert("Existing Task".to_string(), 42);
1293
1294 let operations = classify_operations(&flat_tasks, &existing);
1295
1296 assert_eq!(operations.len(), 2);
1297 assert!(matches!(
1298 operations[0],
1299 Operation::Update { task_id: 42, .. }
1300 ));
1301 assert!(matches!(operations[1], Operation::Create(_)));
1302 }
1303
1304 #[test]
1305 fn test_classify_operations_explicit_task_id() {
1306 let flat_tasks = vec![FlatTask {
1307 name: "Task".to_string(),
1308 spec: None,
1309 priority: None,
1310 parent_name: None,
1311 depends_on: vec![],
1312 task_id: Some(99), status: None,
1314 active_form: None,
1315 explicit_parent_id: None,
1316 }];
1317
1318 let existing = HashMap::new(); let operations = classify_operations(&flat_tasks, &existing);
1321
1322 assert_eq!(operations.len(), 1);
1324 assert!(matches!(
1325 operations[0],
1326 Operation::Update { task_id: 99, .. }
1327 ));
1328 }
1329
1330 #[test]
1331 fn test_find_duplicate_names_no_duplicates() {
1332 let tasks = vec![
1333 TaskTree {
1334 name: "Task 1".to_string(),
1335 spec: None,
1336 priority: None,
1337 children: None,
1338 depends_on: None,
1339 task_id: None,
1340 status: None,
1341 active_form: None,
1342 parent_id: None,
1343 },
1344 TaskTree {
1345 name: "Task 2".to_string(),
1346 spec: None,
1347 priority: None,
1348 children: None,
1349 depends_on: None,
1350 task_id: None,
1351 status: None,
1352 active_form: None,
1353 parent_id: None,
1354 },
1355 ];
1356
1357 let duplicates = find_duplicate_names(&tasks);
1358 assert_eq!(duplicates.len(), 0);
1359 }
1360
1361 #[test]
1362 fn test_find_duplicate_names_with_duplicates() {
1363 let tasks = vec![
1364 TaskTree {
1365 name: "Duplicate".to_string(),
1366 spec: None,
1367 priority: None,
1368 children: None,
1369 depends_on: None,
1370 task_id: None,
1371 status: None,
1372 active_form: None,
1373 parent_id: None,
1374 },
1375 TaskTree {
1376 name: "Unique".to_string(),
1377 spec: None,
1378 priority: None,
1379 children: None,
1380 depends_on: None,
1381 task_id: None,
1382 status: None,
1383 active_form: None,
1384 parent_id: None,
1385 },
1386 TaskTree {
1387 name: "Duplicate".to_string(),
1388 spec: None,
1389 priority: None,
1390 children: None,
1391 depends_on: None,
1392 task_id: None,
1393 status: None,
1394 active_form: None,
1395 parent_id: None,
1396 },
1397 ];
1398
1399 let duplicates = find_duplicate_names(&tasks);
1400 assert_eq!(duplicates.len(), 1);
1401 assert_eq!(duplicates[0], "Duplicate");
1402 }
1403
1404 #[test]
1405 fn test_find_duplicate_names_nested() {
1406 let tasks = vec![TaskTree {
1407 name: "Parent".to_string(),
1408 spec: None,
1409 priority: None,
1410 children: Some(vec![TaskTree {
1411 name: "Parent".to_string(), spec: None,
1413 priority: None,
1414 children: None,
1415 depends_on: None,
1416 task_id: None,
1417 status: None,
1418 active_form: None,
1419 parent_id: None,
1420 }]),
1421 depends_on: None,
1422 task_id: None,
1423 status: None,
1424 active_form: None,
1425 parent_id: None,
1426 }];
1427
1428 let duplicates = find_duplicate_names(&tasks);
1429 assert_eq!(duplicates.len(), 1);
1430 assert_eq!(duplicates[0], "Parent");
1431 }
1432
1433 #[test]
1434 fn test_flatten_task_tree_empty() {
1435 let tasks: Vec<TaskTree> = vec![];
1436 let flat = flatten_task_tree(&tasks);
1437 assert_eq!(flat.len(), 0);
1438 }
1439
1440 #[test]
1441 fn test_flatten_task_tree_deep_nesting() {
1442 let tasks = vec![TaskTree {
1444 name: "Root".to_string(),
1445 spec: None,
1446 priority: None,
1447 children: Some(vec![TaskTree {
1448 name: "Level1".to_string(),
1449 spec: None,
1450 priority: None,
1451 children: Some(vec![TaskTree {
1452 name: "Level2".to_string(),
1453 spec: None,
1454 priority: None,
1455 children: Some(vec![TaskTree {
1456 name: "Level3".to_string(),
1457 spec: None,
1458 priority: None,
1459 children: None,
1460 depends_on: None,
1461 task_id: None,
1462 status: None,
1463 active_form: None,
1464 parent_id: None,
1465 }]),
1466 depends_on: None,
1467 task_id: None,
1468 status: None,
1469 active_form: None,
1470 parent_id: None,
1471 }]),
1472 depends_on: None,
1473 task_id: None,
1474 status: None,
1475 active_form: None,
1476 parent_id: None,
1477 }]),
1478 depends_on: None,
1479 task_id: None,
1480 status: None,
1481 active_form: None,
1482 parent_id: None,
1483 }];
1484
1485 let flat = flatten_task_tree(&tasks);
1486 assert_eq!(flat.len(), 4);
1487
1488 assert_eq!(flat[0].name, "Root");
1490 assert_eq!(flat[0].parent_name, None);
1491
1492 assert_eq!(flat[1].name, "Level1");
1493 assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1494
1495 assert_eq!(flat[2].name, "Level2");
1496 assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1497
1498 assert_eq!(flat[3].name, "Level3");
1499 assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1500 }
1501
1502 #[test]
1503 fn test_flatten_task_tree_many_siblings() {
1504 let children: Vec<TaskTree> = (0..10)
1505 .map(|i| TaskTree {
1506 name: format!("Child {}", i),
1507 spec: None,
1508 priority: None,
1509 children: None,
1510 depends_on: None,
1511 task_id: None,
1512 status: None,
1513 active_form: None,
1514 parent_id: None,
1515 })
1516 .collect();
1517
1518 let tasks = vec![TaskTree {
1519 name: "Parent".to_string(),
1520 spec: None,
1521 priority: None,
1522 children: Some(children),
1523 depends_on: None,
1524 task_id: None,
1525 status: None,
1526 active_form: None,
1527 parent_id: None,
1528 }];
1529
1530 let flat = flatten_task_tree(&tasks);
1531 assert_eq!(flat.len(), 11); for child in flat.iter().skip(1).take(10) {
1535 assert_eq!(child.parent_name, Some("Parent".to_string()));
1536 }
1537 }
1538
1539 #[test]
1540 fn test_flatten_task_tree_complex_mixed() {
1541 let tasks = vec![
1543 TaskTree {
1544 name: "Task 1".to_string(),
1545 spec: None,
1546 priority: None,
1547 children: Some(vec![
1548 TaskTree {
1549 name: "Task 1.1".to_string(),
1550 spec: None,
1551 priority: None,
1552 children: None,
1553 depends_on: None,
1554 task_id: None,
1555 status: None,
1556 active_form: None,
1557 parent_id: None,
1558 },
1559 TaskTree {
1560 name: "Task 1.2".to_string(),
1561 spec: None,
1562 priority: None,
1563 children: Some(vec![TaskTree {
1564 name: "Task 1.2.1".to_string(),
1565 spec: None,
1566 priority: None,
1567 children: None,
1568 depends_on: None,
1569 task_id: None,
1570 status: None,
1571 active_form: None,
1572 parent_id: None,
1573 }]),
1574 depends_on: None,
1575 task_id: None,
1576 status: None,
1577 active_form: None,
1578 parent_id: None,
1579 },
1580 ]),
1581 depends_on: None,
1582 task_id: None,
1583 status: None,
1584 active_form: None,
1585 parent_id: None,
1586 },
1587 TaskTree {
1588 name: "Task 2".to_string(),
1589 spec: None,
1590 priority: None,
1591 children: None,
1592 depends_on: Some(vec!["Task 1".to_string()]),
1593 task_id: None,
1594 status: None,
1595 active_form: None,
1596 parent_id: None,
1597 },
1598 ];
1599
1600 let flat = flatten_task_tree(&tasks);
1601 assert_eq!(flat.len(), 5);
1602
1603 assert_eq!(flat[0].name, "Task 1");
1605 assert_eq!(flat[0].parent_name, None);
1606
1607 assert_eq!(flat[1].name, "Task 1.1");
1608 assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
1609
1610 assert_eq!(flat[2].name, "Task 1.2");
1611 assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
1612
1613 assert_eq!(flat[3].name, "Task 1.2.1");
1614 assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
1615
1616 assert_eq!(flat[4].name, "Task 2");
1617 assert_eq!(flat[4].parent_name, None);
1618 assert_eq!(flat[4].depends_on, vec!["Task 1"]);
1619 }
1620
1621 #[tokio::test]
1622 async fn test_plan_executor_integration() {
1623 use crate::test_utils::test_helpers::TestContext;
1624
1625 let ctx = TestContext::new().await;
1626
1627 let request = PlanRequest {
1629 tasks: vec![TaskTree {
1630 name: "Integration Test Plan".to_string(),
1631 spec: Some("Test plan execution end-to-end".to_string()),
1632 priority: Some(PriorityValue::High),
1633 children: Some(vec![
1634 TaskTree {
1635 name: "Subtask A".to_string(),
1636 spec: Some("First subtask".to_string()),
1637 priority: None,
1638 children: None,
1639 depends_on: None,
1640 task_id: None,
1641 status: None,
1642 active_form: None,
1643 parent_id: None,
1644 },
1645 TaskTree {
1646 name: "Subtask B".to_string(),
1647 spec: Some("Second subtask depends on A".to_string()),
1648 priority: None,
1649 children: None,
1650 depends_on: Some(vec!["Subtask A".to_string()]),
1651 task_id: None,
1652 status: None,
1653 active_form: None,
1654 parent_id: None,
1655 },
1656 ]),
1657 depends_on: None,
1658 task_id: None,
1659 status: None,
1660 active_form: None,
1661 parent_id: None,
1662 }],
1663 };
1664
1665 let executor = PlanExecutor::new(&ctx.pool);
1667 let result = executor.execute(&request).await.unwrap();
1668
1669 assert!(result.success, "Plan execution should succeed");
1671 assert_eq!(result.created_count, 3, "Should create 3 tasks");
1672 assert_eq!(result.updated_count, 0, "Should not update any tasks");
1673 assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
1674 assert!(result.error.is_none(), "Should have no error");
1675
1676 assert_eq!(result.task_id_map.len(), 3);
1678 assert!(result.task_id_map.contains_key("Integration Test Plan"));
1679 assert!(result.task_id_map.contains_key("Subtask A"));
1680 assert!(result.task_id_map.contains_key("Subtask B"));
1681
1682 let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
1684 let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
1685 let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
1686
1687 let parent: (String, String, i64, Option<i64>) =
1689 sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
1690 .bind(parent_id)
1691 .fetch_one(&ctx.pool)
1692 .await
1693 .unwrap();
1694
1695 assert_eq!(parent.0, "Integration Test Plan");
1696 assert_eq!(parent.1, "Test plan execution end-to-end");
1697 assert_eq!(parent.2, 2); assert_eq!(parent.3, None); let subtask_a: (String, Option<i64>) =
1702 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
1703 .bind(subtask_a_id)
1704 .fetch_one(&ctx.pool)
1705 .await
1706 .unwrap();
1707
1708 assert_eq!(subtask_a.0, "Subtask A");
1709 assert_eq!(subtask_a.1, Some(parent_id)); let dep: (i64, i64) = sqlx::query_as(
1713 "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
1714 )
1715 .bind(subtask_b_id)
1716 .fetch_one(&ctx.pool)
1717 .await
1718 .unwrap();
1719
1720 assert_eq!(dep.0, subtask_a_id); assert_eq!(dep.1, subtask_b_id); }
1723
1724 #[tokio::test]
1725 async fn test_plan_executor_idempotency() {
1726 use crate::test_utils::test_helpers::TestContext;
1727
1728 let ctx = TestContext::new().await;
1729
1730 let request = PlanRequest {
1732 tasks: vec![TaskTree {
1733 name: "Idempotent Task".to_string(),
1734 spec: Some("Initial spec".to_string()),
1735 priority: Some(PriorityValue::High),
1736 children: Some(vec![
1737 TaskTree {
1738 name: "Child 1".to_string(),
1739 spec: Some("Child spec 1".to_string()),
1740 priority: None,
1741 children: None,
1742 depends_on: None,
1743 task_id: None,
1744 status: None,
1745 active_form: None,
1746 parent_id: None,
1747 },
1748 TaskTree {
1749 name: "Child 2".to_string(),
1750 spec: Some("Child spec 2".to_string()),
1751 priority: Some(PriorityValue::Low),
1752 children: None,
1753 depends_on: None,
1754 task_id: None,
1755 status: None,
1756 active_form: None,
1757 parent_id: None,
1758 },
1759 ]),
1760 depends_on: None,
1761 task_id: None,
1762 status: None,
1763 active_form: None,
1764 parent_id: None,
1765 }],
1766 };
1767
1768 let executor = PlanExecutor::new(&ctx.pool);
1769
1770 let result1 = executor.execute(&request).await.unwrap();
1772 assert!(result1.success, "First execution should succeed");
1773 assert_eq!(result1.created_count, 3, "Should create 3 tasks");
1774 assert_eq!(result1.updated_count, 0, "Should not update any tasks");
1775 assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
1776
1777 let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
1779 let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
1780 let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
1781
1782 let result2 = executor.execute(&request).await.unwrap();
1784 assert!(result2.success, "Second execution should succeed");
1785 assert_eq!(result2.created_count, 0, "Should not create any new tasks");
1786 assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
1787 assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
1788
1789 let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
1791 let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
1792 let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
1793
1794 assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
1795 assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
1796 assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
1797
1798 let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1800 .bind(parent_id_2)
1801 .fetch_one(&ctx.pool)
1802 .await
1803 .unwrap();
1804
1805 assert_eq!(parent.0, "Initial spec");
1806 assert_eq!(parent.1, 2); let modified_request = PlanRequest {
1810 tasks: vec![TaskTree {
1811 name: "Idempotent Task".to_string(),
1812 spec: Some("Updated spec".to_string()), priority: Some(PriorityValue::Critical), children: Some(vec![
1815 TaskTree {
1816 name: "Child 1".to_string(),
1817 spec: Some("Updated child spec 1".to_string()), priority: None,
1819 children: None,
1820 depends_on: None,
1821 task_id: None,
1822 status: None,
1823 active_form: None,
1824 parent_id: None,
1825 },
1826 TaskTree {
1827 name: "Child 2".to_string(),
1828 spec: Some("Child spec 2".to_string()), priority: Some(PriorityValue::Low),
1830 children: None,
1831 depends_on: None,
1832 task_id: None,
1833 status: None,
1834 active_form: None,
1835 parent_id: None,
1836 },
1837 ]),
1838 depends_on: None,
1839 task_id: None,
1840 status: None,
1841 active_form: None,
1842 parent_id: None,
1843 }],
1844 };
1845
1846 let result3 = executor.execute(&modified_request).await.unwrap();
1847 assert!(result3.success, "Third execution should succeed");
1848 assert_eq!(result3.created_count, 0, "Should not create any new tasks");
1849 assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
1850
1851 let updated_parent: (String, i64) =
1853 sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1854 .bind(parent_id_2)
1855 .fetch_one(&ctx.pool)
1856 .await
1857 .unwrap();
1858
1859 assert_eq!(updated_parent.0, "Updated spec");
1860 assert_eq!(updated_parent.1, 1); let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
1863 .bind(child1_id_2)
1864 .fetch_one(&ctx.pool)
1865 .await
1866 .unwrap();
1867
1868 assert_eq!(updated_child1.0, "Updated child spec 1");
1869 }
1870
1871 #[tokio::test]
1872 async fn test_plan_executor_dependencies() {
1873 use crate::test_utils::test_helpers::TestContext;
1874
1875 let ctx = TestContext::new().await;
1876
1877 let request = PlanRequest {
1879 tasks: vec![
1880 TaskTree {
1881 name: "Foundation".to_string(),
1882 spec: Some("Base layer".to_string()),
1883 priority: Some(PriorityValue::Critical),
1884 children: None,
1885 depends_on: None,
1886 task_id: None,
1887 status: None,
1888 active_form: None,
1889 parent_id: None,
1890 },
1891 TaskTree {
1892 name: "Layer 1".to_string(),
1893 spec: Some("Depends on Foundation".to_string()),
1894 priority: Some(PriorityValue::High),
1895 children: None,
1896 depends_on: Some(vec!["Foundation".to_string()]),
1897 task_id: None,
1898 status: None,
1899 active_form: None,
1900 parent_id: None,
1901 },
1902 TaskTree {
1903 name: "Layer 2".to_string(),
1904 spec: Some("Depends on Layer 1".to_string()),
1905 priority: None,
1906 children: None,
1907 depends_on: Some(vec!["Layer 1".to_string()]),
1908 task_id: None,
1909 status: None,
1910 active_form: None,
1911 parent_id: None,
1912 },
1913 TaskTree {
1914 name: "Integration".to_string(),
1915 spec: Some("Depends on both Foundation and Layer 2".to_string()),
1916 priority: None,
1917 children: None,
1918 depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
1919 task_id: None,
1920 status: None,
1921 active_form: None,
1922 parent_id: None,
1923 },
1924 ],
1925 };
1926
1927 let executor = PlanExecutor::new(&ctx.pool);
1928 let result = executor.execute(&request).await.unwrap();
1929
1930 assert!(result.success, "Plan execution should succeed");
1931 assert_eq!(result.created_count, 4, "Should create 4 tasks");
1932 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
1933
1934 let foundation_id = *result.task_id_map.get("Foundation").unwrap();
1936 let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
1937 let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
1938 let integration_id = *result.task_id_map.get("Integration").unwrap();
1939
1940 let deps1: Vec<(i64,)> =
1942 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1943 .bind(layer1_id)
1944 .fetch_all(&ctx.pool)
1945 .await
1946 .unwrap();
1947
1948 assert_eq!(deps1.len(), 1);
1949 assert_eq!(deps1[0].0, foundation_id);
1950
1951 let deps2: Vec<(i64,)> =
1953 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1954 .bind(layer2_id)
1955 .fetch_all(&ctx.pool)
1956 .await
1957 .unwrap();
1958
1959 assert_eq!(deps2.len(), 1);
1960 assert_eq!(deps2[0].0, layer1_id);
1961
1962 let deps3: Vec<(i64,)> =
1964 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
1965 .bind(integration_id)
1966 .fetch_all(&ctx.pool)
1967 .await
1968 .unwrap();
1969
1970 assert_eq!(deps3.len(), 2);
1971 let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
1972 blocking_ids.sort();
1973
1974 let mut expected_ids = vec![foundation_id, layer2_id];
1975 expected_ids.sort();
1976
1977 assert_eq!(blocking_ids, expected_ids);
1978 }
1979
1980 #[tokio::test]
1981 async fn test_plan_executor_invalid_dependency() {
1982 use crate::test_utils::test_helpers::TestContext;
1983
1984 let ctx = TestContext::new().await;
1985
1986 let request = PlanRequest {
1988 tasks: vec![TaskTree {
1989 name: "Task A".to_string(),
1990 spec: Some("Depends on non-existent task".to_string()),
1991 priority: None,
1992 children: None,
1993 depends_on: Some(vec!["NonExistent".to_string()]),
1994 task_id: None,
1995 status: None,
1996 active_form: None,
1997 parent_id: None,
1998 }],
1999 };
2000
2001 let executor = PlanExecutor::new(&ctx.pool);
2002 let result = executor.execute(&request).await.unwrap();
2003
2004 assert!(!result.success, "Plan execution should fail");
2005 assert!(result.error.is_some(), "Should have error message");
2006 let error = result.error.unwrap();
2007 assert!(
2008 error.contains("NonExistent"),
2009 "Error should mention the missing dependency: {}",
2010 error
2011 );
2012 }
2013
2014 #[tokio::test]
2015 async fn test_plan_executor_simple_cycle() {
2016 use crate::test_utils::test_helpers::TestContext;
2017
2018 let ctx = TestContext::new().await;
2019
2020 let request = PlanRequest {
2022 tasks: vec![
2023 TaskTree {
2024 name: "Task A".to_string(),
2025 spec: Some("Depends on B".to_string()),
2026 priority: None,
2027 children: None,
2028 depends_on: Some(vec!["Task B".to_string()]),
2029 task_id: None,
2030 status: None,
2031 active_form: None,
2032 parent_id: None,
2033 },
2034 TaskTree {
2035 name: "Task B".to_string(),
2036 spec: Some("Depends on A".to_string()),
2037 priority: None,
2038 children: None,
2039 depends_on: Some(vec!["Task A".to_string()]),
2040 task_id: None,
2041 status: None,
2042 active_form: None,
2043 parent_id: None,
2044 },
2045 ],
2046 };
2047
2048 let executor = PlanExecutor::new(&ctx.pool);
2049 let result = executor.execute(&request).await.unwrap();
2050
2051 assert!(!result.success, "Plan execution should fail");
2052 assert!(result.error.is_some(), "Should have error message");
2053 let error = result.error.unwrap();
2054 assert!(
2055 error.contains("Circular dependency"),
2056 "Error should mention circular dependency: {}",
2057 error
2058 );
2059 assert!(
2060 error.contains("Task A") && error.contains("Task B"),
2061 "Error should mention both tasks in the cycle: {}",
2062 error
2063 );
2064 }
2065
2066 #[tokio::test]
2067 async fn test_plan_executor_complex_cycle() {
2068 use crate::test_utils::test_helpers::TestContext;
2069
2070 let ctx = TestContext::new().await;
2071
2072 let request = PlanRequest {
2074 tasks: vec![
2075 TaskTree {
2076 name: "Task A".to_string(),
2077 spec: Some("Depends on B".to_string()),
2078 priority: None,
2079 children: None,
2080 depends_on: Some(vec!["Task B".to_string()]),
2081 task_id: None,
2082 status: None,
2083 active_form: None,
2084 parent_id: None,
2085 },
2086 TaskTree {
2087 name: "Task B".to_string(),
2088 spec: Some("Depends on C".to_string()),
2089 priority: None,
2090 children: None,
2091 depends_on: Some(vec!["Task C".to_string()]),
2092 task_id: None,
2093 status: None,
2094 active_form: None,
2095 parent_id: None,
2096 },
2097 TaskTree {
2098 name: "Task C".to_string(),
2099 spec: Some("Depends on A".to_string()),
2100 priority: None,
2101 children: None,
2102 depends_on: Some(vec!["Task A".to_string()]),
2103 task_id: None,
2104 status: None,
2105 active_form: None,
2106 parent_id: None,
2107 },
2108 ],
2109 };
2110
2111 let executor = PlanExecutor::new(&ctx.pool);
2112 let result = executor.execute(&request).await.unwrap();
2113
2114 assert!(!result.success, "Plan execution should fail");
2115 assert!(result.error.is_some(), "Should have error message");
2116 let error = result.error.unwrap();
2117 assert!(
2118 error.contains("Circular dependency"),
2119 "Error should mention circular dependency: {}",
2120 error
2121 );
2122 assert!(
2123 error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
2124 "Error should mention all tasks in the cycle: {}",
2125 error
2126 );
2127 }
2128
2129 #[tokio::test]
2130 async fn test_plan_executor_valid_dag() {
2131 use crate::test_utils::test_helpers::TestContext;
2132
2133 let ctx = TestContext::new().await;
2134
2135 let request = PlanRequest {
2142 tasks: vec![
2143 TaskTree {
2144 name: "Task A".to_string(),
2145 spec: Some("Root task".to_string()),
2146 priority: None,
2147 children: None,
2148 depends_on: None,
2149 task_id: None,
2150 status: None,
2151 active_form: None,
2152 parent_id: None,
2153 },
2154 TaskTree {
2155 name: "Task B".to_string(),
2156 spec: Some("Depends on A".to_string()),
2157 priority: None,
2158 children: None,
2159 depends_on: Some(vec!["Task A".to_string()]),
2160 task_id: None,
2161 status: None,
2162 active_form: None,
2163 parent_id: None,
2164 },
2165 TaskTree {
2166 name: "Task C".to_string(),
2167 spec: Some("Depends on A".to_string()),
2168 priority: None,
2169 children: None,
2170 depends_on: Some(vec!["Task A".to_string()]),
2171 task_id: None,
2172 status: None,
2173 active_form: None,
2174 parent_id: None,
2175 },
2176 TaskTree {
2177 name: "Task D".to_string(),
2178 spec: Some("Depends on B and C".to_string()),
2179 priority: None,
2180 children: None,
2181 depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
2182 task_id: None,
2183 status: None,
2184 active_form: None,
2185 parent_id: None,
2186 },
2187 ],
2188 };
2189
2190 let executor = PlanExecutor::new(&ctx.pool);
2191 let result = executor.execute(&request).await.unwrap();
2192
2193 assert!(
2194 result.success,
2195 "Plan execution should succeed for valid DAG"
2196 );
2197 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2198 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2199 }
2200
2201 #[tokio::test]
2202 async fn test_plan_executor_self_dependency() {
2203 use crate::test_utils::test_helpers::TestContext;
2204
2205 let ctx = TestContext::new().await;
2206
2207 let request = PlanRequest {
2209 tasks: vec![TaskTree {
2210 name: "Task A".to_string(),
2211 spec: Some("Depends on itself".to_string()),
2212 priority: None,
2213 children: None,
2214 depends_on: Some(vec!["Task A".to_string()]),
2215 task_id: None,
2216 status: None,
2217 active_form: None,
2218 parent_id: None,
2219 }],
2220 };
2221
2222 let executor = PlanExecutor::new(&ctx.pool);
2223 let result = executor.execute(&request).await.unwrap();
2224
2225 assert!(
2226 !result.success,
2227 "Plan execution should fail for self-dependency"
2228 );
2229 assert!(result.error.is_some(), "Should have error message");
2230 let error = result.error.unwrap();
2231 assert!(
2232 error.contains("Circular dependency"),
2233 "Error should mention circular dependency: {}",
2234 error
2235 );
2236 }
2237
2238 #[tokio::test]
2240 async fn test_find_tasks_by_names_empty() {
2241 use crate::test_utils::test_helpers::TestContext;
2242
2243 let ctx = TestContext::new().await;
2244 let executor = PlanExecutor::new(&ctx.pool);
2245
2246 let result = executor.find_tasks_by_names(&[]).await.unwrap();
2247 assert!(result.is_empty(), "Empty input should return empty map");
2248 }
2249
2250 #[tokio::test]
2251 async fn test_find_tasks_by_names_partial() {
2252 use crate::test_utils::test_helpers::TestContext;
2253
2254 let ctx = TestContext::new().await;
2255 let executor = PlanExecutor::new(&ctx.pool);
2256
2257 let request = PlanRequest {
2259 tasks: vec![
2260 TaskTree {
2261 name: "Task A".to_string(),
2262 spec: None,
2263 priority: None,
2264 children: None,
2265 depends_on: None,
2266 task_id: None,
2267 status: None,
2268 active_form: None,
2269 parent_id: None,
2270 },
2271 TaskTree {
2272 name: "Task B".to_string(),
2273 spec: None,
2274 priority: None,
2275 children: None,
2276 depends_on: None,
2277 task_id: None,
2278 status: None,
2279 active_form: None,
2280 parent_id: None,
2281 },
2282 ],
2283 };
2284 executor.execute(&request).await.unwrap();
2285
2286 let names = vec![
2288 "Task A".to_string(),
2289 "Task B".to_string(),
2290 "Task C".to_string(),
2291 ];
2292 let result = executor.find_tasks_by_names(&names).await.unwrap();
2293
2294 assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
2295 assert!(result.contains_key("Task A"));
2296 assert!(result.contains_key("Task B"));
2297 assert!(!result.contains_key("Task C"));
2298 }
2299
2300 #[tokio::test]
2302 async fn test_plan_1000_tasks_performance() {
2303 use crate::test_utils::test_helpers::TestContext;
2304
2305 let ctx = TestContext::new().await;
2306 let executor = PlanExecutor::new(&ctx.pool);
2307
2308 let mut tasks = Vec::new();
2310 for i in 0..1000 {
2311 tasks.push(TaskTree {
2312 name: format!("Task {}", i),
2313 spec: Some(format!("Spec for task {}", i)),
2314 priority: Some(PriorityValue::Medium),
2315 children: None,
2316 depends_on: None,
2317 task_id: None,
2318 status: None,
2319 active_form: None,
2320 parent_id: None,
2321 });
2322 }
2323
2324 let request = PlanRequest { tasks };
2325
2326 let start = std::time::Instant::now();
2327 let result = executor.execute(&request).await.unwrap();
2328 let duration = start.elapsed();
2329
2330 assert!(result.success);
2331 assert_eq!(result.created_count, 1000);
2332 assert!(
2333 duration.as_secs() < 10,
2334 "Should complete 1000 tasks in under 10 seconds, took {:?}",
2335 duration
2336 );
2337
2338 println!("✅ Created 1000 tasks in {:?}", duration);
2339 }
2340
2341 #[tokio::test]
2342 async fn test_plan_deep_nesting_20_levels() {
2343 use crate::test_utils::test_helpers::TestContext;
2344
2345 let ctx = TestContext::new().await;
2346 let executor = PlanExecutor::new(&ctx.pool);
2347
2348 fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
2350 TaskTree {
2351 name: format!("Level {}", current),
2352 spec: Some(format!("Task at depth {}", current)),
2353 priority: Some(PriorityValue::Low),
2354 children: if current < depth {
2355 Some(vec![build_deep_tree(depth, current + 1)])
2356 } else {
2357 None
2358 },
2359 depends_on: None,
2360 task_id: None,
2361 status: None,
2362 active_form: None,
2363 parent_id: None,
2364 }
2365 }
2366
2367 let request = PlanRequest {
2368 tasks: vec![build_deep_tree(20, 1)],
2369 };
2370
2371 let start = std::time::Instant::now();
2372 let result = executor.execute(&request).await.unwrap();
2373 let duration = start.elapsed();
2374
2375 assert!(result.success);
2376 assert_eq!(
2377 result.created_count, 20,
2378 "Should create 20 tasks (1 per level)"
2379 );
2380 assert!(
2381 duration.as_secs() < 5,
2382 "Should handle 20-level nesting in under 5 seconds, took {:?}",
2383 duration
2384 );
2385
2386 println!("✅ Created 20-level deep tree in {:?}", duration);
2387 }
2388
2389 #[test]
2390 fn test_flatten_preserves_all_fields() {
2391 let tasks = vec![TaskTree {
2392 name: "Full Task".to_string(),
2393 spec: Some("Detailed spec".to_string()),
2394 priority: Some(PriorityValue::Critical),
2395 children: None,
2396 depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2397 task_id: Some(42),
2398 status: None,
2399 active_form: None,
2400 parent_id: None,
2401 }];
2402
2403 let flat = flatten_task_tree(&tasks);
2404 assert_eq!(flat.len(), 1);
2405
2406 let task = &flat[0];
2407 assert_eq!(task.name, "Full Task");
2408 assert_eq!(task.spec, Some("Detailed spec".to_string()));
2409 assert_eq!(task.priority, Some(PriorityValue::Critical));
2410 assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2411 assert_eq!(task.task_id, Some(42));
2412 }
2413}
2414
2415#[cfg(test)]
2416mod dataflow_tests {
2417 use super::*;
2418 use crate::tasks::TaskManager;
2419 use crate::test_utils::test_helpers::TestContext;
2420
2421 #[tokio::test]
2422 async fn test_complete_dataflow_status_and_active_form() {
2423 let ctx = TestContext::new().await;
2425
2426 let request = PlanRequest {
2428 tasks: vec![TaskTree {
2429 name: "Test Active Form Task".to_string(),
2430 spec: Some("Testing complete dataflow".to_string()),
2431 priority: Some(PriorityValue::High),
2432 children: None,
2433 depends_on: None,
2434 task_id: None,
2435 status: Some(TaskStatus::Doing),
2436 active_form: Some("Testing complete dataflow now".to_string()),
2437 parent_id: None,
2438 }],
2439 };
2440
2441 let executor = PlanExecutor::new(&ctx.pool);
2442 let result = executor.execute(&request).await.unwrap();
2443
2444 assert!(result.success);
2445 assert_eq!(result.created_count, 1);
2446
2447 let task_mgr = TaskManager::new(&ctx.pool);
2449 let result = task_mgr
2450 .find_tasks(None, None, None, None, None)
2451 .await
2452 .unwrap();
2453
2454 assert_eq!(result.tasks.len(), 1);
2455 let task = &result.tasks[0];
2456
2457 assert_eq!(task.name, "Test Active Form Task");
2459 assert_eq!(task.status, "doing"); assert_eq!(
2461 task.active_form,
2462 Some("Testing complete dataflow now".to_string())
2463 );
2464
2465 let json = serde_json::to_value(task).unwrap();
2467 assert_eq!(json["name"], "Test Active Form Task");
2468 assert_eq!(json["status"], "doing");
2469 assert_eq!(json["active_form"], "Testing complete dataflow now");
2470
2471 println!("✅ 完整数据流验证成功!");
2472 println!(" Plan工具写入 -> Task读取 -> JSON序列化 -> MCP输出");
2473 println!(" active_form: {:?}", task.active_form);
2474 }
2475}
2476
2477#[cfg(test)]
2478mod parent_id_tests {
2479 use super::*;
2480 use crate::test_utils::test_helpers::TestContext;
2481
2482 #[test]
2483 fn test_parent_id_json_deserialization_absent() {
2484 let json = r#"{"name": "Test Task"}"#;
2486 let task: TaskTree = serde_json::from_str(json).unwrap();
2487 assert_eq!(task.parent_id, None);
2488 }
2489
2490 #[test]
2491 fn test_parent_id_json_deserialization_null() {
2492 let json = r#"{"name": "Test Task", "parent_id": null}"#;
2494 let task: TaskTree = serde_json::from_str(json).unwrap();
2495 assert_eq!(task.parent_id, Some(None));
2496 }
2497
2498 #[test]
2499 fn test_parent_id_json_deserialization_number() {
2500 let json = r#"{"name": "Test Task", "parent_id": 42}"#;
2502 let task: TaskTree = serde_json::from_str(json).unwrap();
2503 assert_eq!(task.parent_id, Some(Some(42)));
2504 }
2505
2506 #[test]
2507 fn test_flatten_propagates_parent_id() {
2508 let tasks = vec![TaskTree {
2509 name: "Task with explicit parent".to_string(),
2510 spec: None,
2511 priority: None,
2512 children: None,
2513 depends_on: None,
2514 task_id: None,
2515 status: None,
2516 active_form: None,
2517 parent_id: Some(Some(99)),
2518 }];
2519
2520 let flat = flatten_task_tree(&tasks);
2521 assert_eq!(flat.len(), 1);
2522 assert_eq!(flat[0].explicit_parent_id, Some(Some(99)));
2523 }
2524
2525 #[test]
2526 fn test_flatten_propagates_null_parent_id() {
2527 let tasks = vec![TaskTree {
2528 name: "Explicit root task".to_string(),
2529 spec: None,
2530 priority: None,
2531 children: None,
2532 depends_on: None,
2533 task_id: None,
2534 status: None,
2535 active_form: None,
2536 parent_id: Some(None), }];
2538
2539 let flat = flatten_task_tree(&tasks);
2540 assert_eq!(flat.len(), 1);
2541 assert_eq!(flat[0].explicit_parent_id, Some(None));
2542 }
2543
2544 #[tokio::test]
2545 async fn test_explicit_parent_id_sets_parent() {
2546 let ctx = TestContext::new().await;
2547
2548 let request1 = PlanRequest {
2550 tasks: vec![TaskTree {
2551 name: "Parent Task".to_string(),
2552 spec: Some("This is the parent".to_string()),
2553 priority: None,
2554 children: None,
2555 depends_on: None,
2556 task_id: None,
2557 status: Some(TaskStatus::Doing),
2558 active_form: None,
2559 parent_id: None,
2560 }],
2561 };
2562
2563 let executor = PlanExecutor::new(&ctx.pool);
2564 let result1 = executor.execute(&request1).await.unwrap();
2565 assert!(result1.success);
2566 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
2567
2568 let request2 = PlanRequest {
2570 tasks: vec![TaskTree {
2571 name: "Child Task".to_string(),
2572 spec: Some("This uses explicit parent_id".to_string()),
2573 priority: None,
2574 children: None,
2575 depends_on: None,
2576 task_id: None,
2577 status: None,
2578 active_form: None,
2579 parent_id: Some(Some(parent_id)),
2580 }],
2581 };
2582
2583 let result2 = executor.execute(&request2).await.unwrap();
2584 assert!(result2.success);
2585 let child_id = *result2.task_id_map.get("Child Task").unwrap();
2586
2587 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2589 .bind(child_id)
2590 .fetch_one(&ctx.pool)
2591 .await
2592 .unwrap();
2593 assert_eq!(row.0, Some(parent_id));
2594 }
2595
2596 #[tokio::test]
2597 async fn test_explicit_null_parent_id_creates_root() {
2598 let ctx = TestContext::new().await;
2599
2600 let request = PlanRequest {
2603 tasks: vec![TaskTree {
2604 name: "Explicit Root Task".to_string(),
2605 spec: Some("Should be root despite default parent".to_string()),
2606 priority: None,
2607 children: None,
2608 depends_on: None,
2609 task_id: None,
2610 status: Some(TaskStatus::Doing),
2611 active_form: None,
2612 parent_id: Some(None), }],
2614 };
2615
2616 let parent_request = PlanRequest {
2619 tasks: vec![TaskTree {
2620 name: "Default Parent".to_string(),
2621 spec: None,
2622 priority: None,
2623 children: None,
2624 depends_on: None,
2625 task_id: None,
2626 status: None,
2627 active_form: None,
2628 parent_id: None,
2629 }],
2630 };
2631 let executor = PlanExecutor::new(&ctx.pool);
2632 let parent_result = executor.execute(&parent_request).await.unwrap();
2633 let default_parent_id = *parent_result.task_id_map.get("Default Parent").unwrap();
2634
2635 let executor_with_default =
2637 PlanExecutor::new(&ctx.pool).with_default_parent(default_parent_id);
2638 let result = executor_with_default.execute(&request).await.unwrap();
2639 assert!(result.success);
2640 let task_id = *result.task_id_map.get("Explicit Root Task").unwrap();
2641
2642 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2644 .bind(task_id)
2645 .fetch_one(&ctx.pool)
2646 .await
2647 .unwrap();
2648 assert_eq!(
2649 row.0, None,
2650 "Task with explicit null parent_id should be root"
2651 );
2652 }
2653
2654 #[tokio::test]
2655 async fn test_children_nesting_takes_precedence_over_parent_id() {
2656 let ctx = TestContext::new().await;
2657
2658 let request = PlanRequest {
2660 tasks: vec![TaskTree {
2661 name: "Parent via Nesting".to_string(),
2662 spec: None,
2663 priority: None,
2664 children: Some(vec![TaskTree {
2665 name: "Child via Nesting".to_string(),
2666 spec: None,
2667 priority: None,
2668 children: None,
2669 depends_on: None,
2670 task_id: None,
2671 status: None,
2672 active_form: None,
2673 parent_id: Some(Some(999)), }]),
2675 depends_on: None,
2676 task_id: None,
2677 status: Some(TaskStatus::Doing),
2678 active_form: None,
2679 parent_id: None,
2680 }],
2681 };
2682
2683 let executor = PlanExecutor::new(&ctx.pool);
2684 let result = executor.execute(&request).await.unwrap();
2685 assert!(result.success);
2686
2687 let parent_id = *result.task_id_map.get("Parent via Nesting").unwrap();
2688 let child_id = *result.task_id_map.get("Child via Nesting").unwrap();
2689
2690 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2692 .bind(child_id)
2693 .fetch_one(&ctx.pool)
2694 .await
2695 .unwrap();
2696 assert_eq!(
2697 row.0,
2698 Some(parent_id),
2699 "Children nesting should take precedence"
2700 );
2701 }
2702
2703 #[tokio::test]
2704 async fn test_modify_existing_task_parent() {
2705 let ctx = TestContext::new().await;
2706 let executor = PlanExecutor::new(&ctx.pool);
2707
2708 let request1 = PlanRequest {
2710 tasks: vec![
2711 TaskTree {
2712 name: "Task A".to_string(),
2713 spec: None,
2714 priority: None,
2715 children: None,
2716 depends_on: None,
2717 task_id: None,
2718 status: Some(TaskStatus::Doing),
2719 active_form: None,
2720 parent_id: None,
2721 },
2722 TaskTree {
2723 name: "Task B".to_string(),
2724 spec: None,
2725 priority: None,
2726 children: None,
2727 depends_on: None,
2728 task_id: None,
2729 status: None,
2730 active_form: None,
2731 parent_id: None,
2732 },
2733 ],
2734 };
2735
2736 let result1 = executor.execute(&request1).await.unwrap();
2737 assert!(result1.success);
2738 let task_a_id = *result1.task_id_map.get("Task A").unwrap();
2739 let task_b_id = *result1.task_id_map.get("Task B").unwrap();
2740
2741 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2743 .bind(task_b_id)
2744 .fetch_one(&ctx.pool)
2745 .await
2746 .unwrap();
2747 assert_eq!(row.0, None, "Task B should initially be root");
2748
2749 let request2 = PlanRequest {
2751 tasks: vec![TaskTree {
2752 name: "Task B".to_string(), spec: None,
2754 priority: None,
2755 children: None,
2756 depends_on: None,
2757 task_id: None,
2758 status: None,
2759 active_form: None,
2760 parent_id: Some(Some(task_a_id)), }],
2762 };
2763
2764 let result2 = executor.execute(&request2).await.unwrap();
2765 assert!(result2.success);
2766 assert_eq!(result2.updated_count, 1, "Should update existing task");
2767
2768 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2770 .bind(task_b_id)
2771 .fetch_one(&ctx.pool)
2772 .await
2773 .unwrap();
2774 assert_eq!(
2775 row.0,
2776 Some(task_a_id),
2777 "Task B should now be child of Task A"
2778 );
2779 }
2780}