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