1use serde::{Deserialize, Serialize};
8use sqlx::Row;
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
14pub struct PlanRequest {
15 pub tasks: Vec<TaskTree>,
17}
18
19#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
21pub struct TaskTree {
22 pub name: String,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub spec: Option<String>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub priority: Option<PriorityValue>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub children: Option<Vec<TaskTree>>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub depends_on: Option<Vec<String>>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub task_id: Option<i64>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub status: Option<TaskStatus>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
52 pub active_form: Option<String>,
53
54 #[serde(
59 default,
60 skip_serializing_if = "Option::is_none",
61 deserialize_with = "deserialize_parent_id"
62 )]
63 pub parent_id: Option<Option<i64>>,
64}
65
66fn deserialize_parent_id<'de, D>(
72 deserializer: D,
73) -> std::result::Result<Option<Option<i64>>, D::Error>
74where
75 D: serde::Deserializer<'de>,
76{
77 let inner: Option<i64> = Option::deserialize(deserializer)?;
84 Ok(Some(inner))
85}
86
87#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
89#[serde(rename_all = "snake_case")]
90pub enum TaskStatus {
91 Todo,
92 Doing,
93 Done,
94}
95
96impl TaskStatus {
97 pub fn as_db_str(&self) -> &'static str {
99 match self {
100 TaskStatus::Todo => "todo",
101 TaskStatus::Doing => "doing",
102 TaskStatus::Done => "done",
103 }
104 }
105
106 pub fn from_db_str(s: &str) -> Option<Self> {
108 match s {
109 "todo" => Some(TaskStatus::Todo),
110 "doing" => Some(TaskStatus::Doing),
111 "done" => Some(TaskStatus::Done),
112 _ => None,
113 }
114 }
115
116 pub fn as_str(&self) -> &'static str {
118 match self {
119 TaskStatus::Todo => "todo",
120 TaskStatus::Doing => "doing",
121 TaskStatus::Done => "done",
122 }
123 }
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
128#[serde(rename_all = "lowercase")]
129pub enum PriorityValue {
130 Critical,
131 High,
132 Medium,
133 Low,
134}
135
136impl PriorityValue {
137 pub fn to_int(&self) -> i32 {
139 match self {
140 PriorityValue::Critical => 1,
141 PriorityValue::High => 2,
142 PriorityValue::Medium => 3,
143 PriorityValue::Low => 4,
144 }
145 }
146
147 pub fn from_int(value: i32) -> Option<Self> {
149 match value {
150 1 => Some(PriorityValue::Critical),
151 2 => Some(PriorityValue::High),
152 3 => Some(PriorityValue::Medium),
153 4 => Some(PriorityValue::Low),
154 _ => None,
155 }
156 }
157
158 pub fn as_str(&self) -> &'static str {
160 match self {
161 PriorityValue::Critical => "critical",
162 PriorityValue::High => "high",
163 PriorityValue::Medium => "medium",
164 PriorityValue::Low => "low",
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct ExistingTaskInfo {
172 pub id: i64,
173 pub status: String,
174 pub spec: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
179pub struct PlanResult {
180 pub success: bool,
182
183 pub task_id_map: HashMap<String, i64>,
185
186 pub created_count: usize,
188
189 pub updated_count: usize,
191
192 pub dependency_count: usize,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
198 pub focused_task: Option<crate::db::models::TaskWithEvents>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub error: Option<String>,
203
204 #[serde(skip_serializing_if = "Vec::is_empty", default)]
206 pub warnings: Vec<String>,
207}
208
209impl PlanResult {
210 pub fn success(
212 task_id_map: HashMap<String, i64>,
213 created_count: usize,
214 updated_count: usize,
215 dependency_count: usize,
216 focused_task: Option<crate::db::models::TaskWithEvents>,
217 ) -> Self {
218 Self {
219 success: true,
220 task_id_map,
221 created_count,
222 updated_count,
223 dependency_count,
224 focused_task,
225 error: None,
226 warnings: Vec::new(),
227 }
228 }
229
230 pub fn success_with_warnings(
232 task_id_map: HashMap<String, i64>,
233 created_count: usize,
234 updated_count: usize,
235 dependency_count: usize,
236 focused_task: Option<crate::db::models::TaskWithEvents>,
237 warnings: Vec<String>,
238 ) -> Self {
239 Self {
240 success: true,
241 task_id_map,
242 created_count,
243 updated_count,
244 dependency_count,
245 focused_task,
246 error: None,
247 warnings,
248 }
249 }
250
251 pub fn error(message: impl Into<String>) -> Self {
253 Self {
254 success: false,
255 task_id_map: HashMap::new(),
256 created_count: 0,
257 updated_count: 0,
258 dependency_count: 0,
259 focused_task: None,
260 error: Some(message.into()),
261 warnings: Vec::new(),
262 }
263 }
264}
265
266pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
272 let mut names = Vec::new();
273
274 for task in tasks {
275 names.push(task.name.clone());
276
277 if let Some(children) = &task.children {
278 names.extend(extract_all_names(children));
279 }
280 }
281
282 names
283}
284
285#[derive(Debug, Clone, PartialEq)]
287pub struct FlatTask {
288 pub name: String,
289 pub spec: Option<String>,
290 pub priority: Option<PriorityValue>,
291 pub parent_name: Option<String>,
293 pub depends_on: Vec<String>,
294 pub task_id: Option<i64>,
295 pub status: Option<TaskStatus>,
296 pub active_form: Option<String>,
297 pub explicit_parent_id: Option<Option<i64>>,
302}
303
304pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
305 flatten_task_tree_recursive(tasks, None)
306}
307
308fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
309 let mut flat = Vec::new();
310
311 for task in tasks {
312 let flat_task = FlatTask {
313 name: task.name.clone(),
314 spec: task.spec.clone(),
315 priority: task.priority.clone(),
316 parent_name: parent_name.clone(),
317 depends_on: task.depends_on.clone().unwrap_or_default(),
318 task_id: task.task_id,
319 status: task.status.clone(),
320 active_form: task.active_form.clone(),
321 explicit_parent_id: task.parent_id,
322 };
323
324 flat.push(flat_task);
325
326 if let Some(children) = &task.children {
328 flat.extend(flatten_task_tree_recursive(
329 children,
330 Some(task.name.clone()),
331 ));
332 }
333 }
334
335 flat
336}
337
338#[derive(Debug, Clone, PartialEq)]
340pub enum Operation {
341 Create(FlatTask),
342 Update { task_id: i64, task: FlatTask },
343}
344
345pub fn classify_operations(
354 flat_tasks: &[FlatTask],
355 existing_names: &HashMap<String, i64>,
356) -> Vec<Operation> {
357 let mut operations = Vec::new();
358
359 for task in flat_tasks {
360 let operation = if let Some(task_id) = task.task_id {
362 Operation::Update {
364 task_id,
365 task: task.clone(),
366 }
367 } else if let Some(&task_id) = existing_names.get(&task.name) {
368 Operation::Update {
370 task_id,
371 task: task.clone(),
372 }
373 } else {
374 Operation::Create(task.clone())
376 };
377
378 operations.push(operation);
379 }
380
381 operations
382}
383
384pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
386 let mut seen = HashMap::new();
387 let mut duplicates = Vec::new();
388
389 for name in extract_all_names(tasks) {
390 let count = seen.entry(name.clone()).or_insert(0);
391 *count += 1;
392 if *count == 2 {
393 duplicates.push(name);
395 }
396 }
397
398 duplicates
399}
400
401use crate::error::{IntentError, Result};
406use sqlx::SqlitePool;
407
408pub struct PlanExecutor<'a> {
410 pool: &'a SqlitePool,
411 project_path: Option<String>,
412 default_parent_id: Option<i64>,
414}
415
416impl<'a> PlanExecutor<'a> {
417 pub fn new(pool: &'a SqlitePool) -> Self {
419 Self {
420 pool,
421 project_path: None,
422 default_parent_id: None,
423 }
424 }
425
426 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
428 Self {
429 pool,
430 project_path: Some(project_path),
431 default_parent_id: None,
432 }
433 }
434
435 pub fn with_default_parent(mut self, parent_id: i64) -> Self {
438 self.default_parent_id = Some(parent_id);
439 self
440 }
441
442 fn get_task_manager(&self) -> crate::tasks::TaskManager<'a> {
444 match &self.project_path {
445 Some(path) => crate::tasks::TaskManager::with_project_path(self.pool, path.clone()),
446 None => crate::tasks::TaskManager::new(self.pool),
447 }
448 }
449
450 #[tracing::instrument(skip(self, request), fields(task_count = request.tasks.len()))]
452 pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
453 let duplicates = find_duplicate_names(&request.tasks);
455 if !duplicates.is_empty() {
456 return Ok(PlanResult::error(format!(
457 "Duplicate task names in request: {:?}",
458 duplicates
459 )));
460 }
461
462 let all_names = extract_all_names(&request.tasks);
464
465 let existing = self.find_tasks_by_names(&all_names).await?;
467
468 let flat_tasks = flatten_task_tree(&request.tasks);
470
471 if let Err(e) = self.validate_dependencies(&flat_tasks) {
473 return Ok(PlanResult::error(e.to_string()));
474 }
475
476 if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
478 return Ok(PlanResult::error(e.to_string()));
479 }
480
481 if let Err(e) = self.validate_batch_single_doing(&flat_tasks) {
483 return Ok(PlanResult::error(e.to_string()));
484 }
485
486 let task_mgr = self.get_task_manager();
488
489 let mut tx = self.pool.begin().await?;
491
492 let mut task_id_map = HashMap::new();
494 let mut created_count = 0;
495 let mut updated_count = 0;
496 let mut warnings: Vec<String> = Vec::new();
497 let mut newly_created_names: std::collections::HashSet<String> =
498 std::collections::HashSet::new();
499
500 for task in &flat_tasks {
501 let is_becoming_doing = task.status.as_ref() == Some(&TaskStatus::Doing);
503 let has_spec = task
504 .spec
505 .as_ref()
506 .map(|s| !s.trim().is_empty())
507 .unwrap_or(false);
508
509 if let Some(existing_info) = existing.get(&task.name) {
510 if is_becoming_doing && !has_spec {
515 let existing_is_doing = existing_info.status == "doing";
516 let existing_has_spec = existing_info
517 .spec
518 .as_ref()
519 .map(|s| !s.trim().is_empty())
520 .unwrap_or(false);
521
522 if !existing_is_doing && !existing_has_spec {
524 return Ok(PlanResult::error(format!(
525 "Task '{}': spec (description) is required when starting a task (status: doing).\n\n\
526 Before starting a task, please describe:\n \
527 • What is the goal of this task\n \
528 • How do you plan to approach it\n\n\
529 Tip: Use @file(path) to include content from a file",
530 task.name
531 )));
532 }
533 }
534
535 let is_becoming_done = task.status.as_ref() == Some(&TaskStatus::Done);
537
538 task_mgr
540 .update_task_in_tx(
541 &mut tx,
542 existing_info.id,
543 task.spec.as_deref(),
544 task.priority.as_ref().map(|p| p.to_int()),
545 if is_becoming_done {
547 None
548 } else {
549 task.status.as_ref().map(|s| s.as_db_str())
550 },
551 task.active_form.as_deref(),
552 )
553 .await?;
554
555 if is_becoming_done {
557 if let Err(e) = task_mgr
558 .complete_task_in_tx(&mut tx, existing_info.id)
559 .await
560 {
561 return Ok(PlanResult::error(format!(
563 "Cannot complete task '{}': {}\n\n\
564 Please complete all subtasks before marking the parent as done.",
565 task.name, e
566 )));
567 }
568 }
569
570 task_id_map.insert(task.name.clone(), existing_info.id);
571 updated_count += 1;
572 } else {
573 if is_becoming_doing && !has_spec {
577 return Ok(PlanResult::error(format!(
578 "Task '{}': spec (description) is required when starting a task (status: doing).\n\n\
579 Before starting a task, please describe:\n \
580 • What is the goal of this task\n \
581 • How do you plan to approach it\n\n\
582 Tip: Use @file(path) to include content from a file",
583 task.name
584 )));
585 }
586
587 let id = task_mgr
588 .create_task_in_tx(
589 &mut tx,
590 &task.name,
591 task.spec.as_deref(),
592 task.priority.as_ref().map(|p| p.to_int()),
593 task.status.as_ref().map(|s| s.as_db_str()),
594 task.active_form.as_deref(),
595 "ai", )
597 .await?;
598 task_id_map.insert(task.name.clone(), id);
599 newly_created_names.insert(task.name.clone());
600 created_count += 1;
601
602 if !has_spec && !is_becoming_doing {
604 warnings.push(format!(
605 "Task '{}' has no description. Consider adding one for better context.",
606 task.name
607 ));
608 }
609 }
610 }
611
612 for task in &flat_tasks {
614 if let Some(parent_name) = &task.parent_name {
615 let task_id = task_id_map.get(&task.name).ok_or_else(|| {
616 IntentError::InvalidInput(format!("Task not found: {}", task.name))
617 })?;
618 let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
619 IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
620 })?;
621 task_mgr
622 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
623 .await?;
624 }
625 }
626
627 for task in &flat_tasks {
630 if task.parent_name.is_some() {
632 continue;
633 }
634
635 if let Some(explicit_parent) = &task.explicit_parent_id {
637 let task_id = task_id_map.get(&task.name).ok_or_else(|| {
638 IntentError::InvalidInput(format!("Task not found: {}", task.name))
639 })?;
640
641 match explicit_parent {
642 None => {
643 task_mgr.clear_parent_in_tx(&mut tx, *task_id).await?;
645 },
646 Some(parent_id) => {
647 task_mgr
650 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
651 .await?;
652 },
653 }
654 }
655 }
656
657 if let Some(default_parent) = self.default_parent_id {
659 for task in &flat_tasks {
660 if newly_created_names.contains(&task.name)
665 && task.parent_name.is_none()
666 && task.explicit_parent_id.is_none()
667 {
668 if let Some(&task_id) = task_id_map.get(&task.name) {
669 task_mgr
670 .set_parent_in_tx(&mut tx, task_id, default_parent)
671 .await?;
672 }
673 }
674 }
675 }
676
677 let dep_count = self
679 .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
680 .await?;
681
682 tx.commit().await?;
684
685 task_mgr.notify_batch_changed().await;
687
688 let doing_task = flat_tasks
691 .iter()
692 .find(|task| matches!(task.status, Some(TaskStatus::Doing)));
693
694 let focused_task_response = if let Some(doing_task) = doing_task {
695 if let Some(&task_id) = task_id_map.get(&doing_task.name) {
697 let response = task_mgr.start_task(task_id, true).await?;
699 Some(response)
700 } else {
701 None
702 }
703 } else {
704 None
705 };
706
707 Ok(PlanResult::success_with_warnings(
709 task_id_map,
710 created_count,
711 updated_count,
712 dep_count,
713 focused_task_response,
714 warnings,
715 ))
716 }
717
718 async fn find_tasks_by_names(
720 &self,
721 names: &[String],
722 ) -> Result<HashMap<String, ExistingTaskInfo>> {
723 if names.is_empty() {
724 return Ok(HashMap::new());
725 }
726
727 let mut map = HashMap::new();
728
729 let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
732 let query = format!(
733 "SELECT id, name, status, spec FROM tasks WHERE name IN ({})",
734 placeholders
735 );
736
737 let mut query_builder = sqlx::query(&query);
738 for name in names {
739 query_builder = query_builder.bind(name);
740 }
741
742 let rows = query_builder.fetch_all(self.pool).await?;
743
744 for row in rows {
745 let id: i64 = row.get("id");
746 let name: String = row.get("name");
747 let status: String = row.get("status");
748 let spec: Option<String> = row.get("spec");
749 map.insert(name, ExistingTaskInfo { id, status, spec });
750 }
751
752 Ok(map)
753 }
754
755 async fn build_dependencies(
757 &self,
758 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
759 flat_tasks: &[FlatTask],
760 task_id_map: &HashMap<String, i64>,
761 ) -> Result<usize> {
762 let mut count = 0;
763
764 for task in flat_tasks {
765 if !task.depends_on.is_empty() {
766 let blocked_id = task_id_map.get(&task.name).ok_or_else(|| {
767 IntentError::InvalidInput(format!("Task not found: {}", task.name))
768 })?;
769
770 for dep_name in &task.depends_on {
771 let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
772 IntentError::InvalidInput(format!(
773 "Dependency '{}' not found for task '{}'",
774 dep_name, task.name
775 ))
776 })?;
777
778 sqlx::query(
779 "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
780 )
781 .bind(blocking_id)
782 .bind(blocked_id)
783 .execute(&mut **tx)
784 .await?;
785
786 count += 1;
787 }
788 }
789 }
790
791 Ok(count)
792 }
793
794 fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
796 let task_names: std::collections::HashSet<_> =
797 flat_tasks.iter().map(|t| t.name.as_str()).collect();
798
799 for task in flat_tasks {
800 for dep_name in &task.depends_on {
801 if !task_names.contains(dep_name.as_str()) {
802 return Err(IntentError::InvalidInput(format!(
803 "Task '{}' depends on '{}', but '{}' is not in the plan",
804 task.name, dep_name, dep_name
805 )));
806 }
807 }
808 }
809
810 Ok(())
811 }
812
813 fn validate_batch_single_doing(&self, flat_tasks: &[FlatTask]) -> Result<()> {
817 let doing_tasks: Vec<&FlatTask> = flat_tasks
819 .iter()
820 .filter(|task| matches!(task.status, Some(TaskStatus::Doing)))
821 .collect();
822
823 if doing_tasks.len() > 1 {
825 let names: Vec<&str> = doing_tasks.iter().map(|t| t.name.as_str()).collect();
826 return Err(IntentError::InvalidInput(format!(
827 "Batch single doing constraint violated: only one task per batch can have status='doing'. Found: {}",
828 names.join(", ")
829 )));
830 }
831
832 Ok(())
833 }
834
835 fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
837 if flat_tasks.is_empty() {
838 return Ok(());
839 }
840
841 let name_to_idx: HashMap<&str, usize> = flat_tasks
843 .iter()
844 .enumerate()
845 .map(|(i, t)| (t.name.as_str(), i))
846 .collect();
847
848 let mut graph: Vec<Vec<usize>> = vec![Vec::new(); flat_tasks.len()];
850 for (idx, task) in flat_tasks.iter().enumerate() {
851 for dep_name in &task.depends_on {
852 if let Some(&dep_idx) = name_to_idx.get(dep_name.as_str()) {
853 graph[idx].push(dep_idx);
854 }
855 }
856 }
857
858 for task in flat_tasks {
860 if task.depends_on.contains(&task.name) {
861 return Err(IntentError::InvalidInput(format!(
862 "Circular dependency detected: task '{}' depends on itself",
863 task.name
864 )));
865 }
866 }
867
868 let sccs = self.tarjan_scc(&graph);
870
871 for scc in sccs {
873 if scc.len() > 1 {
874 let cycle_names: Vec<&str> = scc
876 .iter()
877 .map(|&idx| flat_tasks[idx].name.as_str())
878 .collect();
879
880 return Err(IntentError::InvalidInput(format!(
881 "Circular dependency detected: {}",
882 cycle_names.join(" → ")
883 )));
884 }
885 }
886
887 Ok(())
888 }
889
890 fn tarjan_scc(&self, graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
893 let n = graph.len();
894 let mut index = 0;
895 let mut stack = Vec::new();
896 let mut indices = vec![None; n];
897 let mut lowlinks = vec![0; n];
898 let mut on_stack = vec![false; n];
899 let mut sccs = Vec::new();
900
901 #[allow(clippy::too_many_arguments)]
902 fn strongconnect(
903 v: usize,
904 graph: &[Vec<usize>],
905 index: &mut usize,
906 stack: &mut Vec<usize>,
907 indices: &mut [Option<usize>],
908 lowlinks: &mut [usize],
909 on_stack: &mut [bool],
910 sccs: &mut Vec<Vec<usize>>,
911 ) {
912 indices[v] = Some(*index);
914 lowlinks[v] = *index;
915 *index += 1;
916 stack.push(v);
917 on_stack[v] = true;
918
919 for &w in &graph[v] {
921 if indices[w].is_none() {
922 strongconnect(w, graph, index, stack, indices, lowlinks, on_stack, sccs);
924 lowlinks[v] = lowlinks[v].min(lowlinks[w]);
925 } else if on_stack[w] {
926 lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
928 }
929 }
930
931 if lowlinks[v] == indices[v].unwrap() {
933 let mut scc = Vec::new();
934 loop {
935 let w = stack.pop().unwrap();
936 on_stack[w] = false;
937 scc.push(w);
938 if w == v {
939 break;
940 }
941 }
942 sccs.push(scc);
943 }
944 }
945
946 for v in 0..n {
948 if indices[v].is_none() {
949 strongconnect(
950 v,
951 graph,
952 &mut index,
953 &mut stack,
954 &mut indices,
955 &mut lowlinks,
956 &mut on_stack,
957 &mut sccs,
958 );
959 }
960 }
961
962 sccs
963 }
964}
965
966#[derive(Debug, Default)]
968pub struct FileIncludeResult {
969 pub files_to_delete: Vec<PathBuf>,
971}
972
973fn parse_file_directive(value: &str) -> Option<(PathBuf, bool)> {
979 let trimmed = value.trim();
980
981 if !trimmed.starts_with("@file(") || !trimmed.ends_with(')') {
983 return None;
984 }
985
986 let inner = &trimmed[6..trimmed.len() - 1];
988
989 if let Some(path_str) = inner.strip_suffix(", keep") {
991 Some((PathBuf::from(path_str.trim()), false)) } else if let Some(path_str) = inner.strip_suffix(",keep") {
993 Some((PathBuf::from(path_str.trim()), false))
994 } else {
995 Some((PathBuf::from(inner.trim()), true)) }
997}
998
999fn process_task_tree_includes(
1001 task: &mut TaskTree,
1002 files_to_delete: &mut Vec<PathBuf>,
1003) -> std::result::Result<(), String> {
1004 if let Some(ref spec_value) = task.spec {
1006 if let Some((file_path, should_delete)) = parse_file_directive(spec_value) {
1007 let content = std::fs::read_to_string(&file_path)
1009 .map_err(|e| format!("Failed to read @file({}): {}", file_path.display(), e))?;
1010
1011 task.spec = Some(content);
1012
1013 if should_delete {
1014 files_to_delete.push(file_path);
1015 }
1016 }
1017 }
1018
1019 if let Some(ref mut children) = task.children {
1021 for child in children.iter_mut() {
1022 process_task_tree_includes(child, files_to_delete)?;
1023 }
1024 }
1025
1026 Ok(())
1027}
1028
1029pub fn process_file_includes(
1051 request: &mut PlanRequest,
1052) -> std::result::Result<FileIncludeResult, String> {
1053 let mut result = FileIncludeResult::default();
1054
1055 for task in request.tasks.iter_mut() {
1056 process_task_tree_includes(task, &mut result.files_to_delete)?;
1057 }
1058
1059 Ok(result)
1060}
1061
1062pub fn cleanup_included_files(files: &[PathBuf]) {
1064 for file in files {
1065 if let Err(e) = std::fs::remove_file(file) {
1066 tracing::warn!("Failed to delete included file {}: {}", file.display(), e);
1068 }
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1077 fn test_priority_value_to_int() {
1078 assert_eq!(PriorityValue::Critical.to_int(), 1);
1079 assert_eq!(PriorityValue::High.to_int(), 2);
1080 assert_eq!(PriorityValue::Medium.to_int(), 3);
1081 assert_eq!(PriorityValue::Low.to_int(), 4);
1082 }
1083
1084 #[test]
1085 fn test_priority_value_from_int() {
1086 assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
1087 assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
1088 assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
1089 assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
1090 assert_eq!(PriorityValue::from_int(999), None);
1091 }
1092
1093 #[test]
1094 fn test_priority_value_as_str() {
1095 assert_eq!(PriorityValue::Critical.as_str(), "critical");
1096 assert_eq!(PriorityValue::High.as_str(), "high");
1097 assert_eq!(PriorityValue::Medium.as_str(), "medium");
1098 assert_eq!(PriorityValue::Low.as_str(), "low");
1099 }
1100
1101 #[test]
1102 fn test_plan_request_deserialization_minimal() {
1103 let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
1104 let request: PlanRequest = serde_json::from_str(json).unwrap();
1105
1106 assert_eq!(request.tasks.len(), 1);
1107 assert_eq!(request.tasks[0].name, "Test Task");
1108 assert_eq!(request.tasks[0].spec, None);
1109 assert_eq!(request.tasks[0].priority, None);
1110 assert_eq!(request.tasks[0].children, None);
1111 assert_eq!(request.tasks[0].depends_on, None);
1112 assert_eq!(request.tasks[0].task_id, None);
1113 }
1114
1115 #[test]
1116 fn test_plan_request_deserialization_full() {
1117 let json = r#"{
1118 "tasks": [{
1119 "name": "Parent Task",
1120 "spec": "Parent spec",
1121 "priority": "high",
1122 "children": [{
1123 "name": "Child Task",
1124 "spec": "Child spec"
1125 }],
1126 "depends_on": ["Other Task"],
1127 "task_id": 42
1128 }]
1129 }"#;
1130
1131 let request: PlanRequest = serde_json::from_str(json).unwrap();
1132
1133 assert_eq!(request.tasks.len(), 1);
1134 let parent = &request.tasks[0];
1135 assert_eq!(parent.name, "Parent Task");
1136 assert_eq!(parent.spec, Some("Parent spec".to_string()));
1137 assert_eq!(parent.priority, Some(PriorityValue::High));
1138 assert_eq!(parent.task_id, Some(42));
1139
1140 let children = parent.children.as_ref().unwrap();
1141 assert_eq!(children.len(), 1);
1142 assert_eq!(children[0].name, "Child Task");
1143
1144 let depends = parent.depends_on.as_ref().unwrap();
1145 assert_eq!(depends.len(), 1);
1146 assert_eq!(depends[0], "Other Task");
1147 }
1148
1149 #[test]
1150 fn test_plan_request_serialization() {
1151 let request = PlanRequest {
1152 tasks: vec![TaskTree {
1153 name: "Test Task".to_string(),
1154 spec: Some("Test spec".to_string()),
1155 priority: Some(PriorityValue::Medium),
1156 children: None,
1157 depends_on: None,
1158 task_id: None,
1159 status: None,
1160 active_form: None,
1161 parent_id: None,
1162 }],
1163 };
1164
1165 let json = serde_json::to_string(&request).unwrap();
1166 assert!(json.contains("\"name\":\"Test Task\""));
1167 assert!(json.contains("\"spec\":\"Test spec\""));
1168 assert!(json.contains("\"priority\":\"medium\""));
1169 }
1170
1171 #[test]
1172 fn test_plan_result_success() {
1173 let mut map = HashMap::new();
1174 map.insert("Task 1".to_string(), 1);
1175 map.insert("Task 2".to_string(), 2);
1176
1177 let result = PlanResult::success(map.clone(), 2, 0, 1, None);
1178
1179 assert!(result.success);
1180 assert_eq!(result.task_id_map, map);
1181 assert_eq!(result.created_count, 2);
1182 assert_eq!(result.updated_count, 0);
1183 assert_eq!(result.dependency_count, 1);
1184 assert_eq!(result.focused_task, None);
1185 assert_eq!(result.error, None);
1186 }
1187
1188 #[test]
1189 fn test_plan_result_error() {
1190 let result = PlanResult::error("Test error");
1191
1192 assert!(!result.success);
1193 assert_eq!(result.task_id_map.len(), 0);
1194 assert_eq!(result.created_count, 0);
1195 assert_eq!(result.updated_count, 0);
1196 assert_eq!(result.dependency_count, 0);
1197 assert_eq!(result.error, Some("Test error".to_string()));
1198 }
1199
1200 #[test]
1201 fn test_task_tree_nested() {
1202 let tree = TaskTree {
1203 name: "Parent".to_string(),
1204 spec: None,
1205 priority: None,
1206 children: Some(vec![
1207 TaskTree {
1208 name: "Child 1".to_string(),
1209 spec: None,
1210 priority: None,
1211 children: None,
1212 depends_on: None,
1213 task_id: None,
1214 status: None,
1215 active_form: None,
1216 parent_id: None,
1217 },
1218 TaskTree {
1219 name: "Child 2".to_string(),
1220 spec: None,
1221 priority: Some(PriorityValue::High),
1222 children: None,
1223 depends_on: None,
1224 task_id: None,
1225 status: None,
1226 active_form: None,
1227 parent_id: None,
1228 },
1229 ]),
1230 depends_on: None,
1231 task_id: None,
1232 status: None,
1233 active_form: None,
1234 parent_id: None,
1235 };
1236
1237 let json = serde_json::to_string_pretty(&tree).unwrap();
1238 let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
1239
1240 assert_eq!(tree, deserialized);
1241 assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
1242 }
1243
1244 #[test]
1245 fn test_priority_value_case_insensitive_deserialization() {
1246 let json = r#"{"name": "Test", "priority": "high"}"#;
1248 let task: TaskTree = serde_json::from_str(json).unwrap();
1249 assert_eq!(task.priority, Some(PriorityValue::High));
1250
1251 }
1254
1255 #[test]
1256 fn test_extract_all_names_simple() {
1257 let tasks = vec![
1258 TaskTree {
1259 name: "Task 1".to_string(),
1260 spec: None,
1261 priority: None,
1262 children: None,
1263 depends_on: None,
1264 task_id: None,
1265 status: None,
1266 active_form: None,
1267 parent_id: None,
1268 },
1269 TaskTree {
1270 name: "Task 2".to_string(),
1271 spec: None,
1272 priority: None,
1273 children: None,
1274 depends_on: None,
1275 task_id: None,
1276 status: None,
1277 active_form: None,
1278 parent_id: None,
1279 },
1280 ];
1281
1282 let names = extract_all_names(&tasks);
1283 assert_eq!(names, vec!["Task 1", "Task 2"]);
1284 }
1285
1286 #[test]
1287 fn test_extract_all_names_nested() {
1288 let tasks = vec![TaskTree {
1289 name: "Parent".to_string(),
1290 spec: None,
1291 priority: None,
1292 children: Some(vec![
1293 TaskTree {
1294 name: "Child 1".to_string(),
1295 spec: None,
1296 priority: None,
1297 children: None,
1298 depends_on: None,
1299 task_id: None,
1300 status: None,
1301 active_form: None,
1302 parent_id: None,
1303 },
1304 TaskTree {
1305 name: "Child 2".to_string(),
1306 spec: None,
1307 priority: None,
1308 children: Some(vec![TaskTree {
1309 name: "Grandchild".to_string(),
1310 spec: None,
1311 priority: None,
1312 children: None,
1313 depends_on: None,
1314 task_id: None,
1315 status: None,
1316 active_form: None,
1317 parent_id: None,
1318 }]),
1319 depends_on: None,
1320 task_id: None,
1321 status: None,
1322 active_form: None,
1323 parent_id: None,
1324 },
1325 ]),
1326 depends_on: None,
1327 task_id: None,
1328 status: None,
1329 active_form: None,
1330 parent_id: None,
1331 }];
1332
1333 let names = extract_all_names(&tasks);
1334 assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
1335 }
1336
1337 #[test]
1338 fn test_flatten_task_tree_simple() {
1339 let tasks = vec![TaskTree {
1340 name: "Task 1".to_string(),
1341 spec: Some("Spec 1".to_string()),
1342 priority: Some(PriorityValue::High),
1343 children: None,
1344 depends_on: Some(vec!["Task 0".to_string()]),
1345 task_id: None,
1346 status: None,
1347 active_form: None,
1348 parent_id: None,
1349 }];
1350
1351 let flat = flatten_task_tree(&tasks);
1352 assert_eq!(flat.len(), 1);
1353 assert_eq!(flat[0].name, "Task 1");
1354 assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
1355 assert_eq!(flat[0].priority, Some(PriorityValue::High));
1356 assert_eq!(flat[0].parent_name, None);
1357 assert_eq!(flat[0].depends_on, vec!["Task 0"]);
1358 }
1359
1360 #[test]
1361 fn test_flatten_task_tree_nested() {
1362 let tasks = vec![TaskTree {
1363 name: "Parent".to_string(),
1364 spec: None,
1365 priority: None,
1366 children: Some(vec![
1367 TaskTree {
1368 name: "Child 1".to_string(),
1369 spec: None,
1370 priority: None,
1371 children: None,
1372 depends_on: None,
1373 task_id: None,
1374 status: None,
1375 active_form: None,
1376 parent_id: None,
1377 },
1378 TaskTree {
1379 name: "Child 2".to_string(),
1380 spec: None,
1381 priority: None,
1382 children: None,
1383 depends_on: None,
1384 task_id: None,
1385 status: None,
1386 active_form: None,
1387 parent_id: None,
1388 },
1389 ]),
1390 depends_on: None,
1391 task_id: None,
1392 status: None,
1393 active_form: None,
1394 parent_id: None,
1395 }];
1396
1397 let flat = flatten_task_tree(&tasks);
1398 assert_eq!(flat.len(), 3);
1399
1400 assert_eq!(flat[0].name, "Parent");
1402 assert_eq!(flat[0].parent_name, None);
1403
1404 assert_eq!(flat[1].name, "Child 1");
1406 assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
1407
1408 assert_eq!(flat[2].name, "Child 2");
1409 assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
1410 }
1411
1412 #[test]
1413 fn test_classify_operations_all_create() {
1414 let flat_tasks = vec![
1415 FlatTask {
1416 name: "Task 1".to_string(),
1417 spec: None,
1418 priority: None,
1419 parent_name: None,
1420 depends_on: vec![],
1421 task_id: None,
1422 status: None,
1423 active_form: None,
1424 explicit_parent_id: None,
1425 },
1426 FlatTask {
1427 name: "Task 2".to_string(),
1428 spec: None,
1429 priority: None,
1430 parent_name: None,
1431 depends_on: vec![],
1432 task_id: None,
1433 status: None,
1434 active_form: None,
1435 explicit_parent_id: None,
1436 },
1437 ];
1438
1439 let existing = HashMap::new();
1440 let operations = classify_operations(&flat_tasks, &existing);
1441
1442 assert_eq!(operations.len(), 2);
1443 assert!(matches!(operations[0], Operation::Create(_)));
1444 assert!(matches!(operations[1], Operation::Create(_)));
1445 }
1446
1447 #[test]
1448 fn test_classify_operations_all_update() {
1449 let flat_tasks = vec![
1450 FlatTask {
1451 name: "Task 1".to_string(),
1452 spec: None,
1453 priority: None,
1454 parent_name: None,
1455 depends_on: vec![],
1456 task_id: None,
1457 status: None,
1458 active_form: None,
1459 explicit_parent_id: None,
1460 },
1461 FlatTask {
1462 name: "Task 2".to_string(),
1463 spec: None,
1464 priority: None,
1465 parent_name: None,
1466 depends_on: vec![],
1467 task_id: None,
1468 status: None,
1469 active_form: None,
1470 explicit_parent_id: None,
1471 },
1472 ];
1473
1474 let mut existing = HashMap::new();
1475 existing.insert("Task 1".to_string(), 1);
1476 existing.insert("Task 2".to_string(), 2);
1477
1478 let operations = classify_operations(&flat_tasks, &existing);
1479
1480 assert_eq!(operations.len(), 2);
1481 assert!(matches!(
1482 operations[0],
1483 Operation::Update { task_id: 1, .. }
1484 ));
1485 assert!(matches!(
1486 operations[1],
1487 Operation::Update { task_id: 2, .. }
1488 ));
1489 }
1490
1491 #[test]
1492 fn test_classify_operations_mixed() {
1493 let flat_tasks = vec![
1494 FlatTask {
1495 name: "Existing Task".to_string(),
1496 spec: None,
1497 priority: None,
1498 parent_name: None,
1499 depends_on: vec![],
1500 task_id: None,
1501 status: None,
1502 active_form: None,
1503 explicit_parent_id: None,
1504 },
1505 FlatTask {
1506 name: "New Task".to_string(),
1507 spec: None,
1508 priority: None,
1509 parent_name: None,
1510 depends_on: vec![],
1511 task_id: None,
1512 status: None,
1513 active_form: None,
1514 explicit_parent_id: None,
1515 },
1516 ];
1517
1518 let mut existing = HashMap::new();
1519 existing.insert("Existing Task".to_string(), 42);
1520
1521 let operations = classify_operations(&flat_tasks, &existing);
1522
1523 assert_eq!(operations.len(), 2);
1524 assert!(matches!(
1525 operations[0],
1526 Operation::Update { task_id: 42, .. }
1527 ));
1528 assert!(matches!(operations[1], Operation::Create(_)));
1529 }
1530
1531 #[test]
1532 fn test_classify_operations_explicit_task_id() {
1533 let flat_tasks = vec![FlatTask {
1534 name: "Task".to_string(),
1535 spec: None,
1536 priority: None,
1537 parent_name: None,
1538 depends_on: vec![],
1539 task_id: Some(99), status: None,
1541 active_form: None,
1542 explicit_parent_id: None,
1543 }];
1544
1545 let existing = HashMap::new(); let operations = classify_operations(&flat_tasks, &existing);
1548
1549 assert_eq!(operations.len(), 1);
1551 assert!(matches!(
1552 operations[0],
1553 Operation::Update { task_id: 99, .. }
1554 ));
1555 }
1556
1557 #[test]
1558 fn test_find_duplicate_names_no_duplicates() {
1559 let tasks = vec![
1560 TaskTree {
1561 name: "Task 1".to_string(),
1562 spec: None,
1563 priority: None,
1564 children: None,
1565 depends_on: None,
1566 task_id: None,
1567 status: None,
1568 active_form: None,
1569 parent_id: None,
1570 },
1571 TaskTree {
1572 name: "Task 2".to_string(),
1573 spec: None,
1574 priority: None,
1575 children: None,
1576 depends_on: None,
1577 task_id: None,
1578 status: None,
1579 active_form: None,
1580 parent_id: None,
1581 },
1582 ];
1583
1584 let duplicates = find_duplicate_names(&tasks);
1585 assert_eq!(duplicates.len(), 0);
1586 }
1587
1588 #[test]
1589 fn test_find_duplicate_names_with_duplicates() {
1590 let tasks = vec![
1591 TaskTree {
1592 name: "Duplicate".to_string(),
1593 spec: None,
1594 priority: None,
1595 children: None,
1596 depends_on: None,
1597 task_id: None,
1598 status: None,
1599 active_form: None,
1600 parent_id: None,
1601 },
1602 TaskTree {
1603 name: "Unique".to_string(),
1604 spec: None,
1605 priority: None,
1606 children: None,
1607 depends_on: None,
1608 task_id: None,
1609 status: None,
1610 active_form: None,
1611 parent_id: None,
1612 },
1613 TaskTree {
1614 name: "Duplicate".to_string(),
1615 spec: None,
1616 priority: None,
1617 children: None,
1618 depends_on: None,
1619 task_id: None,
1620 status: None,
1621 active_form: None,
1622 parent_id: None,
1623 },
1624 ];
1625
1626 let duplicates = find_duplicate_names(&tasks);
1627 assert_eq!(duplicates.len(), 1);
1628 assert_eq!(duplicates[0], "Duplicate");
1629 }
1630
1631 #[test]
1632 fn test_find_duplicate_names_nested() {
1633 let tasks = vec![TaskTree {
1634 name: "Parent".to_string(),
1635 spec: None,
1636 priority: None,
1637 children: Some(vec![TaskTree {
1638 name: "Parent".to_string(), spec: None,
1640 priority: None,
1641 children: None,
1642 depends_on: None,
1643 task_id: None,
1644 status: None,
1645 active_form: None,
1646 parent_id: None,
1647 }]),
1648 depends_on: None,
1649 task_id: None,
1650 status: None,
1651 active_form: None,
1652 parent_id: None,
1653 }];
1654
1655 let duplicates = find_duplicate_names(&tasks);
1656 assert_eq!(duplicates.len(), 1);
1657 assert_eq!(duplicates[0], "Parent");
1658 }
1659
1660 #[test]
1661 fn test_flatten_task_tree_empty() {
1662 let tasks: Vec<TaskTree> = vec![];
1663 let flat = flatten_task_tree(&tasks);
1664 assert_eq!(flat.len(), 0);
1665 }
1666
1667 #[test]
1668 fn test_flatten_task_tree_deep_nesting() {
1669 let tasks = vec![TaskTree {
1671 name: "Root".to_string(),
1672 spec: None,
1673 priority: None,
1674 children: Some(vec![TaskTree {
1675 name: "Level1".to_string(),
1676 spec: None,
1677 priority: None,
1678 children: Some(vec![TaskTree {
1679 name: "Level2".to_string(),
1680 spec: None,
1681 priority: None,
1682 children: Some(vec![TaskTree {
1683 name: "Level3".to_string(),
1684 spec: None,
1685 priority: None,
1686 children: None,
1687 depends_on: None,
1688 task_id: None,
1689 status: None,
1690 active_form: None,
1691 parent_id: None,
1692 }]),
1693 depends_on: None,
1694 task_id: None,
1695 status: None,
1696 active_form: None,
1697 parent_id: None,
1698 }]),
1699 depends_on: None,
1700 task_id: None,
1701 status: None,
1702 active_form: None,
1703 parent_id: None,
1704 }]),
1705 depends_on: None,
1706 task_id: None,
1707 status: None,
1708 active_form: None,
1709 parent_id: None,
1710 }];
1711
1712 let flat = flatten_task_tree(&tasks);
1713 assert_eq!(flat.len(), 4);
1714
1715 assert_eq!(flat[0].name, "Root");
1717 assert_eq!(flat[0].parent_name, None);
1718
1719 assert_eq!(flat[1].name, "Level1");
1720 assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1721
1722 assert_eq!(flat[2].name, "Level2");
1723 assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1724
1725 assert_eq!(flat[3].name, "Level3");
1726 assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1727 }
1728
1729 #[test]
1730 fn test_flatten_task_tree_many_siblings() {
1731 let children: Vec<TaskTree> = (0..10)
1732 .map(|i| TaskTree {
1733 name: format!("Child {}", i),
1734 spec: None,
1735 priority: None,
1736 children: None,
1737 depends_on: None,
1738 task_id: None,
1739 status: None,
1740 active_form: None,
1741 parent_id: None,
1742 })
1743 .collect();
1744
1745 let tasks = vec![TaskTree {
1746 name: "Parent".to_string(),
1747 spec: None,
1748 priority: None,
1749 children: Some(children),
1750 depends_on: None,
1751 task_id: None,
1752 status: None,
1753 active_form: None,
1754 parent_id: None,
1755 }];
1756
1757 let flat = flatten_task_tree(&tasks);
1758 assert_eq!(flat.len(), 11); for child in flat.iter().skip(1).take(10) {
1762 assert_eq!(child.parent_name, Some("Parent".to_string()));
1763 }
1764 }
1765
1766 #[test]
1767 fn test_flatten_task_tree_complex_mixed() {
1768 let tasks = vec![
1770 TaskTree {
1771 name: "Task 1".to_string(),
1772 spec: None,
1773 priority: None,
1774 children: Some(vec![
1775 TaskTree {
1776 name: "Task 1.1".to_string(),
1777 spec: None,
1778 priority: None,
1779 children: None,
1780 depends_on: None,
1781 task_id: None,
1782 status: None,
1783 active_form: None,
1784 parent_id: None,
1785 },
1786 TaskTree {
1787 name: "Task 1.2".to_string(),
1788 spec: None,
1789 priority: None,
1790 children: Some(vec![TaskTree {
1791 name: "Task 1.2.1".to_string(),
1792 spec: None,
1793 priority: None,
1794 children: None,
1795 depends_on: None,
1796 task_id: None,
1797 status: None,
1798 active_form: None,
1799 parent_id: None,
1800 }]),
1801 depends_on: None,
1802 task_id: None,
1803 status: None,
1804 active_form: None,
1805 parent_id: None,
1806 },
1807 ]),
1808 depends_on: None,
1809 task_id: None,
1810 status: None,
1811 active_form: None,
1812 parent_id: None,
1813 },
1814 TaskTree {
1815 name: "Task 2".to_string(),
1816 spec: None,
1817 priority: None,
1818 children: None,
1819 depends_on: Some(vec!["Task 1".to_string()]),
1820 task_id: None,
1821 status: None,
1822 active_form: None,
1823 parent_id: None,
1824 },
1825 ];
1826
1827 let flat = flatten_task_tree(&tasks);
1828 assert_eq!(flat.len(), 5);
1829
1830 assert_eq!(flat[0].name, "Task 1");
1832 assert_eq!(flat[0].parent_name, None);
1833
1834 assert_eq!(flat[1].name, "Task 1.1");
1835 assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
1836
1837 assert_eq!(flat[2].name, "Task 1.2");
1838 assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
1839
1840 assert_eq!(flat[3].name, "Task 1.2.1");
1841 assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
1842
1843 assert_eq!(flat[4].name, "Task 2");
1844 assert_eq!(flat[4].parent_name, None);
1845 assert_eq!(flat[4].depends_on, vec!["Task 1"]);
1846 }
1847
1848 #[tokio::test]
1849 async fn test_plan_executor_integration() {
1850 use crate::test_utils::test_helpers::TestContext;
1851
1852 let ctx = TestContext::new().await;
1853
1854 let request = PlanRequest {
1856 tasks: vec![TaskTree {
1857 name: "Integration Test Plan".to_string(),
1858 spec: Some("Test plan execution end-to-end".to_string()),
1859 priority: Some(PriorityValue::High),
1860 children: Some(vec![
1861 TaskTree {
1862 name: "Subtask A".to_string(),
1863 spec: Some("First subtask".to_string()),
1864 priority: None,
1865 children: None,
1866 depends_on: None,
1867 task_id: None,
1868 status: None,
1869 active_form: None,
1870 parent_id: None,
1871 },
1872 TaskTree {
1873 name: "Subtask B".to_string(),
1874 spec: Some("Second subtask depends on A".to_string()),
1875 priority: None,
1876 children: None,
1877 depends_on: Some(vec!["Subtask A".to_string()]),
1878 task_id: None,
1879 status: None,
1880 active_form: None,
1881 parent_id: None,
1882 },
1883 ]),
1884 depends_on: None,
1885 task_id: None,
1886 status: None,
1887 active_form: None,
1888 parent_id: None,
1889 }],
1890 };
1891
1892 let executor = PlanExecutor::new(&ctx.pool);
1894 let result = executor.execute(&request).await.unwrap();
1895
1896 assert!(result.success, "Plan execution should succeed");
1898 assert_eq!(result.created_count, 3, "Should create 3 tasks");
1899 assert_eq!(result.updated_count, 0, "Should not update any tasks");
1900 assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
1901 assert!(result.error.is_none(), "Should have no error");
1902
1903 assert_eq!(result.task_id_map.len(), 3);
1905 assert!(result.task_id_map.contains_key("Integration Test Plan"));
1906 assert!(result.task_id_map.contains_key("Subtask A"));
1907 assert!(result.task_id_map.contains_key("Subtask B"));
1908
1909 let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
1911 let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
1912 let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
1913
1914 let parent: (String, String, i64, Option<i64>) =
1916 sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
1917 .bind(parent_id)
1918 .fetch_one(&ctx.pool)
1919 .await
1920 .unwrap();
1921
1922 assert_eq!(parent.0, "Integration Test Plan");
1923 assert_eq!(parent.1, "Test plan execution end-to-end");
1924 assert_eq!(parent.2, 2); assert_eq!(parent.3, None); let subtask_a: (String, Option<i64>) =
1929 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
1930 .bind(subtask_a_id)
1931 .fetch_one(&ctx.pool)
1932 .await
1933 .unwrap();
1934
1935 assert_eq!(subtask_a.0, "Subtask A");
1936 assert_eq!(subtask_a.1, Some(parent_id)); let dep: (i64, i64) = sqlx::query_as(
1940 "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
1941 )
1942 .bind(subtask_b_id)
1943 .fetch_one(&ctx.pool)
1944 .await
1945 .unwrap();
1946
1947 assert_eq!(dep.0, subtask_a_id); assert_eq!(dep.1, subtask_b_id); }
1950
1951 #[tokio::test]
1952 async fn test_plan_executor_idempotency() {
1953 use crate::test_utils::test_helpers::TestContext;
1954
1955 let ctx = TestContext::new().await;
1956
1957 let request = PlanRequest {
1959 tasks: vec![TaskTree {
1960 name: "Idempotent Task".to_string(),
1961 spec: Some("Initial spec".to_string()),
1962 priority: Some(PriorityValue::High),
1963 children: Some(vec![
1964 TaskTree {
1965 name: "Child 1".to_string(),
1966 spec: Some("Child spec 1".to_string()),
1967 priority: None,
1968 children: None,
1969 depends_on: None,
1970 task_id: None,
1971 status: None,
1972 active_form: None,
1973 parent_id: None,
1974 },
1975 TaskTree {
1976 name: "Child 2".to_string(),
1977 spec: Some("Child spec 2".to_string()),
1978 priority: Some(PriorityValue::Low),
1979 children: None,
1980 depends_on: None,
1981 task_id: None,
1982 status: None,
1983 active_form: None,
1984 parent_id: None,
1985 },
1986 ]),
1987 depends_on: None,
1988 task_id: None,
1989 status: None,
1990 active_form: None,
1991 parent_id: None,
1992 }],
1993 };
1994
1995 let executor = PlanExecutor::new(&ctx.pool);
1996
1997 let result1 = executor.execute(&request).await.unwrap();
1999 assert!(result1.success, "First execution should succeed");
2000 assert_eq!(result1.created_count, 3, "Should create 3 tasks");
2001 assert_eq!(result1.updated_count, 0, "Should not update any tasks");
2002 assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
2003
2004 let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
2006 let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
2007 let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
2008
2009 let result2 = executor.execute(&request).await.unwrap();
2011 assert!(result2.success, "Second execution should succeed");
2012 assert_eq!(result2.created_count, 0, "Should not create any new tasks");
2013 assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
2014 assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
2015
2016 let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
2018 let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
2019 let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
2020
2021 assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
2022 assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
2023 assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
2024
2025 let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
2027 .bind(parent_id_2)
2028 .fetch_one(&ctx.pool)
2029 .await
2030 .unwrap();
2031
2032 assert_eq!(parent.0, "Initial spec");
2033 assert_eq!(parent.1, 2); let modified_request = PlanRequest {
2037 tasks: vec![TaskTree {
2038 name: "Idempotent Task".to_string(),
2039 spec: Some("Updated spec".to_string()), priority: Some(PriorityValue::Critical), children: Some(vec![
2042 TaskTree {
2043 name: "Child 1".to_string(),
2044 spec: Some("Updated child spec 1".to_string()), priority: None,
2046 children: None,
2047 depends_on: None,
2048 task_id: None,
2049 status: None,
2050 active_form: None,
2051 parent_id: None,
2052 },
2053 TaskTree {
2054 name: "Child 2".to_string(),
2055 spec: Some("Child spec 2".to_string()), priority: Some(PriorityValue::Low),
2057 children: None,
2058 depends_on: None,
2059 task_id: None,
2060 status: None,
2061 active_form: None,
2062 parent_id: None,
2063 },
2064 ]),
2065 depends_on: None,
2066 task_id: None,
2067 status: None,
2068 active_form: None,
2069 parent_id: None,
2070 }],
2071 };
2072
2073 let result3 = executor.execute(&modified_request).await.unwrap();
2074 assert!(result3.success, "Third execution should succeed");
2075 assert_eq!(result3.created_count, 0, "Should not create any new tasks");
2076 assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
2077
2078 let updated_parent: (String, i64) =
2080 sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
2081 .bind(parent_id_2)
2082 .fetch_one(&ctx.pool)
2083 .await
2084 .unwrap();
2085
2086 assert_eq!(updated_parent.0, "Updated spec");
2087 assert_eq!(updated_parent.1, 1); let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
2090 .bind(child1_id_2)
2091 .fetch_one(&ctx.pool)
2092 .await
2093 .unwrap();
2094
2095 assert_eq!(updated_child1.0, "Updated child spec 1");
2096 }
2097
2098 #[tokio::test]
2099 async fn test_plan_executor_dependencies() {
2100 use crate::test_utils::test_helpers::TestContext;
2101
2102 let ctx = TestContext::new().await;
2103
2104 let request = PlanRequest {
2106 tasks: vec![
2107 TaskTree {
2108 name: "Foundation".to_string(),
2109 spec: Some("Base layer".to_string()),
2110 priority: Some(PriorityValue::Critical),
2111 children: None,
2112 depends_on: None,
2113 task_id: None,
2114 status: None,
2115 active_form: None,
2116 parent_id: None,
2117 },
2118 TaskTree {
2119 name: "Layer 1".to_string(),
2120 spec: Some("Depends on Foundation".to_string()),
2121 priority: Some(PriorityValue::High),
2122 children: None,
2123 depends_on: Some(vec!["Foundation".to_string()]),
2124 task_id: None,
2125 status: None,
2126 active_form: None,
2127 parent_id: None,
2128 },
2129 TaskTree {
2130 name: "Layer 2".to_string(),
2131 spec: Some("Depends on Layer 1".to_string()),
2132 priority: None,
2133 children: None,
2134 depends_on: Some(vec!["Layer 1".to_string()]),
2135 task_id: None,
2136 status: None,
2137 active_form: None,
2138 parent_id: None,
2139 },
2140 TaskTree {
2141 name: "Integration".to_string(),
2142 spec: Some("Depends on both Foundation and Layer 2".to_string()),
2143 priority: None,
2144 children: None,
2145 depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
2146 task_id: None,
2147 status: None,
2148 active_form: None,
2149 parent_id: None,
2150 },
2151 ],
2152 };
2153
2154 let executor = PlanExecutor::new(&ctx.pool);
2155 let result = executor.execute(&request).await.unwrap();
2156
2157 assert!(result.success, "Plan execution should succeed");
2158 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2159 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2160
2161 let foundation_id = *result.task_id_map.get("Foundation").unwrap();
2163 let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
2164 let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
2165 let integration_id = *result.task_id_map.get("Integration").unwrap();
2166
2167 let deps1: Vec<(i64,)> =
2169 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
2170 .bind(layer1_id)
2171 .fetch_all(&ctx.pool)
2172 .await
2173 .unwrap();
2174
2175 assert_eq!(deps1.len(), 1);
2176 assert_eq!(deps1[0].0, foundation_id);
2177
2178 let deps2: Vec<(i64,)> =
2180 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
2181 .bind(layer2_id)
2182 .fetch_all(&ctx.pool)
2183 .await
2184 .unwrap();
2185
2186 assert_eq!(deps2.len(), 1);
2187 assert_eq!(deps2[0].0, layer1_id);
2188
2189 let deps3: Vec<(i64,)> =
2191 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
2192 .bind(integration_id)
2193 .fetch_all(&ctx.pool)
2194 .await
2195 .unwrap();
2196
2197 assert_eq!(deps3.len(), 2);
2198 let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
2199 blocking_ids.sort();
2200
2201 let mut expected_ids = vec![foundation_id, layer2_id];
2202 expected_ids.sort();
2203
2204 assert_eq!(blocking_ids, expected_ids);
2205 }
2206
2207 #[tokio::test]
2208 async fn test_plan_executor_invalid_dependency() {
2209 use crate::test_utils::test_helpers::TestContext;
2210
2211 let ctx = TestContext::new().await;
2212
2213 let request = PlanRequest {
2215 tasks: vec![TaskTree {
2216 name: "Task A".to_string(),
2217 spec: Some("Depends on non-existent task".to_string()),
2218 priority: None,
2219 children: None,
2220 depends_on: Some(vec!["NonExistent".to_string()]),
2221 task_id: None,
2222 status: None,
2223 active_form: None,
2224 parent_id: None,
2225 }],
2226 };
2227
2228 let executor = PlanExecutor::new(&ctx.pool);
2229 let result = executor.execute(&request).await.unwrap();
2230
2231 assert!(!result.success, "Plan execution should fail");
2232 assert!(result.error.is_some(), "Should have error message");
2233 let error = result.error.unwrap();
2234 assert!(
2235 error.contains("NonExistent"),
2236 "Error should mention the missing dependency: {}",
2237 error
2238 );
2239 }
2240
2241 #[tokio::test]
2242 async fn test_plan_executor_simple_cycle() {
2243 use crate::test_utils::test_helpers::TestContext;
2244
2245 let ctx = TestContext::new().await;
2246
2247 let request = PlanRequest {
2249 tasks: vec![
2250 TaskTree {
2251 name: "Task A".to_string(),
2252 spec: Some("Depends on B".to_string()),
2253 priority: None,
2254 children: None,
2255 depends_on: Some(vec!["Task B".to_string()]),
2256 task_id: None,
2257 status: None,
2258 active_form: None,
2259 parent_id: None,
2260 },
2261 TaskTree {
2262 name: "Task B".to_string(),
2263 spec: Some("Depends on A".to_string()),
2264 priority: None,
2265 children: None,
2266 depends_on: Some(vec!["Task A".to_string()]),
2267 task_id: None,
2268 status: None,
2269 active_form: None,
2270 parent_id: None,
2271 },
2272 ],
2273 };
2274
2275 let executor = PlanExecutor::new(&ctx.pool);
2276 let result = executor.execute(&request).await.unwrap();
2277
2278 assert!(!result.success, "Plan execution should fail");
2279 assert!(result.error.is_some(), "Should have error message");
2280 let error = result.error.unwrap();
2281 assert!(
2282 error.contains("Circular dependency"),
2283 "Error should mention circular dependency: {}",
2284 error
2285 );
2286 assert!(
2287 error.contains("Task A") && error.contains("Task B"),
2288 "Error should mention both tasks in the cycle: {}",
2289 error
2290 );
2291 }
2292
2293 #[tokio::test]
2294 async fn test_plan_executor_complex_cycle() {
2295 use crate::test_utils::test_helpers::TestContext;
2296
2297 let ctx = TestContext::new().await;
2298
2299 let request = PlanRequest {
2301 tasks: vec![
2302 TaskTree {
2303 name: "Task A".to_string(),
2304 spec: Some("Depends on B".to_string()),
2305 priority: None,
2306 children: None,
2307 depends_on: Some(vec!["Task B".to_string()]),
2308 task_id: None,
2309 status: None,
2310 active_form: None,
2311 parent_id: None,
2312 },
2313 TaskTree {
2314 name: "Task B".to_string(),
2315 spec: Some("Depends on C".to_string()),
2316 priority: None,
2317 children: None,
2318 depends_on: Some(vec!["Task C".to_string()]),
2319 task_id: None,
2320 status: None,
2321 active_form: None,
2322 parent_id: None,
2323 },
2324 TaskTree {
2325 name: "Task C".to_string(),
2326 spec: Some("Depends on A".to_string()),
2327 priority: None,
2328 children: None,
2329 depends_on: Some(vec!["Task A".to_string()]),
2330 task_id: None,
2331 status: None,
2332 active_form: None,
2333 parent_id: None,
2334 },
2335 ],
2336 };
2337
2338 let executor = PlanExecutor::new(&ctx.pool);
2339 let result = executor.execute(&request).await.unwrap();
2340
2341 assert!(!result.success, "Plan execution should fail");
2342 assert!(result.error.is_some(), "Should have error message");
2343 let error = result.error.unwrap();
2344 assert!(
2345 error.contains("Circular dependency"),
2346 "Error should mention circular dependency: {}",
2347 error
2348 );
2349 assert!(
2350 error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
2351 "Error should mention all tasks in the cycle: {}",
2352 error
2353 );
2354 }
2355
2356 #[tokio::test]
2357 async fn test_plan_executor_valid_dag() {
2358 use crate::test_utils::test_helpers::TestContext;
2359
2360 let ctx = TestContext::new().await;
2361
2362 let request = PlanRequest {
2369 tasks: vec![
2370 TaskTree {
2371 name: "Task A".to_string(),
2372 spec: Some("Root task".to_string()),
2373 priority: None,
2374 children: None,
2375 depends_on: None,
2376 task_id: None,
2377 status: None,
2378 active_form: None,
2379 parent_id: None,
2380 },
2381 TaskTree {
2382 name: "Task B".to_string(),
2383 spec: Some("Depends on A".to_string()),
2384 priority: None,
2385 children: None,
2386 depends_on: Some(vec!["Task A".to_string()]),
2387 task_id: None,
2388 status: None,
2389 active_form: None,
2390 parent_id: None,
2391 },
2392 TaskTree {
2393 name: "Task C".to_string(),
2394 spec: Some("Depends on A".to_string()),
2395 priority: None,
2396 children: None,
2397 depends_on: Some(vec!["Task A".to_string()]),
2398 task_id: None,
2399 status: None,
2400 active_form: None,
2401 parent_id: None,
2402 },
2403 TaskTree {
2404 name: "Task D".to_string(),
2405 spec: Some("Depends on B and C".to_string()),
2406 priority: None,
2407 children: None,
2408 depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
2409 task_id: None,
2410 status: None,
2411 active_form: None,
2412 parent_id: None,
2413 },
2414 ],
2415 };
2416
2417 let executor = PlanExecutor::new(&ctx.pool);
2418 let result = executor.execute(&request).await.unwrap();
2419
2420 assert!(
2421 result.success,
2422 "Plan execution should succeed for valid DAG"
2423 );
2424 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2425 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2426 }
2427
2428 #[tokio::test]
2429 async fn test_plan_executor_self_dependency() {
2430 use crate::test_utils::test_helpers::TestContext;
2431
2432 let ctx = TestContext::new().await;
2433
2434 let request = PlanRequest {
2436 tasks: vec![TaskTree {
2437 name: "Task A".to_string(),
2438 spec: Some("Depends on itself".to_string()),
2439 priority: None,
2440 children: None,
2441 depends_on: Some(vec!["Task A".to_string()]),
2442 task_id: None,
2443 status: None,
2444 active_form: None,
2445 parent_id: None,
2446 }],
2447 };
2448
2449 let executor = PlanExecutor::new(&ctx.pool);
2450 let result = executor.execute(&request).await.unwrap();
2451
2452 assert!(
2453 !result.success,
2454 "Plan execution should fail for self-dependency"
2455 );
2456 assert!(result.error.is_some(), "Should have error message");
2457 let error = result.error.unwrap();
2458 assert!(
2459 error.contains("Circular dependency"),
2460 "Error should mention circular dependency: {}",
2461 error
2462 );
2463 }
2464
2465 #[tokio::test]
2467 async fn test_find_tasks_by_names_empty() {
2468 use crate::test_utils::test_helpers::TestContext;
2469
2470 let ctx = TestContext::new().await;
2471 let executor = PlanExecutor::new(&ctx.pool);
2472
2473 let result = executor.find_tasks_by_names(&[]).await.unwrap();
2474 assert!(result.is_empty(), "Empty input should return empty map");
2475 }
2476
2477 #[tokio::test]
2478 async fn test_find_tasks_by_names_partial() {
2479 use crate::test_utils::test_helpers::TestContext;
2480
2481 let ctx = TestContext::new().await;
2482 let executor = PlanExecutor::new(&ctx.pool);
2483
2484 let request = PlanRequest {
2486 tasks: vec![
2487 TaskTree {
2488 name: "Task A".to_string(),
2489 spec: None,
2490 priority: None,
2491 children: None,
2492 depends_on: None,
2493 task_id: None,
2494 status: None,
2495 active_form: None,
2496 parent_id: None,
2497 },
2498 TaskTree {
2499 name: "Task B".to_string(),
2500 spec: None,
2501 priority: None,
2502 children: None,
2503 depends_on: None,
2504 task_id: None,
2505 status: None,
2506 active_form: None,
2507 parent_id: None,
2508 },
2509 ],
2510 };
2511 executor.execute(&request).await.unwrap();
2512
2513 let names = vec![
2515 "Task A".to_string(),
2516 "Task B".to_string(),
2517 "Task C".to_string(),
2518 ];
2519 let result = executor.find_tasks_by_names(&names).await.unwrap();
2520
2521 assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
2522 assert!(result.contains_key("Task A"));
2523 assert!(result.contains_key("Task B"));
2524 assert!(!result.contains_key("Task C"));
2525 }
2526
2527 #[tokio::test]
2529 async fn test_plan_1000_tasks_performance() {
2530 use crate::test_utils::test_helpers::TestContext;
2531
2532 let ctx = TestContext::new().await;
2533 let executor = PlanExecutor::new(&ctx.pool);
2534
2535 let mut tasks = Vec::new();
2537 for i in 0..1000 {
2538 tasks.push(TaskTree {
2539 name: format!("Task {}", i),
2540 spec: Some(format!("Spec for task {}", i)),
2541 priority: Some(PriorityValue::Medium),
2542 children: None,
2543 depends_on: None,
2544 task_id: None,
2545 status: None,
2546 active_form: None,
2547 parent_id: None,
2548 });
2549 }
2550
2551 let request = PlanRequest { tasks };
2552
2553 let start = std::time::Instant::now();
2554 let result = executor.execute(&request).await.unwrap();
2555 let duration = start.elapsed();
2556
2557 assert!(result.success);
2558 assert_eq!(result.created_count, 1000);
2559 assert!(
2560 duration.as_secs() < 10,
2561 "Should complete 1000 tasks in under 10 seconds, took {:?}",
2562 duration
2563 );
2564
2565 println!("✅ Created 1000 tasks in {:?}", duration);
2566 }
2567
2568 #[tokio::test]
2569 async fn test_plan_deep_nesting_20_levels() {
2570 use crate::test_utils::test_helpers::TestContext;
2571
2572 let ctx = TestContext::new().await;
2573 let executor = PlanExecutor::new(&ctx.pool);
2574
2575 fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
2577 TaskTree {
2578 name: format!("Level {}", current),
2579 spec: Some(format!("Task at depth {}", current)),
2580 priority: Some(PriorityValue::Low),
2581 children: if current < depth {
2582 Some(vec![build_deep_tree(depth, current + 1)])
2583 } else {
2584 None
2585 },
2586 depends_on: None,
2587 task_id: None,
2588 status: None,
2589 active_form: None,
2590 parent_id: None,
2591 }
2592 }
2593
2594 let request = PlanRequest {
2595 tasks: vec![build_deep_tree(20, 1)],
2596 };
2597
2598 let start = std::time::Instant::now();
2599 let result = executor.execute(&request).await.unwrap();
2600 let duration = start.elapsed();
2601
2602 assert!(result.success);
2603 assert_eq!(
2604 result.created_count, 20,
2605 "Should create 20 tasks (1 per level)"
2606 );
2607 assert!(
2608 duration.as_secs() < 5,
2609 "Should handle 20-level nesting in under 5 seconds, took {:?}",
2610 duration
2611 );
2612
2613 println!("✅ Created 20-level deep tree in {:?}", duration);
2614 }
2615
2616 #[test]
2617 fn test_flatten_preserves_all_fields() {
2618 let tasks = vec![TaskTree {
2619 name: "Full Task".to_string(),
2620 spec: Some("Detailed spec".to_string()),
2621 priority: Some(PriorityValue::Critical),
2622 children: None,
2623 depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2624 task_id: Some(42),
2625 status: None,
2626 active_form: None,
2627 parent_id: None,
2628 }];
2629
2630 let flat = flatten_task_tree(&tasks);
2631 assert_eq!(flat.len(), 1);
2632
2633 let task = &flat[0];
2634 assert_eq!(task.name, "Full Task");
2635 assert_eq!(task.spec, Some("Detailed spec".to_string()));
2636 assert_eq!(task.priority, Some(PriorityValue::Critical));
2637 assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2638 assert_eq!(task.task_id, Some(42));
2639 }
2640}
2641
2642#[cfg(test)]
2643mod dataflow_tests {
2644 use super::*;
2645 use crate::tasks::TaskManager;
2646 use crate::test_utils::test_helpers::TestContext;
2647
2648 #[tokio::test]
2649 async fn test_complete_dataflow_status_and_active_form() {
2650 let ctx = TestContext::new().await;
2652
2653 let request = PlanRequest {
2655 tasks: vec![TaskTree {
2656 name: "Test Active Form Task".to_string(),
2657 spec: Some("Testing complete dataflow".to_string()),
2658 priority: Some(PriorityValue::High),
2659 children: None,
2660 depends_on: None,
2661 task_id: None,
2662 status: Some(TaskStatus::Doing),
2663 active_form: Some("Testing complete dataflow now".to_string()),
2664 parent_id: None,
2665 }],
2666 };
2667
2668 let executor = PlanExecutor::new(&ctx.pool);
2669 let result = executor.execute(&request).await.unwrap();
2670
2671 assert!(result.success);
2672 assert_eq!(result.created_count, 1);
2673
2674 let task_mgr = TaskManager::new(&ctx.pool);
2676 let result = task_mgr
2677 .find_tasks(None, None, None, None, None)
2678 .await
2679 .unwrap();
2680
2681 assert_eq!(result.tasks.len(), 1);
2682 let task = &result.tasks[0];
2683
2684 assert_eq!(task.name, "Test Active Form Task");
2686 assert_eq!(task.status, "doing"); assert_eq!(
2688 task.active_form,
2689 Some("Testing complete dataflow now".to_string())
2690 );
2691
2692 let json = serde_json::to_value(task).unwrap();
2694 assert_eq!(json["name"], "Test Active Form Task");
2695 assert_eq!(json["status"], "doing");
2696 assert_eq!(json["active_form"], "Testing complete dataflow now");
2697
2698 println!("✅ 完整数据流验证成功!");
2699 println!(" Plan工具写入 -> Task读取 -> JSON序列化 -> MCP输出");
2700 println!(" active_form: {:?}", task.active_form);
2701 }
2702}
2703
2704#[cfg(test)]
2705mod parent_id_tests {
2706 use super::*;
2707 use crate::test_utils::test_helpers::TestContext;
2708
2709 #[test]
2710 fn test_parent_id_json_deserialization_absent() {
2711 let json = r#"{"name": "Test Task"}"#;
2713 let task: TaskTree = serde_json::from_str(json).unwrap();
2714 assert_eq!(task.parent_id, None);
2715 }
2716
2717 #[test]
2718 fn test_parent_id_json_deserialization_null() {
2719 let json = r#"{"name": "Test Task", "parent_id": null}"#;
2721 let task: TaskTree = serde_json::from_str(json).unwrap();
2722 assert_eq!(task.parent_id, Some(None));
2723 }
2724
2725 #[test]
2726 fn test_parent_id_json_deserialization_number() {
2727 let json = r#"{"name": "Test Task", "parent_id": 42}"#;
2729 let task: TaskTree = serde_json::from_str(json).unwrap();
2730 assert_eq!(task.parent_id, Some(Some(42)));
2731 }
2732
2733 #[test]
2734 fn test_flatten_propagates_parent_id() {
2735 let tasks = vec![TaskTree {
2736 name: "Task with explicit parent".to_string(),
2737 spec: None,
2738 priority: None,
2739 children: None,
2740 depends_on: None,
2741 task_id: None,
2742 status: None,
2743 active_form: None,
2744 parent_id: Some(Some(99)),
2745 }];
2746
2747 let flat = flatten_task_tree(&tasks);
2748 assert_eq!(flat.len(), 1);
2749 assert_eq!(flat[0].explicit_parent_id, Some(Some(99)));
2750 }
2751
2752 #[test]
2753 fn test_flatten_propagates_null_parent_id() {
2754 let tasks = vec![TaskTree {
2755 name: "Explicit root task".to_string(),
2756 spec: None,
2757 priority: None,
2758 children: None,
2759 depends_on: None,
2760 task_id: None,
2761 status: None,
2762 active_form: None,
2763 parent_id: Some(None), }];
2765
2766 let flat = flatten_task_tree(&tasks);
2767 assert_eq!(flat.len(), 1);
2768 assert_eq!(flat[0].explicit_parent_id, Some(None));
2769 }
2770
2771 #[tokio::test]
2772 async fn test_explicit_parent_id_sets_parent() {
2773 let ctx = TestContext::new().await;
2774
2775 let request1 = PlanRequest {
2777 tasks: vec![TaskTree {
2778 name: "Parent Task".to_string(),
2779 spec: Some("This is the parent".to_string()),
2780 priority: None,
2781 children: None,
2782 depends_on: None,
2783 task_id: None,
2784 status: Some(TaskStatus::Doing),
2785 active_form: None,
2786 parent_id: None,
2787 }],
2788 };
2789
2790 let executor = PlanExecutor::new(&ctx.pool);
2791 let result1 = executor.execute(&request1).await.unwrap();
2792 assert!(result1.success);
2793 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
2794
2795 let request2 = PlanRequest {
2797 tasks: vec![TaskTree {
2798 name: "Child Task".to_string(),
2799 spec: Some("This uses explicit parent_id".to_string()),
2800 priority: None,
2801 children: None,
2802 depends_on: None,
2803 task_id: None,
2804 status: None,
2805 active_form: None,
2806 parent_id: Some(Some(parent_id)),
2807 }],
2808 };
2809
2810 let result2 = executor.execute(&request2).await.unwrap();
2811 assert!(result2.success);
2812 let child_id = *result2.task_id_map.get("Child Task").unwrap();
2813
2814 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2816 .bind(child_id)
2817 .fetch_one(&ctx.pool)
2818 .await
2819 .unwrap();
2820 assert_eq!(row.0, Some(parent_id));
2821 }
2822
2823 #[tokio::test]
2824 async fn test_explicit_null_parent_id_creates_root() {
2825 let ctx = TestContext::new().await;
2826
2827 let request = PlanRequest {
2830 tasks: vec![TaskTree {
2831 name: "Explicit Root Task".to_string(),
2832 spec: Some("Should be root despite default parent".to_string()),
2833 priority: None,
2834 children: None,
2835 depends_on: None,
2836 task_id: None,
2837 status: Some(TaskStatus::Doing),
2838 active_form: None,
2839 parent_id: Some(None), }],
2841 };
2842
2843 let parent_request = PlanRequest {
2846 tasks: vec![TaskTree {
2847 name: "Default Parent".to_string(),
2848 spec: None,
2849 priority: None,
2850 children: None,
2851 depends_on: None,
2852 task_id: None,
2853 status: None,
2854 active_form: None,
2855 parent_id: None,
2856 }],
2857 };
2858 let executor = PlanExecutor::new(&ctx.pool);
2859 let parent_result = executor.execute(&parent_request).await.unwrap();
2860 let default_parent_id = *parent_result.task_id_map.get("Default Parent").unwrap();
2861
2862 let executor_with_default =
2864 PlanExecutor::new(&ctx.pool).with_default_parent(default_parent_id);
2865 let result = executor_with_default.execute(&request).await.unwrap();
2866 assert!(result.success);
2867 let task_id = *result.task_id_map.get("Explicit Root Task").unwrap();
2868
2869 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2871 .bind(task_id)
2872 .fetch_one(&ctx.pool)
2873 .await
2874 .unwrap();
2875 assert_eq!(
2876 row.0, None,
2877 "Task with explicit null parent_id should be root"
2878 );
2879 }
2880
2881 #[tokio::test]
2882 async fn test_children_nesting_takes_precedence_over_parent_id() {
2883 let ctx = TestContext::new().await;
2884
2885 let request = PlanRequest {
2887 tasks: vec![TaskTree {
2888 name: "Parent via Nesting".to_string(),
2889 spec: Some("Test parent spec".to_string()),
2890 priority: None,
2891 children: Some(vec![TaskTree {
2892 name: "Child via Nesting".to_string(),
2893 spec: None,
2894 priority: None,
2895 children: None,
2896 depends_on: None,
2897 task_id: None,
2898 status: None,
2899 active_form: None,
2900 parent_id: Some(Some(999)), }]),
2902 depends_on: None,
2903 task_id: None,
2904 status: Some(TaskStatus::Doing),
2905 active_form: None,
2906 parent_id: None,
2907 }],
2908 };
2909
2910 let executor = PlanExecutor::new(&ctx.pool);
2911 let result = executor.execute(&request).await.unwrap();
2912 assert!(result.success);
2913
2914 let parent_id = *result.task_id_map.get("Parent via Nesting").unwrap();
2915 let child_id = *result.task_id_map.get("Child via Nesting").unwrap();
2916
2917 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2919 .bind(child_id)
2920 .fetch_one(&ctx.pool)
2921 .await
2922 .unwrap();
2923 assert_eq!(
2924 row.0,
2925 Some(parent_id),
2926 "Children nesting should take precedence"
2927 );
2928 }
2929
2930 #[tokio::test]
2931 async fn test_modify_existing_task_parent() {
2932 let ctx = TestContext::new().await;
2933 let executor = PlanExecutor::new(&ctx.pool);
2934
2935 let request1 = PlanRequest {
2937 tasks: vec![
2938 TaskTree {
2939 name: "Task A".to_string(),
2940 spec: Some("Task A spec".to_string()),
2941 priority: None,
2942 children: None,
2943 depends_on: None,
2944 task_id: None,
2945 status: Some(TaskStatus::Doing),
2946 active_form: None,
2947 parent_id: None,
2948 },
2949 TaskTree {
2950 name: "Task B".to_string(),
2951 spec: None,
2952 priority: None,
2953 children: None,
2954 depends_on: None,
2955 task_id: None,
2956 status: None,
2957 active_form: None,
2958 parent_id: None,
2959 },
2960 ],
2961 };
2962
2963 let result1 = executor.execute(&request1).await.unwrap();
2964 assert!(result1.success);
2965 let task_a_id = *result1.task_id_map.get("Task A").unwrap();
2966 let task_b_id = *result1.task_id_map.get("Task B").unwrap();
2967
2968 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2970 .bind(task_b_id)
2971 .fetch_one(&ctx.pool)
2972 .await
2973 .unwrap();
2974 assert_eq!(row.0, None, "Task B should initially be root");
2975
2976 let request2 = PlanRequest {
2978 tasks: vec![TaskTree {
2979 name: "Task B".to_string(), spec: None,
2981 priority: None,
2982 children: None,
2983 depends_on: None,
2984 task_id: None,
2985 status: None,
2986 active_form: None,
2987 parent_id: Some(Some(task_a_id)), }],
2989 };
2990
2991 let result2 = executor.execute(&request2).await.unwrap();
2992 assert!(result2.success);
2993 assert_eq!(result2.updated_count, 1, "Should update existing task");
2994
2995 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
2997 .bind(task_b_id)
2998 .fetch_one(&ctx.pool)
2999 .await
3000 .unwrap();
3001 assert_eq!(
3002 row.0,
3003 Some(task_a_id),
3004 "Task B should now be child of Task A"
3005 );
3006 }
3007
3008 #[tokio::test]
3009 async fn test_plan_done_with_incomplete_children_fails() {
3010 let ctx = TestContext::new().await;
3011 let executor = PlanExecutor::new(&ctx.pool);
3012
3013 let request1 = PlanRequest {
3015 tasks: vec![TaskTree {
3016 name: "Parent Task".to_string(),
3017 spec: Some("Parent spec".to_string()),
3018 priority: None,
3019 children: Some(vec![TaskTree {
3020 name: "Child Task".to_string(),
3021 spec: None,
3022 priority: None,
3023 children: None,
3024 depends_on: None,
3025 task_id: None,
3026 status: Some(TaskStatus::Todo), active_form: None,
3028 parent_id: None,
3029 }]),
3030 depends_on: None,
3031 task_id: None,
3032 status: Some(TaskStatus::Doing),
3033 active_form: None,
3034 parent_id: None,
3035 }],
3036 };
3037
3038 let result1 = executor.execute(&request1).await.unwrap();
3039 assert!(result1.success);
3040
3041 let request2 = PlanRequest {
3043 tasks: vec![TaskTree {
3044 name: "Parent Task".to_string(),
3045 spec: None,
3046 priority: None,
3047 children: None,
3048 depends_on: None,
3049 task_id: None,
3050 status: Some(TaskStatus::Done), active_form: None,
3052 parent_id: None,
3053 }],
3054 };
3055
3056 let result2 = executor.execute(&request2).await.unwrap();
3057 assert!(!result2.success, "Should fail when child is incomplete");
3058 assert!(
3059 result2
3060 .error
3061 .as_ref()
3062 .unwrap()
3063 .contains("Uncompleted children"),
3064 "Error should mention uncompleted children: {:?}",
3065 result2.error
3066 );
3067 }
3068
3069 #[tokio::test]
3070 async fn test_plan_done_with_completed_children_succeeds() {
3071 let ctx = TestContext::new().await;
3072 let executor = PlanExecutor::new(&ctx.pool);
3073
3074 let request1 = PlanRequest {
3076 tasks: vec![TaskTree {
3077 name: "Parent Task".to_string(),
3078 spec: Some("Parent spec".to_string()),
3079 priority: None,
3080 children: Some(vec![TaskTree {
3081 name: "Child Task".to_string(),
3082 spec: None,
3083 priority: None,
3084 children: None,
3085 depends_on: None,
3086 task_id: None,
3087 status: Some(TaskStatus::Todo),
3088 active_form: None,
3089 parent_id: None,
3090 }]),
3091 depends_on: None,
3092 task_id: None,
3093 status: Some(TaskStatus::Doing),
3094 active_form: None,
3095 parent_id: None,
3096 }],
3097 };
3098
3099 let result1 = executor.execute(&request1).await.unwrap();
3100 assert!(result1.success);
3101
3102 let request2 = PlanRequest {
3104 tasks: vec![TaskTree {
3105 name: "Child Task".to_string(),
3106 spec: None,
3107 priority: None,
3108 children: None,
3109 depends_on: None,
3110 task_id: None,
3111 status: Some(TaskStatus::Done),
3112 active_form: None,
3113 parent_id: None,
3114 }],
3115 };
3116
3117 let result2 = executor.execute(&request2).await.unwrap();
3118 assert!(result2.success);
3119
3120 let request3 = PlanRequest {
3122 tasks: vec![TaskTree {
3123 name: "Parent Task".to_string(),
3124 spec: None,
3125 priority: None,
3126 children: None,
3127 depends_on: None,
3128 task_id: None,
3129 status: Some(TaskStatus::Done),
3130 active_form: None,
3131 parent_id: None,
3132 }],
3133 };
3134
3135 let result3 = executor.execute(&request3).await.unwrap();
3136 assert!(result3.success, "Should succeed when child is complete");
3137 }
3138}