Skip to main content

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