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, Default)]
21pub struct TaskTree {
22 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub name: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub spec: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub priority: Option<PriorityValue>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub children: Option<Vec<TaskTree>>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub depends_on: Option<Vec<String>>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none", alias = "task_id")]
46 pub id: Option<i64>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub status: Option<TaskStatus>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub active_form: Option<String>,
56
57 #[serde(
62 default,
63 skip_serializing_if = "Option::is_none",
64 deserialize_with = "deserialize_parent_id"
65 )]
66 pub parent_id: Option<Option<i64>>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub delete: Option<bool>,
71}
72
73fn deserialize_parent_id<'de, D>(
79 deserializer: D,
80) -> std::result::Result<Option<Option<i64>>, D::Error>
81where
82 D: serde::Deserializer<'de>,
83{
84 let inner: Option<i64> = Option::deserialize(deserializer)?;
91 Ok(Some(inner))
92}
93
94#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97pub enum TaskStatus {
98 Todo,
99 Doing,
100 Done,
101}
102
103impl TaskStatus {
104 pub fn as_db_str(&self) -> &'static str {
106 match self {
107 TaskStatus::Todo => "todo",
108 TaskStatus::Doing => "doing",
109 TaskStatus::Done => "done",
110 }
111 }
112
113 pub fn from_db_str(s: &str) -> Option<Self> {
115 match s {
116 "todo" => Some(TaskStatus::Todo),
117 "doing" => Some(TaskStatus::Doing),
118 "done" => Some(TaskStatus::Done),
119 _ => None,
120 }
121 }
122
123 pub fn as_str(&self) -> &'static str {
125 match self {
126 TaskStatus::Todo => "todo",
127 TaskStatus::Doing => "doing",
128 TaskStatus::Done => "done",
129 }
130 }
131}
132
133#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
135#[serde(rename_all = "lowercase")]
136pub enum PriorityValue {
137 Critical,
138 High,
139 Medium,
140 Low,
141}
142
143impl PriorityValue {
144 pub fn to_int(&self) -> i32 {
146 match self {
147 PriorityValue::Critical => 1,
148 PriorityValue::High => 2,
149 PriorityValue::Medium => 3,
150 PriorityValue::Low => 4,
151 }
152 }
153
154 pub fn from_int(value: i32) -> Option<Self> {
156 match value {
157 1 => Some(PriorityValue::Critical),
158 2 => Some(PriorityValue::High),
159 3 => Some(PriorityValue::Medium),
160 4 => Some(PriorityValue::Low),
161 _ => None,
162 }
163 }
164
165 pub fn as_str(&self) -> &'static str {
167 match self {
168 PriorityValue::Critical => "critical",
169 PriorityValue::High => "high",
170 PriorityValue::Medium => "medium",
171 PriorityValue::Low => "low",
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct ExistingTaskInfo {
179 pub id: i64,
180 pub status: String,
181 pub spec: Option<String>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186pub struct PlanResult {
187 pub success: bool,
189
190 pub task_id_map: HashMap<String, i64>,
192
193 pub created_count: usize,
195
196 pub updated_count: usize,
198
199 #[serde(default, skip_serializing_if = "is_zero")]
201 pub deleted_count: usize,
202
203 #[serde(default, skip_serializing_if = "is_zero_i64")]
205 pub cascade_deleted_count: i64,
206
207 pub dependency_count: usize,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
213 pub focused_task: Option<crate::db::models::TaskWithEvents>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub error: Option<String>,
218
219 #[serde(skip_serializing_if = "Vec::is_empty", default)]
221 pub warnings: Vec<String>,
222}
223
224fn is_zero(n: &usize) -> bool {
225 *n == 0
226}
227
228fn is_zero_i64(n: &i64) -> bool {
229 *n == 0
230}
231
232impl PlanResult {
233 pub fn success(
235 task_id_map: HashMap<String, i64>,
236 created_count: usize,
237 updated_count: usize,
238 deleted_count: usize,
239 dependency_count: usize,
240 focused_task: Option<crate::db::models::TaskWithEvents>,
241 ) -> Self {
242 Self {
243 success: true,
244 task_id_map,
245 created_count,
246 updated_count,
247 deleted_count,
248 cascade_deleted_count: 0,
249 dependency_count,
250 focused_task,
251 error: None,
252 warnings: Vec::new(),
253 }
254 }
255
256 pub fn success_with_warnings(
258 task_id_map: HashMap<String, i64>,
259 created_count: usize,
260 updated_count: usize,
261 deleted_count: usize,
262 cascade_deleted_count: i64,
263 dependency_count: usize,
264 focused_task: Option<crate::db::models::TaskWithEvents>,
265 warnings: Vec<String>,
266 ) -> Self {
267 Self {
268 success: true,
269 task_id_map,
270 created_count,
271 updated_count,
272 deleted_count,
273 cascade_deleted_count,
274 dependency_count,
275 focused_task,
276 error: None,
277 warnings,
278 }
279 }
280
281 pub fn error(message: impl Into<String>) -> Self {
283 Self {
284 success: false,
285 task_id_map: HashMap::new(),
286 created_count: 0,
287 updated_count: 0,
288 deleted_count: 0,
289 cascade_deleted_count: 0,
290 dependency_count: 0,
291 focused_task: None,
292 error: Some(message.into()),
293 warnings: Vec::new(),
294 }
295 }
296}
297
298pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
305 let mut names = Vec::new();
306
307 for task in tasks {
308 if let Some(name) = &task.name {
309 names.push(name.clone());
310 }
311
312 if let Some(children) = &task.children {
313 names.extend(extract_all_names(children));
314 }
315 }
316
317 names
318}
319
320#[derive(Debug, Clone, PartialEq, Default)]
322pub struct FlatTask {
323 pub name: Option<String>,
325 pub spec: Option<String>,
326 pub priority: Option<PriorityValue>,
327 pub parent_name: Option<String>,
329 pub depends_on: Vec<String>,
330 pub id: Option<i64>,
332 pub status: Option<TaskStatus>,
333 pub active_form: Option<String>,
334 pub explicit_parent_id: Option<Option<i64>>,
339 pub delete: bool,
341}
342
343pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
344 flatten_task_tree_recursive(tasks, None)
345}
346
347fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
348 let mut flat = Vec::new();
349
350 for task in tasks {
351 let flat_task = FlatTask {
352 name: task.name.clone(),
353 spec: task.spec.clone(),
354 priority: task.priority.clone(),
355 parent_name: parent_name.clone(),
356 depends_on: task.depends_on.clone().unwrap_or_default(),
357 id: task.id,
358 status: task.status.clone(),
359 active_form: task.active_form.clone(),
360 explicit_parent_id: task.parent_id,
361 delete: task.delete.unwrap_or(false),
362 };
363
364 flat.push(flat_task);
365
366 if let Some(children) = &task.children {
368 if let Some(name) = &task.name {
369 flat.extend(flatten_task_tree_recursive(children, Some(name.clone())));
370 }
371 }
372 }
373
374 flat
375}
376
377#[derive(Debug, Clone, PartialEq)]
379pub enum Operation {
380 Create(FlatTask),
381 Update { id: i64, task: FlatTask },
382 Delete { id: i64 },
383}
384
385pub fn classify_operations(
394 flat_tasks: &[FlatTask],
395 existing_names: &HashMap<String, i64>,
396) -> Vec<Operation> {
397 let mut operations = Vec::new();
398
399 for task in flat_tasks {
400 if task.delete {
402 if let Some(id) = task.id {
403 operations.push(Operation::Delete { id });
404 }
405 continue;
407 }
408
409 let operation = if let Some(id) = task.id {
411 Operation::Update {
413 id,
414 task: task.clone(),
415 }
416 } else if let Some(name) = &task.name {
417 if let Some(&id) = existing_names.get(name) {
419 Operation::Update {
421 id,
422 task: task.clone(),
423 }
424 } else {
425 Operation::Create(task.clone())
427 }
428 } else {
429 continue;
431 };
432
433 operations.push(operation);
434 }
435
436 operations
437}
438
439pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
441 let mut seen = HashMap::new();
442 let mut duplicates = Vec::new();
443
444 for name in extract_all_names(tasks) {
445 let count = seen.entry(name.clone()).or_insert(0);
446 *count += 1;
447 if *count == 2 {
448 duplicates.push(name);
450 }
451 }
452
453 duplicates
454}
455
456use crate::error::{IntentError, Result};
461use sqlx::SqlitePool;
462
463pub struct PlanExecutor<'a> {
465 pool: &'a SqlitePool,
466 project_path: Option<String>,
467 default_parent_id: Option<i64>,
469}
470
471impl<'a> PlanExecutor<'a> {
472 pub fn new(pool: &'a SqlitePool) -> Self {
474 Self {
475 pool,
476 project_path: None,
477 default_parent_id: None,
478 }
479 }
480
481 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
483 Self {
484 pool,
485 project_path: Some(project_path),
486 default_parent_id: None,
487 }
488 }
489
490 pub fn with_default_parent(mut self, parent_id: i64) -> Self {
493 self.default_parent_id = Some(parent_id);
494 self
495 }
496
497 fn get_task_manager(&self) -> crate::tasks::TaskManager<'a> {
499 match &self.project_path {
500 Some(path) => crate::tasks::TaskManager::with_project_path(self.pool, path.clone()),
501 None => crate::tasks::TaskManager::new(self.pool),
502 }
503 }
504
505 #[tracing::instrument(skip(self, request), fields(task_count = request.tasks.len()))]
507 pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
508 let duplicates = find_duplicate_names(&request.tasks);
510 if !duplicates.is_empty() {
511 return Ok(PlanResult::error(format!(
512 "Duplicate task names in request: {:?}",
513 duplicates
514 )));
515 }
516
517 let all_names = extract_all_names(&request.tasks);
519
520 let existing = self.find_tasks_by_names(&all_names).await?;
522
523 let flat_tasks = flatten_task_tree(&request.tasks);
525
526 if let Err(e) = self.validate_dependencies(&flat_tasks) {
528 return Ok(PlanResult::error(e.to_string()));
529 }
530
531 if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
533 return Ok(PlanResult::error(e.to_string()));
534 }
535
536 if let Err(e) = self.validate_batch_single_doing(&flat_tasks) {
538 return Ok(PlanResult::error(e.to_string()));
539 }
540
541 let task_mgr = self.get_task_manager();
543
544 let mut tx = self.pool.begin().await?;
546
547 let mut task_id_map = HashMap::new();
549 let mut created_count = 0;
550 let mut updated_count = 0;
551 let mut warnings: Vec<String> = Vec::new();
552 let mut newly_created_names: std::collections::HashSet<String> =
553 std::collections::HashSet::new();
554 let mut deleted_count = 0;
555
556 let (delete_tasks, normal_tasks): (Vec<_>, Vec<_>) =
567 flat_tasks.iter().partition(|t| t.delete);
568
569 for task in &delete_tasks {
571 if task.id.is_none() {
572 return Ok(PlanResult::error(
573 "Delete operation requires 'id' field. Use {\"id\": <task_id>, \"delete\": true}",
574 ));
575 }
576 }
577
578 for task in &delete_tasks {
591 if let Some(id) = task.id {
592 if let Some((focused_id, session_id)) =
594 task_mgr.find_focused_in_subtree_in_tx(&mut tx, id).await?
595 {
596 if focused_id == id {
597 return Ok(PlanResult::error(format!(
599 "Task #{} is the current focus of session '{}'. That session must switch focus first.",
600 id, session_id
601 )));
602 } else {
603 return Ok(PlanResult::error(format!(
605 "Task #{} is the current focus of session '{}' and would be deleted by cascade (descendant of #{}). That session must switch focus first.",
606 focused_id, session_id, id
607 )));
608 }
609 }
610 }
611 }
612
613 let mut cascade_deleted_count: i64 = 0;
617 for task in &delete_tasks {
618 if let Some(id) = task.id {
619 let delete_result = task_mgr.delete_task_in_tx(&mut tx, id).await?;
620
621 if !delete_result.found {
622 warnings.push(format!(
625 "Task #{} not found (may have been already deleted)",
626 id
627 ));
628 } else {
629 deleted_count += 1;
630
631 if delete_result.descendant_count > 0 {
633 cascade_deleted_count += delete_result.descendant_count;
634 warnings.push(format!(
635 "Task #{} had {} descendant(s) that were also deleted (cascade)",
636 id, delete_result.descendant_count
637 ));
638 }
639 }
640 }
641 }
642
643 for task in &normal_tasks {
649 let task_name = match &task.name {
651 Some(name) => name,
652 None => continue, };
654
655 let is_becoming_doing = task.status.as_ref() == Some(&TaskStatus::Doing);
657 let has_spec = task
658 .spec
659 .as_ref()
660 .map(|s| !s.trim().is_empty())
661 .unwrap_or(false);
662
663 if let Some(existing_info) = existing.get(task_name) {
664 if is_becoming_doing && !has_spec {
669 let existing_is_doing = existing_info.status == "doing";
670 let existing_has_spec = existing_info
671 .spec
672 .as_ref()
673 .map(|s| !s.trim().is_empty())
674 .unwrap_or(false);
675
676 if !existing_is_doing && !existing_has_spec {
678 return Ok(PlanResult::error(format!(
679 "Task '{}': spec (description) is required when starting a task (status: doing).\n\n\
680 Before starting a task, please describe:\n \
681 • What is the goal of this task\n \
682 • How do you plan to approach it\n\n\
683 Tip: Use @file(path) to include content from a file",
684 task_name
685 )));
686 }
687 }
688
689 let is_becoming_done = task.status.as_ref() == Some(&TaskStatus::Done);
691
692 task_mgr
694 .update_task_in_tx(
695 &mut tx,
696 existing_info.id,
697 task.spec.as_deref(),
698 task.priority.as_ref().map(|p| p.to_int()),
699 if is_becoming_done {
701 None
702 } else {
703 task.status.as_ref().map(|s| s.as_db_str())
704 },
705 task.active_form.as_deref(),
706 )
707 .await?;
708
709 if is_becoming_done {
711 if let Err(e) = task_mgr
712 .complete_task_in_tx(&mut tx, existing_info.id)
713 .await
714 {
715 return Ok(PlanResult::error(format!(
717 "Cannot complete task '{}': {}\n\n\
718 Please complete all subtasks before marking the parent as done.",
719 task_name, e
720 )));
721 }
722 }
723
724 task_id_map.insert(task_name.clone(), existing_info.id);
725 updated_count += 1;
726 } else {
727 if is_becoming_doing && !has_spec {
731 return Ok(PlanResult::error(format!(
732 "Task '{}': spec (description) is required when starting a task (status: doing).\n\n\
733 Before starting a task, please describe:\n \
734 • What is the goal of this task\n \
735 • How do you plan to approach it\n\n\
736 Tip: Use @file(path) to include content from a file",
737 task_name
738 )));
739 }
740
741 let id = task_mgr
742 .create_task_in_tx(
743 &mut tx,
744 task_name,
745 task.spec.as_deref(),
746 task.priority.as_ref().map(|p| p.to_int()),
747 task.status.as_ref().map(|s| s.as_db_str()),
748 task.active_form.as_deref(),
749 "ai", )
751 .await?;
752 task_id_map.insert(task_name.clone(), id);
753 newly_created_names.insert(task_name.clone());
754 created_count += 1;
755
756 if !has_spec && !is_becoming_doing {
758 warnings.push(format!(
759 "Task '{}' has no description. Consider adding one for better context.",
760 task_name
761 ));
762 }
763 }
764 }
765
766 for task in &normal_tasks {
768 if let Some(parent_name) = &task.parent_name {
769 if let Some(task_name) = &task.name {
770 let task_id = task_id_map.get(task_name).ok_or_else(|| {
771 IntentError::InvalidInput(format!("Task not found: {}", task_name))
772 })?;
773 let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
774 IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
775 })?;
776 task_mgr
777 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
778 .await?;
779 }
780 }
781 }
782
783 for task in &normal_tasks {
786 if task.parent_name.is_some() {
788 continue;
789 }
790
791 if let Some(explicit_parent) = &task.explicit_parent_id {
793 if let Some(task_name) = &task.name {
794 let task_id = task_id_map.get(task_name).ok_or_else(|| {
795 IntentError::InvalidInput(format!("Task not found: {}", task_name))
796 })?;
797
798 match explicit_parent {
799 None => {
800 task_mgr.clear_parent_in_tx(&mut tx, *task_id).await?;
802 },
803 Some(parent_id) => {
804 task_mgr
807 .set_parent_in_tx(&mut tx, *task_id, *parent_id)
808 .await?;
809 },
810 }
811 }
812 }
813 }
814
815 if let Some(default_parent) = self.default_parent_id {
817 for task in &normal_tasks {
818 if let Some(task_name) = &task.name {
823 if newly_created_names.contains(task_name)
824 && task.parent_name.is_none()
825 && task.explicit_parent_id.is_none()
826 {
827 if let Some(&task_id) = task_id_map.get(task_name) {
828 task_mgr
829 .set_parent_in_tx(&mut tx, task_id, default_parent)
830 .await?;
831 }
832 }
833 }
834 }
835 }
836
837 let dep_count = self
839 .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
840 .await?;
841
842 tx.commit().await?;
844
845 task_mgr.notify_batch_changed().await;
847
848 let doing_task = normal_tasks
851 .iter()
852 .find(|task| matches!(task.status, Some(TaskStatus::Doing)));
853
854 let focused_task_response = if let Some(doing_task) = doing_task {
855 if let Some(task_name) = &doing_task.name {
857 if let Some(&task_id) = task_id_map.get(task_name) {
858 let response = task_mgr.start_task(task_id, true).await?;
860 Some(response)
861 } else {
862 None
863 }
864 } else {
865 None
866 }
867 } else {
868 None
869 };
870
871 Ok(PlanResult::success_with_warnings(
873 task_id_map,
874 created_count,
875 updated_count,
876 deleted_count,
877 cascade_deleted_count,
878 dep_count,
879 focused_task_response,
880 warnings,
881 ))
882 }
883
884 async fn find_tasks_by_names(
886 &self,
887 names: &[String],
888 ) -> Result<HashMap<String, ExistingTaskInfo>> {
889 if names.is_empty() {
890 return Ok(HashMap::new());
891 }
892
893 let mut map = HashMap::new();
894
895 let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
898 let query = format!(
899 "SELECT id, name, status, spec FROM tasks WHERE name IN ({})",
900 placeholders
901 );
902
903 let mut query_builder = sqlx::query(&query);
904 for name in names {
905 query_builder = query_builder.bind(name);
906 }
907
908 let rows = query_builder.fetch_all(self.pool).await?;
909
910 for row in rows {
911 let id: i64 = row.get("id");
912 let name: String = row.get("name");
913 let status: String = row.get("status");
914 let spec: Option<String> = row.get("spec");
915 map.insert(name, ExistingTaskInfo { id, status, spec });
916 }
917
918 Ok(map)
919 }
920
921 async fn build_dependencies(
923 &self,
924 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
925 flat_tasks: &[FlatTask],
926 task_id_map: &HashMap<String, i64>,
927 ) -> Result<usize> {
928 let mut count = 0;
929
930 for task in flat_tasks {
931 if task.delete {
933 continue;
934 }
935 let task_name = match &task.name {
936 Some(name) => name,
937 None => continue,
938 };
939
940 if !task.depends_on.is_empty() {
941 let blocked_id = task_id_map.get(task_name).ok_or_else(|| {
942 IntentError::InvalidInput(format!("Task not found: {}", task_name))
943 })?;
944
945 for dep_name in &task.depends_on {
946 let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
947 IntentError::InvalidInput(format!(
948 "Dependency '{}' not found for task '{}'",
949 dep_name, task_name
950 ))
951 })?;
952
953 sqlx::query(
954 "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
955 )
956 .bind(blocking_id)
957 .bind(blocked_id)
958 .execute(&mut **tx)
959 .await?;
960
961 count += 1;
962 }
963 }
964 }
965
966 Ok(count)
967 }
968
969 fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
971 let task_names: std::collections::HashSet<_> = flat_tasks
972 .iter()
973 .filter_map(|t| t.name.as_ref().map(|n| n.as_str()))
974 .collect();
975
976 for task in flat_tasks {
977 for dep_name in &task.depends_on {
978 if !task_names.contains(dep_name.as_str()) {
979 let task_name = task.name.as_deref().unwrap_or("<unknown>");
980 return Err(IntentError::InvalidInput(format!(
981 "Task '{}' depends on '{}', but '{}' is not in the plan",
982 task_name, dep_name, dep_name
983 )));
984 }
985 }
986 }
987
988 Ok(())
989 }
990
991 fn validate_batch_single_doing(&self, flat_tasks: &[FlatTask]) -> Result<()> {
995 let doing_tasks: Vec<&FlatTask> = flat_tasks
997 .iter()
998 .filter(|task| matches!(task.status, Some(TaskStatus::Doing)))
999 .collect();
1000
1001 if doing_tasks.len() > 1 {
1003 let names: Vec<&str> = doing_tasks
1004 .iter()
1005 .map(|t| t.name.as_deref().unwrap_or("<unknown>"))
1006 .collect();
1007 return Err(IntentError::InvalidInput(format!(
1008 "Batch single doing constraint violated: only one task per batch can have status='doing'. Found: {}",
1009 names.join(", ")
1010 )));
1011 }
1012
1013 Ok(())
1014 }
1015
1016 fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
1018 if flat_tasks.is_empty() {
1019 return Ok(());
1020 }
1021
1022 let name_to_idx: HashMap<&str, usize> = flat_tasks
1024 .iter()
1025 .enumerate()
1026 .filter_map(|(i, t)| t.name.as_ref().map(|n| (n.as_str(), i)))
1027 .collect();
1028
1029 let mut graph: Vec<Vec<usize>> = vec![Vec::new(); flat_tasks.len()];
1031 for (idx, task) in flat_tasks.iter().enumerate() {
1032 for dep_name in &task.depends_on {
1033 if let Some(&dep_idx) = name_to_idx.get(dep_name.as_str()) {
1034 graph[idx].push(dep_idx);
1035 }
1036 }
1037 }
1038
1039 for task in flat_tasks {
1041 if let Some(name) = &task.name {
1042 if task.depends_on.contains(name) {
1043 return Err(IntentError::InvalidInput(format!(
1044 "Circular dependency detected: task '{}' depends on itself",
1045 name
1046 )));
1047 }
1048 }
1049 }
1050
1051 let sccs = self.tarjan_scc(&graph);
1053
1054 for scc in sccs {
1056 if scc.len() > 1 {
1057 let cycle_names: Vec<&str> = scc
1059 .iter()
1060 .map(|&idx| flat_tasks[idx].name.as_deref().unwrap_or("<unknown>"))
1061 .collect();
1062
1063 return Err(IntentError::InvalidInput(format!(
1064 "Circular dependency detected: {}",
1065 cycle_names.join(" → ")
1066 )));
1067 }
1068 }
1069
1070 Ok(())
1071 }
1072
1073 fn tarjan_scc(&self, graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
1076 let n = graph.len();
1077 let mut index = 0;
1078 let mut stack = Vec::new();
1079 let mut indices = vec![None; n];
1080 let mut lowlinks = vec![0; n];
1081 let mut on_stack = vec![false; n];
1082 let mut sccs = Vec::new();
1083
1084 #[allow(clippy::too_many_arguments)]
1085 fn strongconnect(
1086 v: usize,
1087 graph: &[Vec<usize>],
1088 index: &mut usize,
1089 stack: &mut Vec<usize>,
1090 indices: &mut [Option<usize>],
1091 lowlinks: &mut [usize],
1092 on_stack: &mut [bool],
1093 sccs: &mut Vec<Vec<usize>>,
1094 ) {
1095 indices[v] = Some(*index);
1097 lowlinks[v] = *index;
1098 *index += 1;
1099 stack.push(v);
1100 on_stack[v] = true;
1101
1102 for &w in &graph[v] {
1104 if indices[w].is_none() {
1105 strongconnect(w, graph, index, stack, indices, lowlinks, on_stack, sccs);
1107 lowlinks[v] = lowlinks[v].min(lowlinks[w]);
1108 } else if on_stack[w] {
1109 lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
1111 }
1112 }
1113
1114 if lowlinks[v] == indices[v].unwrap() {
1116 let mut scc = Vec::new();
1117 loop {
1118 let w = stack.pop().unwrap();
1119 on_stack[w] = false;
1120 scc.push(w);
1121 if w == v {
1122 break;
1123 }
1124 }
1125 sccs.push(scc);
1126 }
1127 }
1128
1129 for v in 0..n {
1131 if indices[v].is_none() {
1132 strongconnect(
1133 v,
1134 graph,
1135 &mut index,
1136 &mut stack,
1137 &mut indices,
1138 &mut lowlinks,
1139 &mut on_stack,
1140 &mut sccs,
1141 );
1142 }
1143 }
1144
1145 sccs
1146 }
1147}
1148
1149#[derive(Debug, Default)]
1151pub struct FileIncludeResult {
1152 pub files_to_delete: Vec<PathBuf>,
1154}
1155
1156fn parse_file_directive(value: &str) -> Option<(PathBuf, bool)> {
1162 let trimmed = value.trim();
1163
1164 if !trimmed.starts_with("@file(") || !trimmed.ends_with(')') {
1166 return None;
1167 }
1168
1169 let inner = &trimmed[6..trimmed.len() - 1];
1171
1172 if let Some(path_str) = inner.strip_suffix(", keep") {
1174 Some((PathBuf::from(path_str.trim()), false)) } else if let Some(path_str) = inner.strip_suffix(",keep") {
1176 Some((PathBuf::from(path_str.trim()), false))
1177 } else {
1178 Some((PathBuf::from(inner.trim()), true)) }
1180}
1181
1182fn process_task_tree_includes(
1184 task: &mut TaskTree,
1185 files_to_delete: &mut Vec<PathBuf>,
1186) -> std::result::Result<(), String> {
1187 if let Some(ref spec_value) = task.spec {
1189 if let Some((file_path, should_delete)) = parse_file_directive(spec_value) {
1190 let content = std::fs::read_to_string(&file_path)
1192 .map_err(|e| format!("Failed to read @file({}): {}", file_path.display(), e))?;
1193
1194 task.spec = Some(content);
1195
1196 if should_delete {
1197 files_to_delete.push(file_path);
1198 }
1199 }
1200 }
1201
1202 if let Some(ref mut children) = task.children {
1204 for child in children.iter_mut() {
1205 process_task_tree_includes(child, files_to_delete)?;
1206 }
1207 }
1208
1209 Ok(())
1210}
1211
1212pub fn process_file_includes(
1234 request: &mut PlanRequest,
1235) -> std::result::Result<FileIncludeResult, String> {
1236 let mut result = FileIncludeResult::default();
1237
1238 for task in request.tasks.iter_mut() {
1239 process_task_tree_includes(task, &mut result.files_to_delete)?;
1240 }
1241
1242 Ok(result)
1243}
1244
1245pub fn cleanup_included_files(files: &[PathBuf]) {
1247 for file in files {
1248 if let Err(e) = std::fs::remove_file(file) {
1249 tracing::warn!("Failed to delete included file {}: {}", file.display(), e);
1251 }
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258
1259 #[test]
1260 fn test_priority_value_to_int() {
1261 assert_eq!(PriorityValue::Critical.to_int(), 1);
1262 assert_eq!(PriorityValue::High.to_int(), 2);
1263 assert_eq!(PriorityValue::Medium.to_int(), 3);
1264 assert_eq!(PriorityValue::Low.to_int(), 4);
1265 }
1266
1267 #[test]
1268 fn test_priority_value_from_int() {
1269 assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
1270 assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
1271 assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
1272 assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
1273 assert_eq!(PriorityValue::from_int(999), None);
1274 }
1275
1276 #[test]
1277 fn test_priority_value_as_str() {
1278 assert_eq!(PriorityValue::Critical.as_str(), "critical");
1279 assert_eq!(PriorityValue::High.as_str(), "high");
1280 assert_eq!(PriorityValue::Medium.as_str(), "medium");
1281 assert_eq!(PriorityValue::Low.as_str(), "low");
1282 }
1283
1284 #[test]
1285 fn test_plan_request_deserialization_minimal() {
1286 let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
1287 let request: PlanRequest = serde_json::from_str(json).unwrap();
1288
1289 assert_eq!(request.tasks.len(), 1);
1290 assert_eq!(request.tasks[0].name, Some("Test Task".to_string()));
1291 assert_eq!(request.tasks[0].spec, None);
1292 assert_eq!(request.tasks[0].priority, None);
1293 assert_eq!(request.tasks[0].children, None);
1294 assert_eq!(request.tasks[0].depends_on, None);
1295 assert_eq!(request.tasks[0].id, None);
1296 }
1297
1298 #[test]
1299 fn test_plan_request_deserialization_full() {
1300 let json = r#"{
1301 "tasks": [{
1302 "name": "Parent Task",
1303 "spec": "Parent spec",
1304 "priority": "high",
1305 "children": [{
1306 "name": "Child Task",
1307 "spec": "Child spec"
1308 }],
1309 "depends_on": ["Other Task"],
1310 "task_id": 42
1311 }]
1312 }"#;
1313
1314 let request: PlanRequest = serde_json::from_str(json).unwrap();
1315
1316 assert_eq!(request.tasks.len(), 1);
1317 let parent = &request.tasks[0];
1318 assert_eq!(parent.name, Some("Parent Task".to_string()));
1319 assert_eq!(parent.spec, Some("Parent spec".to_string()));
1320 assert_eq!(parent.priority, Some(PriorityValue::High));
1321 assert_eq!(parent.id, Some(42));
1322
1323 let children = parent.children.as_ref().unwrap();
1324 assert_eq!(children.len(), 1);
1325 assert_eq!(children[0].name, Some("Child Task".to_string()));
1326
1327 let depends = parent.depends_on.as_ref().unwrap();
1328 assert_eq!(depends.len(), 1);
1329 assert_eq!(depends[0], "Other Task");
1330 }
1331
1332 #[test]
1333 fn test_plan_request_serialization() {
1334 let request = PlanRequest {
1335 tasks: vec![TaskTree {
1336 name: Some("Test Task".to_string()),
1337 spec: Some("Test spec".to_string()),
1338 priority: Some(PriorityValue::Medium),
1339 children: None,
1340 depends_on: None,
1341 id: None,
1342 status: None,
1343 active_form: None,
1344 parent_id: None,
1345 ..Default::default()
1346 }],
1347 };
1348
1349 let json = serde_json::to_string(&request).unwrap();
1350 assert!(json.contains("\"name\":\"Test Task\""));
1351 assert!(json.contains("\"spec\":\"Test spec\""));
1352 assert!(json.contains("\"priority\":\"medium\""));
1353 }
1354
1355 #[test]
1356 fn test_plan_result_success() {
1357 let mut map = HashMap::new();
1358 map.insert("Task 1".to_string(), 1);
1359 map.insert("Task 2".to_string(), 2);
1360
1361 let result = PlanResult::success(map.clone(), 2, 0, 0, 1, None);
1362
1363 assert!(result.success);
1364 assert_eq!(result.task_id_map, map);
1365 assert_eq!(result.created_count, 2);
1366 assert_eq!(result.updated_count, 0);
1367 assert_eq!(result.dependency_count, 1);
1368 assert_eq!(result.focused_task, None);
1369 assert_eq!(result.error, None);
1370 }
1371
1372 #[test]
1373 fn test_plan_result_error() {
1374 let result = PlanResult::error("Test error");
1375
1376 assert!(!result.success);
1377 assert_eq!(result.task_id_map.len(), 0);
1378 assert_eq!(result.created_count, 0);
1379 assert_eq!(result.updated_count, 0);
1380 assert_eq!(result.dependency_count, 0);
1381 assert_eq!(result.error, Some("Test error".to_string()));
1382 }
1383
1384 #[test]
1385 fn test_task_tree_nested() {
1386 let tree = TaskTree {
1387 name: Some("Parent".to_string()),
1388 spec: None,
1389 priority: None,
1390 children: Some(vec![
1391 TaskTree {
1392 name: Some("Child 1".to_string()),
1393 spec: None,
1394 priority: None,
1395 children: None,
1396 depends_on: None,
1397 id: None,
1398 status: None,
1399 active_form: None,
1400 parent_id: None,
1401 ..Default::default()
1402 },
1403 TaskTree {
1404 name: Some("Child 2".to_string()),
1405 spec: None,
1406 priority: Some(PriorityValue::High),
1407 children: None,
1408 depends_on: None,
1409 id: None,
1410 status: None,
1411 active_form: None,
1412 parent_id: None,
1413 ..Default::default()
1414 },
1415 ]),
1416 depends_on: None,
1417 id: None,
1418 status: None,
1419 active_form: None,
1420 parent_id: None,
1421 ..Default::default()
1422 };
1423
1424 let json = serde_json::to_string_pretty(&tree).unwrap();
1425 let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
1426
1427 assert_eq!(tree, deserialized);
1428 assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
1429 }
1430
1431 #[test]
1432 fn test_priority_value_case_insensitive_deserialization() {
1433 let json = r#"{"name": "Test", "priority": "high"}"#;
1435 let task: TaskTree = serde_json::from_str(json).unwrap();
1436 assert_eq!(task.priority, Some(PriorityValue::High));
1437
1438 }
1441
1442 #[test]
1443 fn test_extract_all_names_simple() {
1444 let tasks = vec![
1445 TaskTree {
1446 name: Some("Task 1".to_string()),
1447 spec: None,
1448 priority: None,
1449 children: None,
1450 depends_on: None,
1451 id: None,
1452 status: None,
1453 active_form: None,
1454 parent_id: None,
1455 ..Default::default()
1456 },
1457 TaskTree {
1458 name: Some("Task 2".to_string()),
1459 spec: None,
1460 priority: None,
1461 children: None,
1462 depends_on: None,
1463 id: None,
1464 status: None,
1465 active_form: None,
1466 parent_id: None,
1467 ..Default::default()
1468 },
1469 ];
1470
1471 let names = extract_all_names(&tasks);
1472 assert_eq!(names, vec!["Task 1", "Task 2"]);
1473 }
1474
1475 #[test]
1476 fn test_extract_all_names_nested() {
1477 let tasks = vec![TaskTree {
1478 name: Some("Parent".to_string()),
1479 spec: None,
1480 priority: None,
1481 children: Some(vec![
1482 TaskTree {
1483 name: Some("Child 1".to_string()),
1484 spec: None,
1485 priority: None,
1486 children: None,
1487 depends_on: None,
1488 id: None,
1489 status: None,
1490 active_form: None,
1491 parent_id: None,
1492 ..Default::default()
1493 },
1494 TaskTree {
1495 name: Some("Child 2".to_string()),
1496 spec: None,
1497 priority: None,
1498 children: Some(vec![TaskTree {
1499 name: Some("Grandchild".to_string()),
1500 spec: None,
1501 priority: None,
1502 children: None,
1503 depends_on: None,
1504 id: None,
1505 status: None,
1506 active_form: None,
1507 parent_id: None,
1508 ..Default::default()
1509 }]),
1510 depends_on: None,
1511 id: None,
1512 status: None,
1513 active_form: None,
1514 parent_id: None,
1515 ..Default::default()
1516 },
1517 ]),
1518 depends_on: None,
1519 id: None,
1520 status: None,
1521 active_form: None,
1522 parent_id: None,
1523 ..Default::default()
1524 }];
1525
1526 let names = extract_all_names(&tasks);
1527 assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
1528 }
1529
1530 #[test]
1531 fn test_flatten_task_tree_simple() {
1532 let tasks = vec![TaskTree {
1533 name: Some("Task 1".to_string()),
1534 spec: Some("Spec 1".to_string()),
1535 priority: Some(PriorityValue::High),
1536 children: None,
1537 depends_on: Some(vec!["Task 0".to_string()]),
1538 id: None,
1539 status: None,
1540 active_form: None,
1541 parent_id: None,
1542 ..Default::default()
1543 }];
1544
1545 let flat = flatten_task_tree(&tasks);
1546 assert_eq!(flat.len(), 1);
1547 assert_eq!(flat[0].name, Some("Task 1".to_string()));
1548 assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
1549 assert_eq!(flat[0].priority, Some(PriorityValue::High));
1550 assert_eq!(flat[0].parent_name, None);
1551 assert_eq!(flat[0].depends_on, vec!["Task 0"]);
1552 }
1553
1554 #[test]
1555 fn test_flatten_task_tree_nested() {
1556 let tasks = vec![TaskTree {
1557 name: Some("Parent".to_string()),
1558 spec: None,
1559 priority: None,
1560 children: Some(vec![
1561 TaskTree {
1562 name: Some("Child 1".to_string()),
1563 spec: None,
1564 priority: None,
1565 children: None,
1566 depends_on: None,
1567 id: None,
1568 status: None,
1569 active_form: None,
1570 parent_id: None,
1571 ..Default::default()
1572 },
1573 TaskTree {
1574 name: Some("Child 2".to_string()),
1575 spec: None,
1576 priority: None,
1577 children: None,
1578 depends_on: None,
1579 id: None,
1580 status: None,
1581 active_form: None,
1582 parent_id: None,
1583 ..Default::default()
1584 },
1585 ]),
1586 depends_on: None,
1587 id: None,
1588 status: None,
1589 active_form: None,
1590 parent_id: None,
1591 ..Default::default()
1592 }];
1593
1594 let flat = flatten_task_tree(&tasks);
1595 assert_eq!(flat.len(), 3);
1596
1597 assert_eq!(flat[0].name, Some("Parent".to_string()));
1599 assert_eq!(flat[0].parent_name, None);
1600
1601 assert_eq!(flat[1].name, Some("Child 1".to_string()));
1603 assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
1604
1605 assert_eq!(flat[2].name, Some("Child 2".to_string()));
1606 assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
1607 }
1608
1609 #[test]
1610 fn test_classify_operations_all_create() {
1611 let flat_tasks = vec![
1612 FlatTask {
1613 name: Some("Task 1".to_string()),
1614 spec: None,
1615 priority: None,
1616 parent_name: None,
1617 depends_on: vec![],
1618 id: None,
1619 status: None,
1620 active_form: None,
1621 explicit_parent_id: None,
1622 ..Default::default()
1623 },
1624 FlatTask {
1625 name: Some("Task 2".to_string()),
1626 spec: None,
1627 priority: None,
1628 parent_name: None,
1629 depends_on: vec![],
1630 id: None,
1631 status: None,
1632 active_form: None,
1633 explicit_parent_id: None,
1634 ..Default::default()
1635 },
1636 ];
1637
1638 let existing = HashMap::new();
1639 let operations = classify_operations(&flat_tasks, &existing);
1640
1641 assert_eq!(operations.len(), 2);
1642 assert!(matches!(operations[0], Operation::Create(_)));
1643 assert!(matches!(operations[1], Operation::Create(_)));
1644 }
1645
1646 #[test]
1647 fn test_classify_operations_all_update() {
1648 let flat_tasks = vec![
1649 FlatTask {
1650 name: Some("Task 1".to_string()),
1651 spec: None,
1652 priority: None,
1653 parent_name: None,
1654 depends_on: vec![],
1655 id: None,
1656 status: None,
1657 active_form: None,
1658 explicit_parent_id: None,
1659 ..Default::default()
1660 },
1661 FlatTask {
1662 name: Some("Task 2".to_string()),
1663 spec: None,
1664 priority: None,
1665 parent_name: None,
1666 depends_on: vec![],
1667 id: None,
1668 status: None,
1669 active_form: None,
1670 explicit_parent_id: None,
1671 ..Default::default()
1672 },
1673 ];
1674
1675 let mut existing = HashMap::new();
1676 existing.insert("Task 1".to_string(), 1);
1677 existing.insert("Task 2".to_string(), 2);
1678
1679 let operations = classify_operations(&flat_tasks, &existing);
1680
1681 assert_eq!(operations.len(), 2);
1682 assert!(matches!(operations[0], Operation::Update { id: 1, .. }));
1683 assert!(matches!(operations[1], Operation::Update { id: 2, .. }));
1684 }
1685
1686 #[test]
1687 fn test_classify_operations_mixed() {
1688 let flat_tasks = vec![
1689 FlatTask {
1690 name: Some("Existing Task".to_string()),
1691 spec: None,
1692 priority: None,
1693 parent_name: None,
1694 depends_on: vec![],
1695 id: None,
1696 status: None,
1697 active_form: None,
1698 explicit_parent_id: None,
1699 ..Default::default()
1700 },
1701 FlatTask {
1702 name: Some("New Task".to_string()),
1703 spec: None,
1704 priority: None,
1705 parent_name: None,
1706 depends_on: vec![],
1707 id: None,
1708 status: None,
1709 active_form: None,
1710 explicit_parent_id: None,
1711 ..Default::default()
1712 },
1713 ];
1714
1715 let mut existing = HashMap::new();
1716 existing.insert("Existing Task".to_string(), 42);
1717
1718 let operations = classify_operations(&flat_tasks, &existing);
1719
1720 assert_eq!(operations.len(), 2);
1721 assert!(matches!(operations[0], Operation::Update { id: 42, .. }));
1722 assert!(matches!(operations[1], Operation::Create(_)));
1723 }
1724
1725 #[test]
1726 fn test_classify_operations_explicit_task_id() {
1727 let flat_tasks = vec![FlatTask {
1728 name: Some("Task".to_string()),
1729 spec: None,
1730 priority: None,
1731 parent_name: None,
1732 depends_on: vec![],
1733 id: Some(99), status: None,
1735 active_form: None,
1736 explicit_parent_id: None,
1737 ..Default::default()
1738 }];
1739
1740 let existing = HashMap::new(); let operations = classify_operations(&flat_tasks, &existing);
1743
1744 assert_eq!(operations.len(), 1);
1746 assert!(matches!(operations[0], Operation::Update { id: 99, .. }));
1747 }
1748
1749 #[test]
1750 fn test_find_duplicate_names_no_duplicates() {
1751 let tasks = vec![
1752 TaskTree {
1753 name: Some("Task 1".to_string()),
1754 spec: None,
1755 priority: None,
1756 children: None,
1757 depends_on: None,
1758 id: None,
1759 status: None,
1760 active_form: None,
1761 parent_id: None,
1762 ..Default::default()
1763 },
1764 TaskTree {
1765 name: Some("Task 2".to_string()),
1766 spec: None,
1767 priority: None,
1768 children: None,
1769 depends_on: None,
1770 id: None,
1771 status: None,
1772 active_form: None,
1773 parent_id: None,
1774 ..Default::default()
1775 },
1776 ];
1777
1778 let duplicates = find_duplicate_names(&tasks);
1779 assert_eq!(duplicates.len(), 0);
1780 }
1781
1782 #[test]
1783 fn test_find_duplicate_names_with_duplicates() {
1784 let tasks = vec![
1785 TaskTree {
1786 name: Some("Duplicate".to_string()),
1787 spec: None,
1788 priority: None,
1789 children: None,
1790 depends_on: None,
1791 id: None,
1792 status: None,
1793 active_form: None,
1794 parent_id: None,
1795 ..Default::default()
1796 },
1797 TaskTree {
1798 name: Some("Unique".to_string()),
1799 spec: None,
1800 priority: None,
1801 children: None,
1802 depends_on: None,
1803 id: None,
1804 status: None,
1805 active_form: None,
1806 parent_id: None,
1807 ..Default::default()
1808 },
1809 TaskTree {
1810 name: Some("Duplicate".to_string()),
1811 spec: None,
1812 priority: None,
1813 children: None,
1814 depends_on: None,
1815 id: None,
1816 status: None,
1817 active_form: None,
1818 parent_id: None,
1819 ..Default::default()
1820 },
1821 ];
1822
1823 let duplicates = find_duplicate_names(&tasks);
1824 assert_eq!(duplicates.len(), 1);
1825 assert_eq!(duplicates[0], "Duplicate");
1826 }
1827
1828 #[test]
1829 fn test_find_duplicate_names_nested() {
1830 let tasks = vec![TaskTree {
1831 name: Some("Parent".to_string()),
1832 spec: None,
1833 priority: None,
1834 children: Some(vec![TaskTree {
1835 name: Some("Parent".to_string()), spec: None,
1837 priority: None,
1838 children: None,
1839 depends_on: None,
1840 id: None,
1841 status: None,
1842 active_form: None,
1843 parent_id: None,
1844 ..Default::default()
1845 }]),
1846 depends_on: None,
1847 id: None,
1848 status: None,
1849 active_form: None,
1850 parent_id: None,
1851 ..Default::default()
1852 }];
1853
1854 let duplicates = find_duplicate_names(&tasks);
1855 assert_eq!(duplicates.len(), 1);
1856 assert_eq!(duplicates[0], "Parent");
1857 }
1858
1859 #[test]
1860 fn test_flatten_task_tree_empty() {
1861 let tasks: Vec<TaskTree> = vec![];
1862 let flat = flatten_task_tree(&tasks);
1863 assert_eq!(flat.len(), 0);
1864 }
1865
1866 #[test]
1867 fn test_flatten_task_tree_deep_nesting() {
1868 let tasks = vec![TaskTree {
1870 name: Some("Root".to_string()),
1871 spec: None,
1872 priority: None,
1873 children: Some(vec![TaskTree {
1874 name: Some("Level1".to_string()),
1875 spec: None,
1876 priority: None,
1877 children: Some(vec![TaskTree {
1878 name: Some("Level2".to_string()),
1879 spec: None,
1880 priority: None,
1881 children: Some(vec![TaskTree {
1882 name: Some("Level3".to_string()),
1883 spec: None,
1884 priority: None,
1885 children: None,
1886 depends_on: None,
1887 id: None,
1888 status: None,
1889 active_form: None,
1890 parent_id: None,
1891 ..Default::default()
1892 }]),
1893 depends_on: None,
1894 id: None,
1895 status: None,
1896 active_form: None,
1897 parent_id: None,
1898 ..Default::default()
1899 }]),
1900 depends_on: None,
1901 id: None,
1902 status: None,
1903 active_form: None,
1904 parent_id: None,
1905 ..Default::default()
1906 }]),
1907 depends_on: None,
1908 id: None,
1909 status: None,
1910 active_form: None,
1911 parent_id: None,
1912 ..Default::default()
1913 }];
1914
1915 let flat = flatten_task_tree(&tasks);
1916 assert_eq!(flat.len(), 4);
1917
1918 assert_eq!(flat[0].name, Some("Root".to_string()));
1920 assert_eq!(flat[0].parent_name, None);
1921
1922 assert_eq!(flat[1].name, Some("Level1".to_string()));
1923 assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1924
1925 assert_eq!(flat[2].name, Some("Level2".to_string()));
1926 assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1927
1928 assert_eq!(flat[3].name, Some("Level3".to_string()));
1929 assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1930 }
1931
1932 #[test]
1933 fn test_flatten_task_tree_many_siblings() {
1934 let children: Vec<TaskTree> = (0..10)
1935 .map(|i| TaskTree {
1936 name: Some(format!("Child {}", i)),
1937 spec: None,
1938 priority: None,
1939 children: None,
1940 depends_on: None,
1941 id: None,
1942 status: None,
1943 active_form: None,
1944 parent_id: None,
1945 ..Default::default()
1946 })
1947 .collect();
1948
1949 let tasks = vec![TaskTree {
1950 name: Some("Parent".to_string()),
1951 spec: None,
1952 priority: None,
1953 children: Some(children),
1954 depends_on: None,
1955 id: None,
1956 status: None,
1957 active_form: None,
1958 parent_id: None,
1959 ..Default::default()
1960 }];
1961
1962 let flat = flatten_task_tree(&tasks);
1963 assert_eq!(flat.len(), 11); for child in flat.iter().skip(1).take(10) {
1967 assert_eq!(child.parent_name, Some("Parent".to_string()));
1968 }
1969 }
1970
1971 #[test]
1972 fn test_flatten_task_tree_complex_mixed() {
1973 let tasks = vec![
1975 TaskTree {
1976 name: Some("Task 1".to_string()),
1977 spec: None,
1978 priority: None,
1979 children: Some(vec![
1980 TaskTree {
1981 name: Some("Task 1.1".to_string()),
1982 spec: None,
1983 priority: None,
1984 children: None,
1985 depends_on: None,
1986 id: None,
1987 status: None,
1988 active_form: None,
1989 parent_id: None,
1990 ..Default::default()
1991 },
1992 TaskTree {
1993 name: Some("Task 1.2".to_string()),
1994 spec: None,
1995 priority: None,
1996 children: Some(vec![TaskTree {
1997 name: Some("Task 1.2.1".to_string()),
1998 spec: None,
1999 priority: None,
2000 children: None,
2001 depends_on: None,
2002 id: None,
2003 status: None,
2004 active_form: None,
2005 parent_id: None,
2006 ..Default::default()
2007 }]),
2008 depends_on: None,
2009 id: None,
2010 status: None,
2011 active_form: None,
2012 parent_id: None,
2013 ..Default::default()
2014 },
2015 ]),
2016 depends_on: None,
2017 id: None,
2018 status: None,
2019 active_form: None,
2020 parent_id: None,
2021 ..Default::default()
2022 },
2023 TaskTree {
2024 name: Some("Task 2".to_string()),
2025 spec: None,
2026 priority: None,
2027 children: None,
2028 depends_on: Some(vec!["Task 1".to_string()]),
2029 id: None,
2030 status: None,
2031 active_form: None,
2032 parent_id: None,
2033 ..Default::default()
2034 },
2035 ];
2036
2037 let flat = flatten_task_tree(&tasks);
2038 assert_eq!(flat.len(), 5);
2039
2040 assert_eq!(flat[0].name, Some("Task 1".to_string()));
2042 assert_eq!(flat[0].parent_name, None);
2043
2044 assert_eq!(flat[1].name, Some("Task 1.1".to_string()));
2045 assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
2046
2047 assert_eq!(flat[2].name, Some("Task 1.2".to_string()));
2048 assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
2049
2050 assert_eq!(flat[3].name, Some("Task 1.2.1".to_string()));
2051 assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
2052
2053 assert_eq!(flat[4].name, Some("Task 2".to_string()));
2054 assert_eq!(flat[4].parent_name, None);
2055 assert_eq!(flat[4].depends_on, vec!["Task 1"]);
2056 }
2057
2058 #[tokio::test]
2059 async fn test_plan_executor_integration() {
2060 use crate::test_utils::test_helpers::TestContext;
2061
2062 let ctx = TestContext::new().await;
2063
2064 let request = PlanRequest {
2066 tasks: vec![TaskTree {
2067 name: Some("Integration Test Plan".to_string()),
2068 spec: Some("Test plan execution end-to-end".to_string()),
2069 priority: Some(PriorityValue::High),
2070 children: Some(vec![
2071 TaskTree {
2072 name: Some("Subtask A".to_string()),
2073 spec: Some("First subtask".to_string()),
2074 priority: None,
2075 children: None,
2076 depends_on: None,
2077 id: None,
2078 status: None,
2079 active_form: None,
2080 parent_id: None,
2081 ..Default::default()
2082 },
2083 TaskTree {
2084 name: Some("Subtask B".to_string()),
2085 spec: Some("Second subtask depends on A".to_string()),
2086 priority: None,
2087 children: None,
2088 depends_on: Some(vec!["Subtask A".to_string()]),
2089 id: None,
2090 status: None,
2091 active_form: None,
2092 parent_id: None,
2093 ..Default::default()
2094 },
2095 ]),
2096 depends_on: None,
2097 id: None,
2098 status: None,
2099 active_form: None,
2100 parent_id: None,
2101 ..Default::default()
2102 }],
2103 };
2104
2105 let executor = PlanExecutor::new(&ctx.pool);
2107 let result = executor.execute(&request).await.unwrap();
2108
2109 assert!(result.success, "Plan execution should succeed");
2111 assert_eq!(result.created_count, 3, "Should create 3 tasks");
2112 assert_eq!(result.updated_count, 0, "Should not update any tasks");
2113 assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
2114 assert!(result.error.is_none(), "Should have no error");
2115
2116 assert_eq!(result.task_id_map.len(), 3);
2118 assert!(result.task_id_map.contains_key("Integration Test Plan"));
2119 assert!(result.task_id_map.contains_key("Subtask A"));
2120 assert!(result.task_id_map.contains_key("Subtask B"));
2121
2122 let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
2124 let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
2125 let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
2126
2127 let parent: (String, String, i64, Option<i64>) =
2129 sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
2130 .bind(parent_id)
2131 .fetch_one(&ctx.pool)
2132 .await
2133 .unwrap();
2134
2135 assert_eq!(parent.0, "Integration Test Plan");
2136 assert_eq!(parent.1, "Test plan execution end-to-end");
2137 assert_eq!(parent.2, 2); assert_eq!(parent.3, None); let subtask_a: (String, Option<i64>) =
2142 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
2143 .bind(subtask_a_id)
2144 .fetch_one(&ctx.pool)
2145 .await
2146 .unwrap();
2147
2148 assert_eq!(subtask_a.0, "Subtask A");
2149 assert_eq!(subtask_a.1, Some(parent_id)); let dep: (i64, i64) = sqlx::query_as(
2153 "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
2154 )
2155 .bind(subtask_b_id)
2156 .fetch_one(&ctx.pool)
2157 .await
2158 .unwrap();
2159
2160 assert_eq!(dep.0, subtask_a_id); assert_eq!(dep.1, subtask_b_id); }
2163
2164 #[tokio::test]
2165 async fn test_plan_executor_idempotency() {
2166 use crate::test_utils::test_helpers::TestContext;
2167
2168 let ctx = TestContext::new().await;
2169
2170 let request = PlanRequest {
2172 tasks: vec![TaskTree {
2173 name: Some("Idempotent Task".to_string()),
2174 spec: Some("Initial spec".to_string()),
2175 priority: Some(PriorityValue::High),
2176 children: Some(vec![
2177 TaskTree {
2178 name: Some("Child 1".to_string()),
2179 spec: Some("Child spec 1".to_string()),
2180 priority: None,
2181 children: None,
2182 depends_on: None,
2183 id: None,
2184 status: None,
2185 active_form: None,
2186 parent_id: None,
2187 ..Default::default()
2188 },
2189 TaskTree {
2190 name: Some("Child 2".to_string()),
2191 spec: Some("Child spec 2".to_string()),
2192 priority: Some(PriorityValue::Low),
2193 children: None,
2194 depends_on: None,
2195 id: None,
2196 status: None,
2197 active_form: None,
2198 parent_id: None,
2199 ..Default::default()
2200 },
2201 ]),
2202 depends_on: None,
2203 id: None,
2204 status: None,
2205 active_form: None,
2206 parent_id: None,
2207 ..Default::default()
2208 }],
2209 };
2210
2211 let executor = PlanExecutor::new(&ctx.pool);
2212
2213 let result1 = executor.execute(&request).await.unwrap();
2215 assert!(result1.success, "First execution should succeed");
2216 assert_eq!(result1.created_count, 3, "Should create 3 tasks");
2217 assert_eq!(result1.updated_count, 0, "Should not update any tasks");
2218 assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
2219
2220 let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
2222 let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
2223 let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
2224
2225 let result2 = executor.execute(&request).await.unwrap();
2227 assert!(result2.success, "Second execution should succeed");
2228 assert_eq!(result2.created_count, 0, "Should not create any new tasks");
2229 assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
2230 assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
2231
2232 let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
2234 let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
2235 let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
2236
2237 assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
2238 assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
2239 assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
2240
2241 let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
2243 .bind(parent_id_2)
2244 .fetch_one(&ctx.pool)
2245 .await
2246 .unwrap();
2247
2248 assert_eq!(parent.0, "Initial spec");
2249 assert_eq!(parent.1, 2); let modified_request = PlanRequest {
2253 tasks: vec![TaskTree {
2254 name: Some("Idempotent Task".to_string()),
2255 spec: Some("Updated spec".to_string()), priority: Some(PriorityValue::Critical), children: Some(vec![
2258 TaskTree {
2259 name: Some("Child 1".to_string()),
2260 spec: Some("Updated child spec 1".to_string()), priority: None,
2262 children: None,
2263 depends_on: None,
2264 id: None,
2265 status: None,
2266 active_form: None,
2267 parent_id: None,
2268 ..Default::default()
2269 },
2270 TaskTree {
2271 name: Some("Child 2".to_string()),
2272 spec: Some("Child spec 2".to_string()), priority: Some(PriorityValue::Low),
2274 children: None,
2275 depends_on: None,
2276 id: None,
2277 status: None,
2278 active_form: None,
2279 parent_id: None,
2280 ..Default::default()
2281 },
2282 ]),
2283 depends_on: None,
2284 id: None,
2285 status: None,
2286 active_form: None,
2287 parent_id: None,
2288 ..Default::default()
2289 }],
2290 };
2291
2292 let result3 = executor.execute(&modified_request).await.unwrap();
2293 assert!(result3.success, "Third execution should succeed");
2294 assert_eq!(result3.created_count, 0, "Should not create any new tasks");
2295 assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
2296
2297 let updated_parent: (String, i64) =
2299 sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
2300 .bind(parent_id_2)
2301 .fetch_one(&ctx.pool)
2302 .await
2303 .unwrap();
2304
2305 assert_eq!(updated_parent.0, "Updated spec");
2306 assert_eq!(updated_parent.1, 1); let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
2309 .bind(child1_id_2)
2310 .fetch_one(&ctx.pool)
2311 .await
2312 .unwrap();
2313
2314 assert_eq!(updated_child1.0, "Updated child spec 1");
2315 }
2316
2317 #[tokio::test]
2318 async fn test_plan_executor_dependencies() {
2319 use crate::test_utils::test_helpers::TestContext;
2320
2321 let ctx = TestContext::new().await;
2322
2323 let request = PlanRequest {
2325 tasks: vec![
2326 TaskTree {
2327 name: Some("Foundation".to_string()),
2328 spec: Some("Base layer".to_string()),
2329 priority: Some(PriorityValue::Critical),
2330 children: None,
2331 depends_on: None,
2332 id: None,
2333 status: None,
2334 active_form: None,
2335 parent_id: None,
2336 ..Default::default()
2337 },
2338 TaskTree {
2339 name: Some("Layer 1".to_string()),
2340 spec: Some("Depends on Foundation".to_string()),
2341 priority: Some(PriorityValue::High),
2342 children: None,
2343 depends_on: Some(vec!["Foundation".to_string()]),
2344 id: None,
2345 status: None,
2346 active_form: None,
2347 parent_id: None,
2348 ..Default::default()
2349 },
2350 TaskTree {
2351 name: Some("Layer 2".to_string()),
2352 spec: Some("Depends on Layer 1".to_string()),
2353 priority: None,
2354 children: None,
2355 depends_on: Some(vec!["Layer 1".to_string()]),
2356 id: None,
2357 status: None,
2358 active_form: None,
2359 parent_id: None,
2360 ..Default::default()
2361 },
2362 TaskTree {
2363 name: Some("Integration".to_string()),
2364 spec: Some("Depends on both Foundation and Layer 2".to_string()),
2365 priority: None,
2366 children: None,
2367 depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
2368 id: None,
2369 status: None,
2370 active_form: None,
2371 parent_id: None,
2372 ..Default::default()
2373 },
2374 ],
2375 };
2376
2377 let executor = PlanExecutor::new(&ctx.pool);
2378 let result = executor.execute(&request).await.unwrap();
2379
2380 assert!(result.success, "Plan execution should succeed");
2381 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2382 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2383
2384 let foundation_id = *result.task_id_map.get("Foundation").unwrap();
2386 let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
2387 let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
2388 let integration_id = *result.task_id_map.get("Integration").unwrap();
2389
2390 let deps1: Vec<(i64,)> =
2392 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
2393 .bind(layer1_id)
2394 .fetch_all(&ctx.pool)
2395 .await
2396 .unwrap();
2397
2398 assert_eq!(deps1.len(), 1);
2399 assert_eq!(deps1[0].0, foundation_id);
2400
2401 let deps2: Vec<(i64,)> =
2403 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
2404 .bind(layer2_id)
2405 .fetch_all(&ctx.pool)
2406 .await
2407 .unwrap();
2408
2409 assert_eq!(deps2.len(), 1);
2410 assert_eq!(deps2[0].0, layer1_id);
2411
2412 let deps3: Vec<(i64,)> =
2414 sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
2415 .bind(integration_id)
2416 .fetch_all(&ctx.pool)
2417 .await
2418 .unwrap();
2419
2420 assert_eq!(deps3.len(), 2);
2421 let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
2422 blocking_ids.sort();
2423
2424 let mut expected_ids = vec![foundation_id, layer2_id];
2425 expected_ids.sort();
2426
2427 assert_eq!(blocking_ids, expected_ids);
2428 }
2429
2430 #[tokio::test]
2431 async fn test_plan_executor_invalid_dependency() {
2432 use crate::test_utils::test_helpers::TestContext;
2433
2434 let ctx = TestContext::new().await;
2435
2436 let request = PlanRequest {
2438 tasks: vec![TaskTree {
2439 name: Some("Task A".to_string()),
2440 spec: Some("Depends on non-existent task".to_string()),
2441 priority: None,
2442 children: None,
2443 depends_on: Some(vec!["NonExistent".to_string()]),
2444 id: None,
2445 status: None,
2446 active_form: None,
2447 parent_id: None,
2448 ..Default::default()
2449 }],
2450 };
2451
2452 let executor = PlanExecutor::new(&ctx.pool);
2453 let result = executor.execute(&request).await.unwrap();
2454
2455 assert!(!result.success, "Plan execution should fail");
2456 assert!(result.error.is_some(), "Should have error message");
2457 let error = result.error.unwrap();
2458 assert!(
2459 error.contains("NonExistent"),
2460 "Error should mention the missing dependency: {}",
2461 error
2462 );
2463 }
2464
2465 #[tokio::test]
2466 async fn test_plan_executor_simple_cycle() {
2467 use crate::test_utils::test_helpers::TestContext;
2468
2469 let ctx = TestContext::new().await;
2470
2471 let request = PlanRequest {
2473 tasks: vec![
2474 TaskTree {
2475 name: Some("Task A".to_string()),
2476 spec: Some("Depends on B".to_string()),
2477 priority: None,
2478 children: None,
2479 depends_on: Some(vec!["Task B".to_string()]),
2480 id: None,
2481 status: None,
2482 active_form: None,
2483 parent_id: None,
2484 ..Default::default()
2485 },
2486 TaskTree {
2487 name: Some("Task B".to_string()),
2488 spec: Some("Depends on A".to_string()),
2489 priority: None,
2490 children: None,
2491 depends_on: Some(vec!["Task A".to_string()]),
2492 id: None,
2493 status: None,
2494 active_form: None,
2495 parent_id: None,
2496 ..Default::default()
2497 },
2498 ],
2499 };
2500
2501 let executor = PlanExecutor::new(&ctx.pool);
2502 let result = executor.execute(&request).await.unwrap();
2503
2504 assert!(!result.success, "Plan execution should fail");
2505 assert!(result.error.is_some(), "Should have error message");
2506 let error = result.error.unwrap();
2507 assert!(
2508 error.contains("Circular dependency"),
2509 "Error should mention circular dependency: {}",
2510 error
2511 );
2512 assert!(
2513 error.contains("Task A") && error.contains("Task B"),
2514 "Error should mention both tasks in the cycle: {}",
2515 error
2516 );
2517 }
2518
2519 #[tokio::test]
2520 async fn test_plan_executor_complex_cycle() {
2521 use crate::test_utils::test_helpers::TestContext;
2522
2523 let ctx = TestContext::new().await;
2524
2525 let request = PlanRequest {
2527 tasks: vec![
2528 TaskTree {
2529 name: Some("Task A".to_string()),
2530 spec: Some("Depends on B".to_string()),
2531 priority: None,
2532 children: None,
2533 depends_on: Some(vec!["Task B".to_string()]),
2534 id: None,
2535 status: None,
2536 active_form: None,
2537 parent_id: None,
2538 ..Default::default()
2539 },
2540 TaskTree {
2541 name: Some("Task B".to_string()),
2542 spec: Some("Depends on C".to_string()),
2543 priority: None,
2544 children: None,
2545 depends_on: Some(vec!["Task C".to_string()]),
2546 id: None,
2547 status: None,
2548 active_form: None,
2549 parent_id: None,
2550 ..Default::default()
2551 },
2552 TaskTree {
2553 name: Some("Task C".to_string()),
2554 spec: Some("Depends on A".to_string()),
2555 priority: None,
2556 children: None,
2557 depends_on: Some(vec!["Task A".to_string()]),
2558 id: None,
2559 status: None,
2560 active_form: None,
2561 parent_id: None,
2562 ..Default::default()
2563 },
2564 ],
2565 };
2566
2567 let executor = PlanExecutor::new(&ctx.pool);
2568 let result = executor.execute(&request).await.unwrap();
2569
2570 assert!(!result.success, "Plan execution should fail");
2571 assert!(result.error.is_some(), "Should have error message");
2572 let error = result.error.unwrap();
2573 assert!(
2574 error.contains("Circular dependency"),
2575 "Error should mention circular dependency: {}",
2576 error
2577 );
2578 assert!(
2579 error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
2580 "Error should mention all tasks in the cycle: {}",
2581 error
2582 );
2583 }
2584
2585 #[tokio::test]
2586 async fn test_plan_executor_valid_dag() {
2587 use crate::test_utils::test_helpers::TestContext;
2588
2589 let ctx = TestContext::new().await;
2590
2591 let request = PlanRequest {
2598 tasks: vec![
2599 TaskTree {
2600 name: Some("Task A".to_string()),
2601 spec: Some("Root task".to_string()),
2602 priority: None,
2603 children: None,
2604 depends_on: None,
2605 id: None,
2606 status: None,
2607 active_form: None,
2608 parent_id: None,
2609 ..Default::default()
2610 },
2611 TaskTree {
2612 name: Some("Task B".to_string()),
2613 spec: Some("Depends on A".to_string()),
2614 priority: None,
2615 children: None,
2616 depends_on: Some(vec!["Task A".to_string()]),
2617 id: None,
2618 status: None,
2619 active_form: None,
2620 parent_id: None,
2621 ..Default::default()
2622 },
2623 TaskTree {
2624 name: Some("Task C".to_string()),
2625 spec: Some("Depends on A".to_string()),
2626 priority: None,
2627 children: None,
2628 depends_on: Some(vec!["Task A".to_string()]),
2629 id: None,
2630 status: None,
2631 active_form: None,
2632 parent_id: None,
2633 ..Default::default()
2634 },
2635 TaskTree {
2636 name: Some("Task D".to_string()),
2637 spec: Some("Depends on B and C".to_string()),
2638 priority: None,
2639 children: None,
2640 depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
2641 id: None,
2642 status: None,
2643 active_form: None,
2644 parent_id: None,
2645 ..Default::default()
2646 },
2647 ],
2648 };
2649
2650 let executor = PlanExecutor::new(&ctx.pool);
2651 let result = executor.execute(&request).await.unwrap();
2652
2653 assert!(
2654 result.success,
2655 "Plan execution should succeed for valid DAG"
2656 );
2657 assert_eq!(result.created_count, 4, "Should create 4 tasks");
2658 assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
2659 }
2660
2661 #[tokio::test]
2662 async fn test_plan_executor_self_dependency() {
2663 use crate::test_utils::test_helpers::TestContext;
2664
2665 let ctx = TestContext::new().await;
2666
2667 let request = PlanRequest {
2669 tasks: vec![TaskTree {
2670 name: Some("Task A".to_string()),
2671 spec: Some("Depends on itself".to_string()),
2672 priority: None,
2673 children: None,
2674 depends_on: Some(vec!["Task A".to_string()]),
2675 id: None,
2676 status: None,
2677 active_form: None,
2678 parent_id: None,
2679 ..Default::default()
2680 }],
2681 };
2682
2683 let executor = PlanExecutor::new(&ctx.pool);
2684 let result = executor.execute(&request).await.unwrap();
2685
2686 assert!(
2687 !result.success,
2688 "Plan execution should fail for self-dependency"
2689 );
2690 assert!(result.error.is_some(), "Should have error message");
2691 let error = result.error.unwrap();
2692 assert!(
2693 error.contains("Circular dependency"),
2694 "Error should mention circular dependency: {}",
2695 error
2696 );
2697 }
2698
2699 #[tokio::test]
2701 async fn test_find_tasks_by_names_empty() {
2702 use crate::test_utils::test_helpers::TestContext;
2703
2704 let ctx = TestContext::new().await;
2705 let executor = PlanExecutor::new(&ctx.pool);
2706
2707 let result = executor.find_tasks_by_names(&[]).await.unwrap();
2708 assert!(result.is_empty(), "Empty input should return empty map");
2709 }
2710
2711 #[tokio::test]
2712 async fn test_find_tasks_by_names_partial() {
2713 use crate::test_utils::test_helpers::TestContext;
2714
2715 let ctx = TestContext::new().await;
2716 let executor = PlanExecutor::new(&ctx.pool);
2717
2718 let request = PlanRequest {
2720 tasks: vec![
2721 TaskTree {
2722 name: Some("Task A".to_string()),
2723 spec: None,
2724 priority: None,
2725 children: None,
2726 depends_on: None,
2727 id: None,
2728 status: None,
2729 active_form: None,
2730 parent_id: None,
2731 ..Default::default()
2732 },
2733 TaskTree {
2734 name: Some("Task B".to_string()),
2735 spec: None,
2736 priority: None,
2737 children: None,
2738 depends_on: None,
2739 id: None,
2740 status: None,
2741 active_form: None,
2742 parent_id: None,
2743 ..Default::default()
2744 },
2745 ],
2746 };
2747 executor.execute(&request).await.unwrap();
2748
2749 let names = vec![
2751 "Task A".to_string(),
2752 "Task B".to_string(),
2753 "Task C".to_string(),
2754 ];
2755 let result = executor.find_tasks_by_names(&names).await.unwrap();
2756
2757 assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
2758 assert!(result.contains_key("Task A"));
2759 assert!(result.contains_key("Task B"));
2760 assert!(!result.contains_key("Task C"));
2761 }
2762
2763 #[tokio::test]
2765 async fn test_plan_1000_tasks_performance() {
2766 use crate::test_utils::test_helpers::TestContext;
2767
2768 let ctx = TestContext::new().await;
2769 let executor = PlanExecutor::new(&ctx.pool);
2770
2771 let mut tasks = Vec::new();
2773 for i in 0..1000 {
2774 tasks.push(TaskTree {
2775 name: Some(format!("Task {}", i)),
2776 spec: Some(format!("Spec for task {}", i)),
2777 priority: Some(PriorityValue::Medium),
2778 children: None,
2779 depends_on: None,
2780 id: None,
2781 status: None,
2782 active_form: None,
2783 parent_id: None,
2784 ..Default::default()
2785 });
2786 }
2787
2788 let request = PlanRequest { tasks };
2789
2790 let start = std::time::Instant::now();
2791 let result = executor.execute(&request).await.unwrap();
2792 let duration = start.elapsed();
2793
2794 assert!(result.success);
2795 assert_eq!(result.created_count, 1000);
2796 assert!(
2797 duration.as_secs() < 10,
2798 "Should complete 1000 tasks in under 10 seconds, took {:?}",
2799 duration
2800 );
2801
2802 println!("✅ Created 1000 tasks in {:?}", duration);
2803 }
2804
2805 #[tokio::test]
2806 async fn test_plan_deep_nesting_20_levels() {
2807 use crate::test_utils::test_helpers::TestContext;
2808
2809 let ctx = TestContext::new().await;
2810 let executor = PlanExecutor::new(&ctx.pool);
2811
2812 fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
2814 TaskTree {
2815 name: Some(format!("Level {}", current)),
2816 spec: Some(format!("Task at depth {}", current)),
2817 priority: Some(PriorityValue::Low),
2818 children: if current < depth {
2819 Some(vec![build_deep_tree(depth, current + 1)])
2820 } else {
2821 None
2822 },
2823 depends_on: None,
2824 id: None,
2825 status: None,
2826 active_form: None,
2827 parent_id: None,
2828 ..Default::default()
2829 }
2830 }
2831
2832 let request = PlanRequest {
2833 tasks: vec![build_deep_tree(20, 1)],
2834 };
2835
2836 let start = std::time::Instant::now();
2837 let result = executor.execute(&request).await.unwrap();
2838 let duration = start.elapsed();
2839
2840 assert!(result.success);
2841 assert_eq!(
2842 result.created_count, 20,
2843 "Should create 20 tasks (1 per level)"
2844 );
2845 assert!(
2846 duration.as_secs() < 5,
2847 "Should handle 20-level nesting in under 5 seconds, took {:?}",
2848 duration
2849 );
2850
2851 println!("✅ Created 20-level deep tree in {:?}", duration);
2852 }
2853
2854 #[test]
2855 fn test_flatten_preserves_all_fields() {
2856 let tasks = vec![TaskTree {
2857 name: Some("Full Task".to_string()),
2858 spec: Some("Detailed spec".to_string()),
2859 priority: Some(PriorityValue::Critical),
2860 children: None,
2861 depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2862 id: Some(42),
2863 status: None,
2864 active_form: None,
2865 parent_id: None,
2866 ..Default::default()
2867 }];
2868
2869 let flat = flatten_task_tree(&tasks);
2870 assert_eq!(flat.len(), 1);
2871
2872 let task = &flat[0];
2873 assert_eq!(task.name, Some("Full Task".to_string()));
2874 assert_eq!(task.spec, Some("Detailed spec".to_string()));
2875 assert_eq!(task.priority, Some(PriorityValue::Critical));
2876 assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2877 assert_eq!(task.id, Some(42));
2878 }
2879}
2880
2881#[cfg(test)]
2882mod dataflow_tests {
2883 use super::*;
2884 use crate::tasks::TaskManager;
2885 use crate::test_utils::test_helpers::TestContext;
2886
2887 #[tokio::test]
2888 async fn test_complete_dataflow_status_and_active_form() {
2889 let ctx = TestContext::new().await;
2891
2892 let request = PlanRequest {
2894 tasks: vec![TaskTree {
2895 name: Some("Test Active Form Task".to_string()),
2896 spec: Some("Testing complete dataflow".to_string()),
2897 priority: Some(PriorityValue::High),
2898 children: None,
2899 depends_on: None,
2900 id: None,
2901 status: Some(TaskStatus::Doing),
2902 active_form: Some("Testing complete dataflow now".to_string()),
2903 parent_id: None,
2904 ..Default::default()
2905 }],
2906 };
2907
2908 let executor = PlanExecutor::new(&ctx.pool);
2909 let result = executor.execute(&request).await.unwrap();
2910
2911 assert!(result.success);
2912 assert_eq!(result.created_count, 1);
2913
2914 let task_mgr = TaskManager::new(&ctx.pool);
2916 let result = task_mgr
2917 .find_tasks(None, None, None, None, None)
2918 .await
2919 .unwrap();
2920
2921 assert_eq!(result.tasks.len(), 1);
2922 let task = &result.tasks[0];
2923
2924 assert_eq!(task.name, "Test Active Form Task");
2926 assert_eq!(task.status, "doing"); assert_eq!(
2928 task.active_form,
2929 Some("Testing complete dataflow now".to_string())
2930 );
2931
2932 let json = serde_json::to_value(task).unwrap();
2934 assert_eq!(json["name"], "Test Active Form Task");
2935 assert_eq!(json["status"], "doing");
2936 assert_eq!(json["active_form"], "Testing complete dataflow now");
2937
2938 println!("✅ 完整数据流验证成功!");
2939 println!(" Plan工具写入 -> Task读取 -> JSON序列化 -> MCP输出");
2940 println!(" active_form: {:?}", task.active_form);
2941 }
2942}
2943
2944#[cfg(test)]
2945mod parent_id_tests {
2946 use super::*;
2947 use crate::test_utils::test_helpers::TestContext;
2948
2949 #[test]
2950 fn test_parent_id_json_deserialization_absent() {
2951 let json = r#"{"name": "Test Task"}"#;
2953 let task: TaskTree = serde_json::from_str(json).unwrap();
2954 assert_eq!(task.parent_id, None);
2955 }
2956
2957 #[test]
2958 fn test_parent_id_json_deserialization_null() {
2959 let json = r#"{"name": "Test Task", "parent_id": null}"#;
2961 let task: TaskTree = serde_json::from_str(json).unwrap();
2962 assert_eq!(task.parent_id, Some(None));
2963 }
2964
2965 #[test]
2966 fn test_parent_id_json_deserialization_number() {
2967 let json = r#"{"name": "Test Task", "parent_id": 42}"#;
2969 let task: TaskTree = serde_json::from_str(json).unwrap();
2970 assert_eq!(task.parent_id, Some(Some(42)));
2971 }
2972
2973 #[test]
2974 fn test_flatten_propagates_parent_id() {
2975 let tasks = vec![TaskTree {
2976 name: Some("Task with explicit parent".to_string()),
2977 spec: None,
2978 priority: None,
2979 children: None,
2980 depends_on: None,
2981 id: None,
2982 status: None,
2983 active_form: None,
2984 parent_id: Some(Some(99)),
2985 ..Default::default()
2986 }];
2987
2988 let flat = flatten_task_tree(&tasks);
2989 assert_eq!(flat.len(), 1);
2990 assert_eq!(flat[0].explicit_parent_id, Some(Some(99)));
2991 }
2992
2993 #[test]
2994 fn test_flatten_propagates_null_parent_id() {
2995 let tasks = vec![TaskTree {
2996 name: Some("Explicit root task".to_string()),
2997 spec: None,
2998 priority: None,
2999 children: None,
3000 depends_on: None,
3001 id: None,
3002 status: None,
3003 active_form: None,
3004 parent_id: Some(None), ..Default::default()
3006 }];
3007
3008 let flat = flatten_task_tree(&tasks);
3009 assert_eq!(flat.len(), 1);
3010 assert_eq!(flat[0].explicit_parent_id, Some(None));
3011 }
3012
3013 #[tokio::test]
3014 async fn test_explicit_parent_id_sets_parent() {
3015 let ctx = TestContext::new().await;
3016
3017 let request1 = PlanRequest {
3019 tasks: vec![TaskTree {
3020 name: Some("Parent Task".to_string()),
3021 spec: Some("This is the parent".to_string()),
3022 priority: None,
3023 children: None,
3024 depends_on: None,
3025 id: None,
3026 status: Some(TaskStatus::Doing),
3027 active_form: None,
3028 parent_id: None,
3029 ..Default::default()
3030 }],
3031 };
3032
3033 let executor = PlanExecutor::new(&ctx.pool);
3034 let result1 = executor.execute(&request1).await.unwrap();
3035 assert!(result1.success);
3036 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
3037
3038 let request2 = PlanRequest {
3040 tasks: vec![TaskTree {
3041 name: Some("Child Task".to_string()),
3042 spec: Some("This uses explicit parent_id".to_string()),
3043 priority: None,
3044 children: None,
3045 depends_on: None,
3046 id: None,
3047 status: None,
3048 active_form: None,
3049 parent_id: Some(Some(parent_id)),
3050 ..Default::default()
3051 }],
3052 };
3053
3054 let result2 = executor.execute(&request2).await.unwrap();
3055 assert!(result2.success);
3056 let child_id = *result2.task_id_map.get("Child Task").unwrap();
3057
3058 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3060 .bind(child_id)
3061 .fetch_one(&ctx.pool)
3062 .await
3063 .unwrap();
3064 assert_eq!(row.0, Some(parent_id));
3065 }
3066
3067 #[tokio::test]
3068 async fn test_explicit_null_parent_id_creates_root() {
3069 let ctx = TestContext::new().await;
3070
3071 let request = PlanRequest {
3074 tasks: vec![TaskTree {
3075 name: Some("Explicit Root Task".to_string()),
3076 spec: Some("Should be root despite default parent".to_string()),
3077 priority: None,
3078 children: None,
3079 depends_on: None,
3080 id: None,
3081 status: Some(TaskStatus::Doing),
3082 active_form: None,
3083 parent_id: Some(None), ..Default::default()
3085 }],
3086 };
3087
3088 let parent_request = PlanRequest {
3091 tasks: vec![TaskTree {
3092 name: Some("Default Parent".to_string()),
3093 spec: None,
3094 priority: None,
3095 children: None,
3096 depends_on: None,
3097 id: None,
3098 status: None,
3099 active_form: None,
3100 parent_id: None,
3101 ..Default::default()
3102 }],
3103 };
3104 let executor = PlanExecutor::new(&ctx.pool);
3105 let parent_result = executor.execute(&parent_request).await.unwrap();
3106 let default_parent_id = *parent_result.task_id_map.get("Default Parent").unwrap();
3107
3108 let executor_with_default =
3110 PlanExecutor::new(&ctx.pool).with_default_parent(default_parent_id);
3111 let result = executor_with_default.execute(&request).await.unwrap();
3112 assert!(result.success);
3113 let task_id = *result.task_id_map.get("Explicit Root Task").unwrap();
3114
3115 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3117 .bind(task_id)
3118 .fetch_one(&ctx.pool)
3119 .await
3120 .unwrap();
3121 assert_eq!(
3122 row.0, None,
3123 "Task with explicit null parent_id should be root"
3124 );
3125 }
3126
3127 #[tokio::test]
3128 async fn test_children_nesting_takes_precedence_over_parent_id() {
3129 let ctx = TestContext::new().await;
3130
3131 let request = PlanRequest {
3133 tasks: vec![TaskTree {
3134 name: Some("Parent via Nesting".to_string()),
3135 spec: Some("Test parent spec".to_string()),
3136 priority: None,
3137 children: Some(vec![TaskTree {
3138 name: Some("Child via Nesting".to_string()),
3139 spec: None,
3140 priority: None,
3141 children: None,
3142 depends_on: None,
3143 id: None,
3144 status: None,
3145 active_form: None,
3146 parent_id: Some(Some(999)), ..Default::default()
3148 }]),
3149 depends_on: None,
3150 id: None,
3151 status: Some(TaskStatus::Doing),
3152 active_form: None,
3153 parent_id: None,
3154 ..Default::default()
3155 }],
3156 };
3157
3158 let executor = PlanExecutor::new(&ctx.pool);
3159 let result = executor.execute(&request).await.unwrap();
3160 assert!(result.success);
3161
3162 let parent_id = *result.task_id_map.get("Parent via Nesting").unwrap();
3163 let child_id = *result.task_id_map.get("Child via Nesting").unwrap();
3164
3165 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3167 .bind(child_id)
3168 .fetch_one(&ctx.pool)
3169 .await
3170 .unwrap();
3171 assert_eq!(
3172 row.0,
3173 Some(parent_id),
3174 "Children nesting should take precedence"
3175 );
3176 }
3177
3178 #[tokio::test]
3179 async fn test_modify_existing_task_parent() {
3180 let ctx = TestContext::new().await;
3181 let executor = PlanExecutor::new(&ctx.pool);
3182
3183 let request1 = PlanRequest {
3185 tasks: vec![
3186 TaskTree {
3187 name: Some("Task A".to_string()),
3188 spec: Some("Task A spec".to_string()),
3189 priority: None,
3190 children: None,
3191 depends_on: None,
3192 id: None,
3193 status: Some(TaskStatus::Doing),
3194 active_form: None,
3195 parent_id: None,
3196 ..Default::default()
3197 },
3198 TaskTree {
3199 name: Some("Task B".to_string()),
3200 spec: None,
3201 priority: None,
3202 children: None,
3203 depends_on: None,
3204 id: None,
3205 status: None,
3206 active_form: None,
3207 parent_id: None,
3208 ..Default::default()
3209 },
3210 ],
3211 };
3212
3213 let result1 = executor.execute(&request1).await.unwrap();
3214 assert!(result1.success);
3215 let task_a_id = *result1.task_id_map.get("Task A").unwrap();
3216 let task_b_id = *result1.task_id_map.get("Task B").unwrap();
3217
3218 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3220 .bind(task_b_id)
3221 .fetch_one(&ctx.pool)
3222 .await
3223 .unwrap();
3224 assert_eq!(row.0, None, "Task B should initially be root");
3225
3226 let request2 = PlanRequest {
3228 tasks: vec![TaskTree {
3229 name: Some("Task B".to_string()), spec: None,
3231 priority: None,
3232 children: None,
3233 depends_on: None,
3234 id: None,
3235 status: None,
3236 active_form: None,
3237 parent_id: Some(Some(task_a_id)), ..Default::default()
3239 }],
3240 };
3241
3242 let result2 = executor.execute(&request2).await.unwrap();
3243 assert!(result2.success);
3244 assert_eq!(result2.updated_count, 1, "Should update existing task");
3245
3246 let row: (Option<i64>,) = sqlx::query_as("SELECT parent_id FROM tasks WHERE id = ?")
3248 .bind(task_b_id)
3249 .fetch_one(&ctx.pool)
3250 .await
3251 .unwrap();
3252 assert_eq!(
3253 row.0,
3254 Some(task_a_id),
3255 "Task B should now be child of Task A"
3256 );
3257 }
3258
3259 #[tokio::test]
3260 async fn test_plan_done_with_incomplete_children_fails() {
3261 let ctx = TestContext::new().await;
3262 let executor = PlanExecutor::new(&ctx.pool);
3263
3264 let request1 = PlanRequest {
3266 tasks: vec![TaskTree {
3267 name: Some("Parent Task".to_string()),
3268 spec: Some("Parent spec".to_string()),
3269 priority: None,
3270 children: Some(vec![TaskTree {
3271 name: Some("Child Task".to_string()),
3272 spec: None,
3273 priority: None,
3274 children: None,
3275 depends_on: None,
3276 id: None,
3277 status: Some(TaskStatus::Todo), active_form: None,
3279 parent_id: None,
3280 ..Default::default()
3281 }]),
3282 depends_on: None,
3283 id: None,
3284 status: Some(TaskStatus::Doing),
3285 active_form: None,
3286 parent_id: None,
3287 ..Default::default()
3288 }],
3289 };
3290
3291 let result1 = executor.execute(&request1).await.unwrap();
3292 assert!(result1.success);
3293
3294 let request2 = PlanRequest {
3296 tasks: vec![TaskTree {
3297 name: Some("Parent Task".to_string()),
3298 spec: None,
3299 priority: None,
3300 children: None,
3301 depends_on: None,
3302 id: None,
3303 status: Some(TaskStatus::Done), active_form: None,
3305 parent_id: None,
3306 ..Default::default()
3307 }],
3308 };
3309
3310 let result2 = executor.execute(&request2).await.unwrap();
3311 assert!(!result2.success, "Should fail when child is incomplete");
3312 assert!(
3313 result2
3314 .error
3315 .as_ref()
3316 .unwrap()
3317 .contains("Uncompleted children"),
3318 "Error should mention uncompleted children: {:?}",
3319 result2.error
3320 );
3321 }
3322
3323 #[tokio::test]
3324 async fn test_plan_done_with_completed_children_succeeds() {
3325 let ctx = TestContext::new().await;
3326 let executor = PlanExecutor::new(&ctx.pool);
3327
3328 let request1 = PlanRequest {
3330 tasks: vec![TaskTree {
3331 name: Some("Parent Task".to_string()),
3332 spec: Some("Parent spec".to_string()),
3333 priority: None,
3334 children: Some(vec![TaskTree {
3335 name: Some("Child Task".to_string()),
3336 spec: None,
3337 priority: None,
3338 children: None,
3339 depends_on: None,
3340 id: None,
3341 status: Some(TaskStatus::Todo),
3342 active_form: None,
3343 parent_id: None,
3344 ..Default::default()
3345 }]),
3346 depends_on: None,
3347 id: None,
3348 status: Some(TaskStatus::Doing),
3349 active_form: None,
3350 parent_id: None,
3351 ..Default::default()
3352 }],
3353 };
3354
3355 let result1 = executor.execute(&request1).await.unwrap();
3356 assert!(result1.success);
3357
3358 let request2 = PlanRequest {
3360 tasks: vec![TaskTree {
3361 name: Some("Child Task".to_string()),
3362 spec: None,
3363 priority: None,
3364 children: None,
3365 depends_on: None,
3366 id: None,
3367 status: Some(TaskStatus::Done),
3368 active_form: None,
3369 parent_id: None,
3370 ..Default::default()
3371 }],
3372 };
3373
3374 let result2 = executor.execute(&request2).await.unwrap();
3375 assert!(result2.success);
3376
3377 let request3 = PlanRequest {
3379 tasks: vec![TaskTree {
3380 name: Some("Parent Task".to_string()),
3381 spec: None,
3382 priority: None,
3383 children: None,
3384 depends_on: None,
3385 id: None,
3386 status: Some(TaskStatus::Done),
3387 active_form: None,
3388 parent_id: None,
3389 ..Default::default()
3390 }],
3391 };
3392
3393 let result3 = executor.execute(&request3).await.unwrap();
3394 assert!(result3.success, "Should succeed when child is complete");
3395 }
3396}
3397
3398#[cfg(test)]
3399mod delete_tests {
3400 use super::*;
3401 use crate::test_utils::test_helpers::TestContext;
3402 use serial_test::serial;
3403
3404 #[tokio::test]
3405 async fn test_delete_task_by_id_only() {
3406 let ctx = TestContext::new().await;
3407 let executor = PlanExecutor::new(&ctx.pool);
3408
3409 let request1 = PlanRequest {
3411 tasks: vec![TaskTree {
3412 name: Some("Task to delete".to_string()),
3413 spec: Some("This will be deleted".to_string()),
3414 priority: None,
3415 children: None,
3416 depends_on: None,
3417 id: None,
3418 status: None,
3419 active_form: None,
3420 parent_id: None,
3421 ..Default::default()
3422 }],
3423 };
3424
3425 let result1 = executor.execute(&request1).await.unwrap();
3426 assert!(result1.success);
3427 let task_id = *result1.task_id_map.get("Task to delete").unwrap();
3428
3429 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
3431 .bind(task_id)
3432 .fetch_one(&ctx.pool)
3433 .await
3434 .unwrap();
3435 assert_eq!(exists.0, 1, "Task should exist");
3436
3437 let request2 = PlanRequest {
3439 tasks: vec![TaskTree {
3440 name: None, spec: None,
3442 priority: None,
3443 children: None,
3444 depends_on: None,
3445 id: Some(task_id),
3446 status: None,
3447 active_form: None,
3448 parent_id: None,
3449 delete: Some(true),
3450 }],
3451 };
3452
3453 let result2 = executor.execute(&request2).await.unwrap();
3454 assert!(result2.success, "Delete should succeed");
3455 assert_eq!(result2.deleted_count, 1, "Should delete 1 task");
3456 assert_eq!(result2.created_count, 0);
3457 assert_eq!(result2.updated_count, 0);
3458
3459 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
3461 .bind(task_id)
3462 .fetch_one(&ctx.pool)
3463 .await
3464 .unwrap();
3465 assert_eq!(exists.0, 0, "Task should be deleted");
3466 }
3467
3468 #[tokio::test]
3469 async fn test_delete_requires_id() {
3470 let ctx = TestContext::new().await;
3471 let executor = PlanExecutor::new(&ctx.pool);
3472
3473 let request = PlanRequest {
3475 tasks: vec![TaskTree {
3476 name: Some("Task name without id".to_string()),
3477 spec: None,
3478 priority: None,
3479 children: None,
3480 depends_on: None,
3481 id: None, status: None,
3483 active_form: None,
3484 parent_id: None,
3485 delete: Some(true),
3486 }],
3487 };
3488
3489 let result = executor.execute(&request).await.unwrap();
3490 assert!(!result.success, "Delete without id should fail");
3491 assert!(
3492 result.error.as_ref().unwrap().contains("id"),
3493 "Error should mention 'id' requirement"
3494 );
3495 }
3496
3497 #[tokio::test]
3498 async fn test_delete_with_json_syntax() {
3499 let ctx = TestContext::new().await;
3500 let executor = PlanExecutor::new(&ctx.pool);
3501
3502 let request1 = PlanRequest {
3504 tasks: vec![TaskTree {
3505 name: Some("JSON delete test".to_string()),
3506 spec: None,
3507 priority: None,
3508 children: None,
3509 depends_on: None,
3510 id: None,
3511 status: None,
3512 active_form: None,
3513 parent_id: None,
3514 ..Default::default()
3515 }],
3516 };
3517
3518 let result1 = executor.execute(&request1).await.unwrap();
3519 assert!(result1.success);
3520 let task_id = *result1.task_id_map.get("JSON delete test").unwrap();
3521
3522 let json = format!(r#"{{"tasks": [{{"id": {}, "delete": true}}]}}"#, task_id);
3524 let request2: PlanRequest = serde_json::from_str(&json).unwrap();
3525
3526 let result2 = executor.execute(&request2).await.unwrap();
3527 assert!(result2.success, "Delete via JSON should succeed");
3528 assert_eq!(result2.deleted_count, 1);
3529
3530 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
3532 .bind(task_id)
3533 .fetch_one(&ctx.pool)
3534 .await
3535 .unwrap();
3536 assert_eq!(exists.0, 0, "Task should be deleted");
3537 }
3538
3539 #[tokio::test]
3540 async fn test_mixed_create_update_delete_in_one_request() {
3541 let ctx = TestContext::new().await;
3542 let executor = PlanExecutor::new(&ctx.pool);
3543
3544 let request1 = PlanRequest {
3546 tasks: vec![
3547 TaskTree {
3548 name: Some("To Update".to_string()),
3549 spec: Some("Original spec".to_string()),
3550 priority: None,
3551 children: None,
3552 depends_on: None,
3553 id: None,
3554 status: None,
3555 active_form: None,
3556 parent_id: None,
3557 ..Default::default()
3558 },
3559 TaskTree {
3560 name: Some("To Delete".to_string()),
3561 spec: None,
3562 priority: None,
3563 children: None,
3564 depends_on: None,
3565 id: None,
3566 status: None,
3567 active_form: None,
3568 parent_id: None,
3569 ..Default::default()
3570 },
3571 ],
3572 };
3573
3574 let result1 = executor.execute(&request1).await.unwrap();
3575 assert!(result1.success);
3576 let delete_id = *result1.task_id_map.get("To Delete").unwrap();
3577
3578 let request2 = PlanRequest {
3580 tasks: vec![
3581 TaskTree {
3583 name: Some("Newly Created".to_string()),
3584 spec: Some("Brand new".to_string()),
3585 priority: None,
3586 children: None,
3587 depends_on: None,
3588 id: None,
3589 status: None,
3590 active_form: None,
3591 parent_id: None,
3592 ..Default::default()
3593 },
3594 TaskTree {
3596 name: Some("To Update".to_string()),
3597 spec: Some("Updated spec".to_string()),
3598 priority: None,
3599 children: None,
3600 depends_on: None,
3601 id: None,
3602 status: None,
3603 active_form: None,
3604 parent_id: None,
3605 ..Default::default()
3606 },
3607 TaskTree {
3609 name: None,
3610 spec: None,
3611 priority: None,
3612 children: None,
3613 depends_on: None,
3614 id: Some(delete_id),
3615 status: None,
3616 active_form: None,
3617 parent_id: None,
3618 delete: Some(true),
3619 },
3620 ],
3621 };
3622
3623 let result2 = executor.execute(&request2).await.unwrap();
3624 assert!(result2.success);
3625 assert_eq!(result2.created_count, 1, "Should create 1 task");
3626 assert_eq!(result2.updated_count, 1, "Should update 1 task");
3627 assert_eq!(result2.deleted_count, 1, "Should delete 1 task");
3628
3629 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
3631 .bind(delete_id)
3632 .fetch_one(&ctx.pool)
3633 .await
3634 .unwrap();
3635 assert_eq!(exists.0, 0, "Deleted task should not exist");
3636 }
3637
3638 #[tokio::test]
3639 async fn test_delete_nonexistent_id_returns_warning() {
3640 let ctx = TestContext::new().await;
3641 let executor = PlanExecutor::new(&ctx.pool);
3642
3643 let request = PlanRequest {
3645 tasks: vec![TaskTree {
3646 name: None,
3647 spec: None,
3648 priority: None,
3649 children: None,
3650 depends_on: None,
3651 id: Some(99999), status: None,
3653 active_form: None,
3654 parent_id: None,
3655 delete: Some(true),
3656 }],
3657 };
3658
3659 let result = executor.execute(&request).await.unwrap();
3660
3661 assert!(
3663 result.success,
3664 "Delete of non-existent ID should still succeed"
3665 );
3666 assert_eq!(
3667 result.deleted_count, 0,
3668 "Should not count non-existent task as deleted"
3669 );
3670 assert!(
3671 !result.warnings.is_empty(),
3672 "Should have warning about non-existent task"
3673 );
3674 assert!(
3675 result.warnings[0].contains("not found"),
3676 "Warning should mention task not found: {:?}",
3677 result.warnings
3678 );
3679 }
3680
3681 #[tokio::test]
3682 async fn test_cascade_delete_reports_descendants() {
3683 let ctx = TestContext::new().await;
3684 let executor = PlanExecutor::new(&ctx.pool);
3685
3686 let request1 = PlanRequest {
3688 tasks: vec![TaskTree {
3689 name: Some("Parent Task".to_string()),
3690 spec: None,
3691 priority: None,
3692 children: Some(vec![
3693 TaskTree {
3694 name: Some("Child 1".to_string()),
3695 spec: None,
3696 priority: None,
3697 children: None,
3698 depends_on: None,
3699 id: None,
3700 status: None,
3701 active_form: None,
3702 parent_id: None,
3703 ..Default::default()
3704 },
3705 TaskTree {
3706 name: Some("Child 2".to_string()),
3707 spec: None,
3708 priority: None,
3709 children: None,
3710 depends_on: None,
3711 id: None,
3712 status: None,
3713 active_form: None,
3714 parent_id: None,
3715 ..Default::default()
3716 },
3717 ]),
3718 depends_on: None,
3719 id: None,
3720 status: None,
3721 active_form: None,
3722 parent_id: None,
3723 ..Default::default()
3724 }],
3725 };
3726
3727 let result1 = executor.execute(&request1).await.unwrap();
3728 assert!(result1.success);
3729 assert_eq!(
3730 result1.created_count, 3,
3731 "Should create parent + 2 children"
3732 );
3733 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
3734
3735 let request2 = PlanRequest {
3737 tasks: vec![TaskTree {
3738 name: None,
3739 spec: None,
3740 priority: None,
3741 children: None,
3742 depends_on: None,
3743 id: Some(parent_id),
3744 status: None,
3745 active_form: None,
3746 parent_id: None,
3747 delete: Some(true),
3748 }],
3749 };
3750
3751 let result2 = executor.execute(&request2).await.unwrap();
3752
3753 assert!(result2.success, "Cascade delete should succeed");
3754 assert_eq!(
3755 result2.deleted_count, 1,
3756 "deleted_count should only count direct deletes"
3757 );
3758 assert_eq!(
3759 result2.cascade_deleted_count, 2,
3760 "Should report 2 cascade-deleted children"
3761 );
3762 assert!(
3763 result2.warnings.iter().any(|w| w.contains("descendant")),
3764 "Should have warning about cascade-deleted descendants: {:?}",
3765 result2.warnings
3766 );
3767
3768 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
3770 .fetch_one(&ctx.pool)
3771 .await
3772 .unwrap();
3773 assert_eq!(count.0, 0, "All tasks should be deleted");
3774 }
3775
3776 #[tokio::test]
3777 async fn test_cascade_delete_deep_hierarchy() {
3778 let ctx = TestContext::new().await;
3779 let executor = PlanExecutor::new(&ctx.pool);
3780
3781 let request1 = PlanRequest {
3783 tasks: vec![TaskTree {
3784 name: Some("Root".to_string()),
3785 spec: None,
3786 priority: None,
3787 children: Some(vec![TaskTree {
3788 name: Some("Level1".to_string()),
3789 spec: None,
3790 priority: None,
3791 children: Some(vec![TaskTree {
3792 name: Some("Level2".to_string()),
3793 spec: None,
3794 priority: None,
3795 children: Some(vec![TaskTree {
3796 name: Some("Level3".to_string()),
3797 spec: None,
3798 priority: None,
3799 children: None,
3800 depends_on: None,
3801 id: None,
3802 status: None,
3803 active_form: None,
3804 parent_id: None,
3805 ..Default::default()
3806 }]),
3807 depends_on: None,
3808 id: None,
3809 status: None,
3810 active_form: None,
3811 parent_id: None,
3812 ..Default::default()
3813 }]),
3814 depends_on: None,
3815 id: None,
3816 status: None,
3817 active_form: None,
3818 parent_id: None,
3819 ..Default::default()
3820 }]),
3821 depends_on: None,
3822 id: None,
3823 status: None,
3824 active_form: None,
3825 parent_id: None,
3826 ..Default::default()
3827 }],
3828 };
3829
3830 let result1 = executor.execute(&request1).await.unwrap();
3831 assert!(result1.success);
3832 assert_eq!(result1.created_count, 4);
3833 let root_id = *result1.task_id_map.get("Root").unwrap();
3834
3835 let request2 = PlanRequest {
3837 tasks: vec![TaskTree {
3838 name: None,
3839 spec: None,
3840 priority: None,
3841 children: None,
3842 depends_on: None,
3843 id: Some(root_id),
3844 status: None,
3845 active_form: None,
3846 parent_id: None,
3847 delete: Some(true),
3848 }],
3849 };
3850
3851 let result2 = executor.execute(&request2).await.unwrap();
3852
3853 assert!(result2.success);
3854 assert_eq!(
3855 result2.deleted_count, 1,
3856 "Only root counted as direct delete"
3857 );
3858 assert_eq!(
3859 result2.cascade_deleted_count, 3,
3860 "Should cascade-delete 3 descendants"
3861 );
3862 }
3863
3864 #[tokio::test]
3865 async fn test_delete_multiple_ids_with_mixed_results() {
3866 let ctx = TestContext::new().await;
3867 let executor = PlanExecutor::new(&ctx.pool);
3868
3869 let request1 = PlanRequest {
3871 tasks: vec![TaskTree {
3872 name: Some("Existing Task".to_string()),
3873 spec: None,
3874 priority: None,
3875 children: None,
3876 depends_on: None,
3877 id: None,
3878 status: None,
3879 active_form: None,
3880 parent_id: None,
3881 ..Default::default()
3882 }],
3883 };
3884
3885 let result1 = executor.execute(&request1).await.unwrap();
3886 let existing_id = *result1.task_id_map.get("Existing Task").unwrap();
3887
3888 let request2 = PlanRequest {
3890 tasks: vec![
3891 TaskTree {
3892 name: None,
3893 spec: None,
3894 priority: None,
3895 children: None,
3896 depends_on: None,
3897 id: Some(existing_id),
3898 status: None,
3899 active_form: None,
3900 parent_id: None,
3901 delete: Some(true),
3902 },
3903 TaskTree {
3904 name: None,
3905 spec: None,
3906 priority: None,
3907 children: None,
3908 depends_on: None,
3909 id: Some(88888), status: None,
3911 active_form: None,
3912 parent_id: None,
3913 delete: Some(true),
3914 },
3915 ],
3916 };
3917
3918 let result2 = executor.execute(&request2).await.unwrap();
3919
3920 assert!(result2.success, "Mixed delete should still succeed");
3921 assert_eq!(result2.deleted_count, 1, "Only one task actually deleted");
3922 assert!(
3923 result2
3924 .warnings
3925 .iter()
3926 .any(|w| w.contains("88888") && w.contains("not found")),
3927 "Should warn about non-existent ID 88888: {:?}",
3928 result2.warnings
3929 );
3930 }
3931
3932 #[tokio::test]
3934 #[serial]
3935 async fn test_delete_focused_task_returns_error() {
3936 let ctx = TestContext::new().await;
3937 let executor = PlanExecutor::new(&ctx.pool);
3938
3939 let test_session_id = format!("test-delete-focus-{}", std::process::id());
3941 std::env::set_var("IE_SESSION_ID", &test_session_id);
3942
3943 let request1 = PlanRequest {
3945 tasks: vec![TaskTree {
3946 name: Some("Focused Task".to_string()),
3947 spec: Some("## Goal\nTest focus deletion".to_string()),
3948 priority: None,
3949 children: None,
3950 depends_on: None,
3951 id: None,
3952 status: Some(TaskStatus::Doing),
3953 active_form: None,
3954 parent_id: None,
3955 ..Default::default()
3956 }],
3957 };
3958
3959 let result1 = executor.execute(&request1).await.unwrap();
3960 assert!(result1.success, "Create focused task should succeed");
3961 let task_id = *result1.task_id_map.get("Focused Task").unwrap();
3962
3963 let focus_check: Option<(i64,)> =
3965 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = ?")
3966 .bind(&test_session_id)
3967 .fetch_optional(&ctx.pool)
3968 .await
3969 .unwrap();
3970 assert_eq!(
3971 focus_check.map(|r| r.0),
3972 Some(task_id),
3973 "Task should be the session's current focus"
3974 );
3975
3976 let request2 = PlanRequest {
3978 tasks: vec![TaskTree {
3979 name: None,
3980 spec: None,
3981 priority: None,
3982 children: None,
3983 depends_on: None,
3984 id: Some(task_id),
3985 status: None,
3986 active_form: None,
3987 parent_id: None,
3988 delete: Some(true),
3989 }],
3990 };
3991
3992 let result2 = executor.execute(&request2).await.unwrap();
3993
3994 assert!(!result2.success, "Delete focused task should fail");
3996 let error = result2.error.as_ref().unwrap();
3997 assert!(
3998 error.contains("focus") && error.contains(&test_session_id),
3999 "Error should mention focus and session: {}",
4000 error
4001 );
4002 assert_eq!(result2.deleted_count, 0, "Nothing should be deleted");
4003
4004 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
4006 .bind(task_id)
4007 .fetch_one(&ctx.pool)
4008 .await
4009 .unwrap();
4010 assert_eq!(exists.0, 1, "Focused task should NOT be deleted");
4011
4012 std::env::remove_var("IE_SESSION_ID");
4014 }
4015
4016 #[tokio::test]
4018 async fn test_delete_duplicate_id_in_batch() {
4019 let ctx = TestContext::new().await;
4020 let executor = PlanExecutor::new(&ctx.pool);
4021
4022 let request1 = PlanRequest {
4024 tasks: vec![TaskTree {
4025 name: Some("Duplicate Delete Target".to_string()),
4026 spec: None,
4027 priority: None,
4028 children: None,
4029 depends_on: None,
4030 id: None,
4031 status: None,
4032 active_form: None,
4033 parent_id: None,
4034 ..Default::default()
4035 }],
4036 };
4037
4038 let result1 = executor.execute(&request1).await.unwrap();
4039 assert!(result1.success);
4040 let task_id = *result1.task_id_map.get("Duplicate Delete Target").unwrap();
4041
4042 let request2 = PlanRequest {
4044 tasks: vec![
4045 TaskTree {
4046 name: None,
4047 spec: None,
4048 priority: None,
4049 children: None,
4050 depends_on: None,
4051 id: Some(task_id),
4052 status: None,
4053 active_form: None,
4054 parent_id: None,
4055 delete: Some(true),
4056 },
4057 TaskTree {
4058 name: None,
4059 spec: None,
4060 priority: None,
4061 children: None,
4062 depends_on: None,
4063 id: Some(task_id), status: None,
4065 active_form: None,
4066 parent_id: None,
4067 delete: Some(true),
4068 },
4069 ],
4070 };
4071
4072 let result2 = executor.execute(&request2).await.unwrap();
4073
4074 assert!(result2.success, "Duplicate delete should still succeed");
4076 assert_eq!(
4077 result2.deleted_count, 1,
4078 "Only the first delete should count"
4079 );
4080
4081 let not_found_warnings: Vec<_> = result2
4083 .warnings
4084 .iter()
4085 .filter(|w| w.contains("not found"))
4086 .collect();
4087 assert_eq!(
4088 not_found_warnings.len(),
4089 1,
4090 "Should have exactly one 'not found' warning for the duplicate: {:?}",
4091 result2.warnings
4092 );
4093
4094 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
4096 .bind(task_id)
4097 .fetch_one(&ctx.pool)
4098 .await
4099 .unwrap();
4100 assert_eq!(exists.0, 0, "Task should be deleted");
4101 }
4102
4103 #[tokio::test]
4106 #[serial]
4107 async fn test_delete_parent_blocked_when_child_is_focused() {
4108 let ctx = TestContext::new().await;
4109 let executor = PlanExecutor::new(&ctx.pool);
4110
4111 let test_session_id = format!("test-cascade-focus-{}", std::process::id());
4113 std::env::set_var("IE_SESSION_ID", &test_session_id);
4114
4115 let request1 = PlanRequest {
4117 tasks: vec![TaskTree {
4118 name: Some("Parent Task".to_string()),
4119 spec: None,
4120 priority: None,
4121 children: Some(vec![TaskTree {
4122 name: Some("Child Task".to_string()),
4123 spec: Some("## Goal\nChild is focused".to_string()),
4124 priority: None,
4125 children: None,
4126 depends_on: None,
4127 id: None,
4128 status: Some(TaskStatus::Doing), active_form: None,
4130 parent_id: None,
4131 ..Default::default()
4132 }]),
4133 depends_on: None,
4134 id: None,
4135 status: None,
4136 active_form: None,
4137 parent_id: None,
4138 ..Default::default()
4139 }],
4140 };
4141
4142 let result1 = executor.execute(&request1).await.unwrap();
4143 assert!(result1.success, "Create hierarchy should succeed");
4144 let parent_id = *result1.task_id_map.get("Parent Task").unwrap();
4145 let child_id = *result1.task_id_map.get("Child Task").unwrap();
4146
4147 let focus_check: Option<(i64,)> =
4149 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = ?")
4150 .bind(&test_session_id)
4151 .fetch_optional(&ctx.pool)
4152 .await
4153 .unwrap();
4154 assert_eq!(
4155 focus_check.map(|r| r.0),
4156 Some(child_id),
4157 "Child should be the session's focus"
4158 );
4159
4160 let request2 = PlanRequest {
4162 tasks: vec![TaskTree {
4163 name: None,
4164 spec: None,
4165 priority: None,
4166 children: None,
4167 depends_on: None,
4168 id: Some(parent_id),
4169 status: None,
4170 active_form: None,
4171 parent_id: None,
4172 delete: Some(true),
4173 }],
4174 };
4175
4176 let result2 = executor.execute(&request2).await.unwrap();
4177
4178 assert!(
4180 !result2.success,
4181 "Delete parent should fail when child is focused"
4182 );
4183 let error = result2.error.as_ref().unwrap();
4184 assert!(
4185 error.contains("cascade"),
4186 "Error should mention cascade: {}",
4187 error
4188 );
4189 assert!(
4190 error.contains(&child_id.to_string()),
4191 "Error should mention child task ID {}: {}",
4192 child_id,
4193 error
4194 );
4195
4196 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
4198 .fetch_one(&ctx.pool)
4199 .await
4200 .unwrap();
4201 assert_eq!(count.0, 2, "Both tasks should still exist");
4202
4203 std::env::remove_var("IE_SESSION_ID");
4205 }
4206
4207 #[tokio::test]
4210 #[serial]
4211 async fn test_batch_delete_blocked_when_subtree_contains_focus() {
4212 let ctx = TestContext::new().await;
4213 let executor = PlanExecutor::new(&ctx.pool);
4214
4215 let test_session_id = format!("test-batch-focus-{}", std::process::id());
4217 std::env::set_var("IE_SESSION_ID", &test_session_id);
4218
4219 let request1 = PlanRequest {
4221 tasks: vec![TaskTree {
4222 name: Some("BatchParent".to_string()),
4223 spec: None,
4224 priority: None,
4225 children: Some(vec![TaskTree {
4226 name: Some("BatchChild".to_string()),
4227 spec: Some("## Goal\nFocused child".to_string()),
4228 priority: None,
4229 children: None,
4230 depends_on: None,
4231 id: None,
4232 status: Some(TaskStatus::Doing),
4233 active_form: None,
4234 parent_id: None,
4235 ..Default::default()
4236 }]),
4237 depends_on: None,
4238 id: None,
4239 status: None,
4240 active_form: None,
4241 parent_id: None,
4242 ..Default::default()
4243 }],
4244 };
4245
4246 let result1 = executor.execute(&request1).await.unwrap();
4247 assert!(result1.success);
4248 let parent_id = *result1.task_id_map.get("BatchParent").unwrap();
4249 let child_id = *result1.task_id_map.get("BatchChild").unwrap();
4250
4251 let request2 = PlanRequest {
4254 tasks: vec![
4255 TaskTree {
4256 name: None,
4257 spec: None,
4258 priority: None,
4259 children: None,
4260 depends_on: None,
4261 id: Some(parent_id),
4262 status: None,
4263 active_form: None,
4264 parent_id: None,
4265 delete: Some(true),
4266 },
4267 TaskTree {
4268 name: None,
4269 spec: None,
4270 priority: None,
4271 children: None,
4272 depends_on: None,
4273 id: Some(child_id),
4274 status: None,
4275 active_form: None,
4276 parent_id: None,
4277 delete: Some(true),
4278 },
4279 ],
4280 };
4281
4282 let result2 = executor.execute(&request2).await.unwrap();
4283
4284 assert!(!result2.success, "Batch delete should fail");
4286 assert!(
4287 result2.error.as_ref().unwrap().contains("focus"),
4288 "Error should mention focus: {:?}",
4289 result2.error
4290 );
4291 assert_eq!(result2.deleted_count, 0, "Nothing should be deleted");
4292
4293 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
4295 .fetch_one(&ctx.pool)
4296 .await
4297 .unwrap();
4298 assert_eq!(count.0, 2, "Both tasks should still exist");
4299
4300 std::env::remove_var("IE_SESSION_ID");
4302 }
4303
4304 #[tokio::test]
4306 #[serial]
4307 async fn test_delete_blocked_when_deep_descendant_is_focused() {
4308 let ctx = TestContext::new().await;
4309 let executor = PlanExecutor::new(&ctx.pool);
4310
4311 let test_session_id = format!("test-deep-focus-{}", std::process::id());
4312 std::env::set_var("IE_SESSION_ID", &test_session_id);
4313
4314 let request1 = PlanRequest {
4316 tasks: vec![TaskTree {
4317 name: Some("Root".to_string()),
4318 spec: None,
4319 priority: None,
4320 children: Some(vec![TaskTree {
4321 name: Some("Level1".to_string()),
4322 spec: None,
4323 priority: None,
4324 children: Some(vec![TaskTree {
4325 name: Some("Level2".to_string()),
4326 spec: None,
4327 priority: None,
4328 children: Some(vec![TaskTree {
4329 name: Some("Level3".to_string()),
4330 spec: Some("## Goal\nDeep focused task".to_string()),
4331 priority: None,
4332 children: None,
4333 depends_on: None,
4334 id: None,
4335 status: Some(TaskStatus::Doing),
4336 active_form: None,
4337 parent_id: None,
4338 ..Default::default()
4339 }]),
4340 depends_on: None,
4341 id: None,
4342 status: None,
4343 active_form: None,
4344 parent_id: None,
4345 ..Default::default()
4346 }]),
4347 depends_on: None,
4348 id: None,
4349 status: None,
4350 active_form: None,
4351 parent_id: None,
4352 ..Default::default()
4353 }]),
4354 depends_on: None,
4355 id: None,
4356 status: None,
4357 active_form: None,
4358 parent_id: None,
4359 ..Default::default()
4360 }],
4361 };
4362
4363 let result1 = executor.execute(&request1).await.unwrap();
4364 assert!(result1.success);
4365 let root_id = *result1.task_id_map.get("Root").unwrap();
4366 let level3_id = *result1.task_id_map.get("Level3").unwrap();
4367
4368 let focus_check: Option<(i64,)> =
4370 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = ?")
4371 .bind(&test_session_id)
4372 .fetch_optional(&ctx.pool)
4373 .await
4374 .unwrap();
4375 assert_eq!(focus_check.map(|r| r.0), Some(level3_id));
4376
4377 let request2 = PlanRequest {
4379 tasks: vec![TaskTree {
4380 name: None,
4381 spec: None,
4382 priority: None,
4383 children: None,
4384 depends_on: None,
4385 id: Some(root_id),
4386 status: None,
4387 active_form: None,
4388 parent_id: None,
4389 delete: Some(true),
4390 }],
4391 };
4392
4393 let result2 = executor.execute(&request2).await.unwrap();
4394
4395 assert!(
4396 !result2.success,
4397 "Delete root should fail when deep descendant is focused"
4398 );
4399 let error = result2.error.as_ref().unwrap();
4400 assert!(
4401 error.contains("cascade"),
4402 "Error should mention cascade: {}",
4403 error
4404 );
4405 assert!(
4406 error.contains(&level3_id.to_string()),
4407 "Error should mention Level3 ID {}: {}",
4408 level3_id,
4409 error
4410 );
4411
4412 let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks")
4414 .fetch_one(&ctx.pool)
4415 .await
4416 .unwrap();
4417 assert_eq!(count.0, 4, "All tasks should still exist");
4418
4419 std::env::remove_var("IE_SESSION_ID");
4420 }
4421
4422 #[tokio::test]
4425 async fn test_delete_nonexistent_task_subtree_check_succeeds() {
4426 let ctx = TestContext::new().await;
4427 let executor = PlanExecutor::new(&ctx.pool);
4428
4429 let request = PlanRequest {
4432 tasks: vec![TaskTree {
4433 name: None,
4434 spec: None,
4435 priority: None,
4436 children: None,
4437 depends_on: None,
4438 id: Some(99999), status: None,
4440 active_form: None,
4441 parent_id: None,
4442 delete: Some(true),
4443 }],
4444 };
4445
4446 let result = executor.execute(&request).await.unwrap();
4447
4448 assert!(result.success, "Delete of non-existent should succeed");
4450 assert_eq!(result.deleted_count, 0);
4451 assert!(
4452 result.warnings.iter().any(|w| w.contains("not found")),
4453 "Should have 'not found' warning: {:?}",
4454 result.warnings
4455 );
4456 }
4457
4458 #[tokio::test]
4461 #[serial]
4462 async fn test_default_session_focus_also_protected() {
4463 let ctx = TestContext::new().await;
4464 let executor = PlanExecutor::new(&ctx.pool);
4465
4466 std::env::remove_var("IE_SESSION_ID");
4468
4469 let request1 = PlanRequest {
4472 tasks: vec![TaskTree {
4473 name: Some("Default Session Task".to_string()),
4474 spec: Some("## Goal\nTest default session".to_string()),
4475 priority: None,
4476 children: None,
4477 depends_on: None,
4478 id: None,
4479 status: Some(TaskStatus::Doing),
4480 active_form: None,
4481 parent_id: None,
4482 ..Default::default()
4483 }],
4484 };
4485
4486 let result1 = executor.execute(&request1).await.unwrap();
4487 assert!(result1.success);
4488 let task_id = *result1.task_id_map.get("Default Session Task").unwrap();
4489
4490 let request2 = PlanRequest {
4492 tasks: vec![TaskTree {
4493 name: None,
4494 spec: None,
4495 priority: None,
4496 children: None,
4497 depends_on: None,
4498 id: Some(task_id),
4499 status: None,
4500 active_form: None,
4501 parent_id: None,
4502 delete: Some(true),
4503 }],
4504 };
4505
4506 let result2 = executor.execute(&request2).await.unwrap();
4507
4508 assert!(
4510 !result2.success,
4511 "Default session focus should be protected"
4512 );
4513 assert_eq!(result2.deleted_count, 0);
4514
4515 let error = result2.error.as_ref().unwrap();
4517 assert!(
4518 error.contains("-1"),
4519 "Error should mention default session '-1': {}",
4520 error
4521 );
4522
4523 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
4525 .bind(task_id)
4526 .fetch_one(&ctx.pool)
4527 .await
4528 .unwrap();
4529 assert_eq!(exists.0, 1, "Task should still exist");
4530 }
4531
4532 #[tokio::test]
4535 #[serial]
4536 async fn test_cross_session_delete_blocked() {
4537 let ctx = TestContext::new().await;
4538 let executor = PlanExecutor::new(&ctx.pool);
4539
4540 let session_a = "session-A-cross-test";
4542 std::env::set_var("IE_SESSION_ID", session_a);
4543
4544 let request1 = PlanRequest {
4545 tasks: vec![TaskTree {
4546 name: Some("Session A Focus".to_string()),
4547 spec: Some("## Goal\nSession A's task".to_string()),
4548 priority: None,
4549 children: None,
4550 depends_on: None,
4551 id: None,
4552 status: Some(TaskStatus::Doing),
4553 active_form: None,
4554 parent_id: None,
4555 ..Default::default()
4556 }],
4557 };
4558
4559 let result1 = executor.execute(&request1).await.unwrap();
4560 assert!(result1.success);
4561 let task_id = *result1.task_id_map.get("Session A Focus").unwrap();
4562
4563 let focus_a: Option<(i64,)> =
4565 sqlx::query_as("SELECT current_task_id FROM sessions WHERE session_id = ?")
4566 .bind(session_a)
4567 .fetch_optional(&ctx.pool)
4568 .await
4569 .unwrap();
4570 assert_eq!(
4571 focus_a.map(|r| r.0),
4572 Some(task_id),
4573 "Session A should have focus"
4574 );
4575
4576 let session_b = "session-B-cross-test";
4578 std::env::set_var("IE_SESSION_ID", session_b);
4579
4580 let request2 = PlanRequest {
4581 tasks: vec![TaskTree {
4582 name: None,
4583 spec: None,
4584 priority: None,
4585 children: None,
4586 depends_on: None,
4587 id: Some(task_id),
4588 status: None,
4589 active_form: None,
4590 parent_id: None,
4591 delete: Some(true),
4592 }],
4593 };
4594
4595 let result2 = executor.execute(&request2).await.unwrap();
4596
4597 assert!(
4599 !result2.success,
4600 "Session B should NOT be able to delete Session A's focus"
4601 );
4602 assert_eq!(result2.deleted_count, 0);
4603
4604 let error = result2.error.as_ref().unwrap();
4606 assert!(
4607 error.contains(session_a),
4608 "Error should mention session '{}': {}",
4609 session_a,
4610 error
4611 );
4612
4613 let exists: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tasks WHERE id = ?")
4615 .bind(task_id)
4616 .fetch_one(&ctx.pool)
4617 .await
4618 .unwrap();
4619 assert_eq!(exists.0, 1, "Task should still exist");
4620
4621 std::env::remove_var("IE_SESSION_ID");
4623 }
4624}