intent_engine/
plan.rs

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