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