intent_engine/
tasks.rs

1use crate::db::models::{
2    CurrentTaskInfo, DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, ParentTaskInfo,
3    PickNextResponse, PreviousTaskInfo, SpawnSubtaskResponse, SubtaskInfo, SwitchTaskResponse,
4    Task, TaskContext, TaskSearchResult, TaskWithEvents, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::{Row, SqlitePool};
9
10pub struct TaskManager<'a> {
11    pool: &'a SqlitePool,
12}
13
14impl<'a> TaskManager<'a> {
15    pub fn new(pool: &'a SqlitePool) -> Self {
16        Self { pool }
17    }
18
19    /// Add a new task
20    pub async fn add_task(
21        &self,
22        name: &str,
23        spec: Option<&str>,
24        parent_id: Option<i64>,
25    ) -> Result<Task> {
26        // Check for circular dependency if parent_id is provided
27        if let Some(pid) = parent_id {
28            self.check_task_exists(pid).await?;
29        }
30
31        let now = Utc::now();
32
33        let result = sqlx::query(
34            r#"
35            INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
36            VALUES (?, ?, ?, 'todo', ?)
37            "#,
38        )
39        .bind(name)
40        .bind(spec)
41        .bind(parent_id)
42        .bind(now)
43        .execute(self.pool)
44        .await?;
45
46        let id = result.last_insert_rowid();
47        self.get_task(id).await
48    }
49
50    /// Get a task by ID
51    pub async fn get_task(&self, id: i64) -> Result<Task> {
52        let task = sqlx::query_as::<_, Task>(
53            r#"
54            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
55            FROM tasks
56            WHERE id = ?
57            "#,
58        )
59        .bind(id)
60        .fetch_optional(self.pool)
61        .await?
62        .ok_or(IntentError::TaskNotFound(id))?;
63
64        Ok(task)
65    }
66
67    /// Get a task with events summary
68    pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
69        let task = self.get_task(id).await?;
70        let events_summary = self.get_events_summary(id).await?;
71
72        Ok(TaskWithEvents {
73            task,
74            events_summary: Some(events_summary),
75        })
76    }
77
78    /// Get task context - the complete family tree of a task
79    ///
80    /// Returns:
81    /// - task: The requested task
82    /// - ancestors: Parent chain up to root (ordered from immediate parent to root)
83    /// - siblings: Other tasks at the same level (same parent_id)
84    /// - children: Direct subtasks of this task
85    pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
86        // Get the main task
87        let task = self.get_task(id).await?;
88
89        // Get ancestors (walk up parent chain)
90        let mut ancestors = Vec::new();
91        let mut current_parent_id = task.parent_id;
92
93        while let Some(parent_id) = current_parent_id {
94            let parent = self.get_task(parent_id).await?;
95            current_parent_id = parent.parent_id;
96            ancestors.push(parent);
97        }
98
99        // Get siblings (tasks with same parent_id)
100        let siblings = if let Some(parent_id) = task.parent_id {
101            sqlx::query_as::<_, Task>(
102                r#"
103                SELECT id, parent_id, name, spec, status, complexity, priority,
104                       first_todo_at, first_doing_at, first_done_at
105                FROM tasks
106                WHERE parent_id = ? AND id != ?
107                ORDER BY priority ASC NULLS LAST, id ASC
108                "#,
109            )
110            .bind(parent_id)
111            .bind(id)
112            .fetch_all(self.pool)
113            .await?
114        } else {
115            // For root tasks, get other root tasks as siblings
116            sqlx::query_as::<_, Task>(
117                r#"
118                SELECT id, parent_id, name, spec, status, complexity, priority,
119                       first_todo_at, first_doing_at, first_done_at
120                FROM tasks
121                WHERE parent_id IS NULL AND id != ?
122                ORDER BY priority ASC NULLS LAST, id ASC
123                "#,
124            )
125            .bind(id)
126            .fetch_all(self.pool)
127            .await?
128        };
129
130        // Get children (direct subtasks)
131        let children = sqlx::query_as::<_, Task>(
132            r#"
133            SELECT id, parent_id, name, spec, status, complexity, priority,
134                   first_todo_at, first_doing_at, first_done_at
135            FROM tasks
136            WHERE parent_id = ?
137            ORDER BY priority ASC NULLS LAST, id ASC
138            "#,
139        )
140        .bind(id)
141        .fetch_all(self.pool)
142        .await?;
143
144        Ok(TaskContext {
145            task,
146            ancestors,
147            siblings,
148            children,
149        })
150    }
151
152    /// Get events summary for a task
153    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
154        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
155            .bind(task_id)
156            .fetch_one(self.pool)
157            .await?;
158
159        let recent_events = sqlx::query_as::<_, Event>(
160            r#"
161            SELECT id, task_id, timestamp, log_type, discussion_data
162            FROM events
163            WHERE task_id = ?
164            ORDER BY timestamp DESC
165            LIMIT 10
166            "#,
167        )
168        .bind(task_id)
169        .fetch_all(self.pool)
170        .await?;
171
172        Ok(EventsSummary {
173            total_count,
174            recent_events,
175        })
176    }
177
178    /// Update a task
179    #[allow(clippy::too_many_arguments)]
180    pub async fn update_task(
181        &self,
182        id: i64,
183        name: Option<&str>,
184        spec: Option<&str>,
185        parent_id: Option<Option<i64>>,
186        status: Option<&str>,
187        complexity: Option<i32>,
188        priority: Option<i32>,
189    ) -> Result<Task> {
190        // Check task exists
191        let task = self.get_task(id).await?;
192
193        // Validate status if provided
194        if let Some(s) = status {
195            if !["todo", "doing", "done"].contains(&s) {
196                return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
197            }
198        }
199
200        // Check for circular dependency if parent_id is being changed
201        if let Some(Some(pid)) = parent_id {
202            if pid == id {
203                return Err(IntentError::CircularDependency);
204            }
205            self.check_task_exists(pid).await?;
206            self.check_circular_dependency(id, pid).await?;
207        }
208
209        // Build dynamic update query
210        let mut query = String::from("UPDATE tasks SET ");
211        let mut updates = Vec::new();
212
213        if let Some(n) = name {
214            updates.push(format!("name = '{}'", n.replace('\'', "''")));
215        }
216
217        if let Some(s) = spec {
218            updates.push(format!("spec = '{}'", s.replace('\'', "''")));
219        }
220
221        if let Some(pid) = parent_id {
222            match pid {
223                Some(p) => updates.push(format!("parent_id = {}", p)),
224                None => updates.push("parent_id = NULL".to_string()),
225            }
226        }
227
228        if let Some(c) = complexity {
229            updates.push(format!("complexity = {}", c));
230        }
231
232        if let Some(p) = priority {
233            updates.push(format!("priority = {}", p));
234        }
235
236        if let Some(s) = status {
237            updates.push(format!("status = '{}'", s));
238
239            // Update timestamp fields based on status
240            let now = Utc::now();
241            match s {
242                "todo" if task.first_todo_at.is_none() => {
243                    updates.push(format!("first_todo_at = '{}'", now.to_rfc3339()));
244                },
245                "doing" if task.first_doing_at.is_none() => {
246                    updates.push(format!("first_doing_at = '{}'", now.to_rfc3339()));
247                },
248                "done" if task.first_done_at.is_none() => {
249                    updates.push(format!("first_done_at = '{}'", now.to_rfc3339()));
250                },
251                _ => {},
252            }
253        }
254
255        if updates.is_empty() {
256            return Ok(task);
257        }
258
259        query.push_str(&updates.join(", "));
260        query.push_str(&format!(" WHERE id = {}", id));
261
262        sqlx::query(&query).execute(self.pool).await?;
263
264        self.get_task(id).await
265    }
266
267    /// Delete a task
268    pub async fn delete_task(&self, id: i64) -> Result<()> {
269        self.check_task_exists(id).await?;
270
271        sqlx::query("DELETE FROM tasks WHERE id = ?")
272            .bind(id)
273            .execute(self.pool)
274            .await?;
275
276        Ok(())
277    }
278
279    /// Find tasks with optional filters
280    pub async fn find_tasks(
281        &self,
282        status: Option<&str>,
283        parent_id: Option<Option<i64>>,
284    ) -> Result<Vec<Task>> {
285        let mut query = String::from(
286            "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at FROM tasks WHERE 1=1"
287        );
288        let mut conditions = Vec::new();
289
290        if let Some(s) = status {
291            query.push_str(" AND status = ?");
292            conditions.push(s.to_string());
293        }
294
295        if let Some(pid) = parent_id {
296            if let Some(p) = pid {
297                query.push_str(" AND parent_id = ?");
298                conditions.push(p.to_string());
299            } else {
300                query.push_str(" AND parent_id IS NULL");
301            }
302        }
303
304        query.push_str(" ORDER BY id");
305
306        let mut q = sqlx::query_as::<_, Task>(&query);
307        for cond in conditions {
308            q = q.bind(cond);
309        }
310
311        let tasks = q.fetch_all(self.pool).await?;
312        Ok(tasks)
313    }
314
315    /// Search tasks using full-text search (FTS5)
316    /// Returns tasks with match snippets showing highlighted keywords
317    pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
318        // Escape special FTS5 characters in the query
319        let escaped_query = self.escape_fts_query(query);
320
321        // Use FTS5 to search and get snippets
322        // snippet(table, column, start_mark, end_mark, ellipsis, max_tokens)
323        // We search in both name (column 0) and spec (column 1)
324        let results = sqlx::query(
325            r#"
326            SELECT
327                t.id,
328                t.parent_id,
329                t.name,
330                t.spec,
331                t.status,
332                t.complexity,
333                t.priority,
334                t.first_todo_at,
335                t.first_doing_at,
336                t.first_done_at,
337                COALESCE(
338                    snippet(tasks_fts, 1, '**', '**', '...', 15),
339                    snippet(tasks_fts, 0, '**', '**', '...', 15)
340                ) as match_snippet
341            FROM tasks_fts
342            INNER JOIN tasks t ON tasks_fts.rowid = t.id
343            WHERE tasks_fts MATCH ?
344            ORDER BY rank
345            "#,
346        )
347        .bind(&escaped_query)
348        .fetch_all(self.pool)
349        .await?;
350
351        let mut search_results = Vec::new();
352        for row in results {
353            let task = Task {
354                id: row.get("id"),
355                parent_id: row.get("parent_id"),
356                name: row.get("name"),
357                spec: row.get("spec"),
358                status: row.get("status"),
359                complexity: row.get("complexity"),
360                priority: row.get("priority"),
361                first_todo_at: row.get("first_todo_at"),
362                first_doing_at: row.get("first_doing_at"),
363                first_done_at: row.get("first_done_at"),
364            };
365            let match_snippet: String = row.get("match_snippet");
366
367            search_results.push(TaskSearchResult {
368                task,
369                match_snippet,
370            });
371        }
372
373        Ok(search_results)
374    }
375
376    /// Escape FTS5 special characters in query
377    fn escape_fts_query(&self, query: &str) -> String {
378        // FTS5 queries are passed through as-is to support advanced syntax
379        // Users can use operators like AND, OR, NOT, *, "phrase search", etc.
380        // We only need to handle basic escaping for quotes
381        query.replace('"', "\"\"")
382    }
383
384    /// Start a task (atomic: update status + set current)
385    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
386        let mut tx = self.pool.begin().await?;
387
388        let now = Utc::now();
389
390        // Update task status to doing
391        sqlx::query(
392            r#"
393            UPDATE tasks
394            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
395            WHERE id = ?
396            "#,
397        )
398        .bind(now)
399        .bind(id)
400        .execute(&mut *tx)
401        .await?;
402
403        // Set as current task
404        sqlx::query(
405            r#"
406            INSERT OR REPLACE INTO workspace_state (key, value)
407            VALUES ('current_task_id', ?)
408            "#,
409        )
410        .bind(id.to_string())
411        .execute(&mut *tx)
412        .await?;
413
414        tx.commit().await?;
415
416        if with_events {
417            self.get_task_with_events(id).await
418        } else {
419            let task = self.get_task(id).await?;
420            Ok(TaskWithEvents {
421                task,
422                events_summary: None,
423            })
424        }
425    }
426
427    /// Complete the current focused task (atomic: check children + update status + clear current)
428    /// This command only operates on the current_task_id.
429    /// Prerequisites: A task must be set as current
430    pub async fn done_task(&self) -> Result<DoneTaskResponse> {
431        let mut tx = self.pool.begin().await?;
432
433        // Get the current task ID
434        let current_task_id: Option<String> =
435            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
436                .fetch_optional(&mut *tx)
437                .await?;
438
439        let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
440            IntentError::InvalidInput(
441                "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
442            ),
443        )?;
444
445        // Get the task info before completing it
446        let task_info: (String, Option<i64>) =
447            sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
448                .bind(id)
449                .fetch_one(&mut *tx)
450                .await?;
451        let (task_name, parent_id) = task_info;
452
453        // Check if all children are done
454        let uncompleted_children: i64 = sqlx::query_scalar(
455            "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
456        )
457        .bind(id)
458        .fetch_one(&mut *tx)
459        .await?;
460
461        if uncompleted_children > 0 {
462            return Err(IntentError::UncompletedChildren);
463        }
464
465        let now = Utc::now();
466
467        // Update task status to done
468        sqlx::query(
469            r#"
470            UPDATE tasks
471            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
472            WHERE id = ?
473            "#,
474        )
475        .bind(now)
476        .bind(id)
477        .execute(&mut *tx)
478        .await?;
479
480        // Clear the current task
481        sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
482            .execute(&mut *tx)
483            .await?;
484
485        // Determine next step suggestion based on context
486        let next_step_suggestion = if let Some(parent_task_id) = parent_id {
487            // Task has a parent - check sibling status
488            let remaining_siblings: i64 = sqlx::query_scalar(
489                "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
490            )
491            .bind(parent_task_id)
492            .bind(id)
493            .fetch_one(&mut *tx)
494            .await?;
495
496            if remaining_siblings == 0 {
497                // All siblings are done - parent is ready
498                let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
499                    .bind(parent_task_id)
500                    .fetch_one(&mut *tx)
501                    .await?;
502
503                NextStepSuggestion::ParentIsReady {
504                    message: format!(
505                        "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
506                        parent_task_id, parent_name
507                    ),
508                    parent_task_id,
509                    parent_task_name: parent_name,
510                }
511            } else {
512                // Siblings remain
513                let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
514                    .bind(parent_task_id)
515                    .fetch_one(&mut *tx)
516                    .await?;
517
518                NextStepSuggestion::SiblingTasksRemain {
519                    message: format!(
520                        "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
521                        id, parent_task_id, parent_name
522                    ),
523                    parent_task_id,
524                    parent_task_name: parent_name,
525                    remaining_siblings_count: remaining_siblings,
526                }
527            }
528        } else {
529            // No parent - check if this was a top-level task with children or standalone
530            let child_count: i64 =
531                sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
532                    .bind(id)
533                    .fetch_one(&mut *tx)
534                    .await?;
535
536            if child_count > 0 {
537                // Top-level task with children completed
538                NextStepSuggestion::TopLevelTaskCompleted {
539                    message: format!(
540                        "Top-level task #{} '{}' has been completed. Well done!",
541                        id, task_name
542                    ),
543                    completed_task_id: id,
544                    completed_task_name: task_name.clone(),
545                }
546            } else {
547                // Check if workspace is clear
548                let remaining_tasks: i64 = sqlx::query_scalar(
549                    "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
550                )
551                .bind(id)
552                .fetch_one(&mut *tx)
553                .await?;
554
555                if remaining_tasks == 0 {
556                    NextStepSuggestion::WorkspaceIsClear {
557                        message: format!(
558                            "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
559                            id
560                        ),
561                        completed_task_id: id,
562                    }
563                } else {
564                    NextStepSuggestion::NoParentContext {
565                        message: format!("Task #{} '{}' has been completed.", id, task_name),
566                        completed_task_id: id,
567                        completed_task_name: task_name.clone(),
568                    }
569                }
570            }
571        };
572
573        tx.commit().await?;
574
575        let completed_task = self.get_task(id).await?;
576
577        Ok(DoneTaskResponse {
578            completed_task,
579            workspace_status: WorkspaceStatus {
580                current_task_id: None,
581            },
582            next_step_suggestion,
583        })
584    }
585
586    /// Check if a task exists
587    async fn check_task_exists(&self, id: i64) -> Result<()> {
588        let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
589            .bind(id)
590            .fetch_one(self.pool)
591            .await?;
592
593        if !exists {
594            return Err(IntentError::TaskNotFound(id));
595        }
596
597        Ok(())
598    }
599
600    /// Check for circular dependencies
601    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
602        let mut current_id = new_parent_id;
603
604        loop {
605            if current_id == task_id {
606                return Err(IntentError::CircularDependency);
607            }
608
609            let parent: Option<i64> =
610                sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
611                    .bind(current_id)
612                    .fetch_optional(self.pool)
613                    .await?;
614
615            match parent {
616                Some(pid) => current_id = pid,
617                None => break,
618            }
619        }
620
621        Ok(())
622    }
623
624    /// Switch to a specific task (atomic: update status to doing + set as current)
625    /// If the task is not in 'doing' status, it will be transitioned to 'doing'
626    /// Returns response with previous task info (if any) and current task info
627    pub async fn switch_to_task(&self, id: i64) -> Result<SwitchTaskResponse> {
628        // Verify task exists
629        self.check_task_exists(id).await?;
630
631        let mut tx = self.pool.begin().await?;
632        let now = Utc::now();
633
634        // Get current task info before switching (if any)
635        let current_task_id: Option<String> =
636            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
637                .fetch_optional(&mut *tx)
638                .await?;
639
640        let previous_task = if let Some(prev_id_str) = current_task_id {
641            if let Ok(prev_id) = prev_id_str.parse::<i64>() {
642                // Set previous task back to 'todo' if it was 'doing'
643                sqlx::query(
644                    r#"
645                    UPDATE tasks
646                    SET status = 'todo'
647                    WHERE id = ? AND status = 'doing'
648                    "#,
649                )
650                .bind(prev_id)
651                .execute(&mut *tx)
652                .await?;
653
654                Some(PreviousTaskInfo {
655                    id: prev_id,
656                    status: "todo".to_string(),
657                })
658            } else {
659                None
660            }
661        } else {
662            None
663        };
664
665        // Update new task to doing status if not already
666        sqlx::query(
667            r#"
668            UPDATE tasks
669            SET status = 'doing',
670                first_doing_at = COALESCE(first_doing_at, ?)
671            WHERE id = ? AND status != 'doing'
672            "#,
673        )
674        .bind(now)
675        .bind(id)
676        .execute(&mut *tx)
677        .await?;
678
679        // Get new task name for response
680        let (task_name, task_status): (String, String) =
681            sqlx::query_as("SELECT name, status FROM tasks WHERE id = ?")
682                .bind(id)
683                .fetch_one(&mut *tx)
684                .await?;
685
686        // Set as current task
687        sqlx::query(
688            r#"
689            INSERT OR REPLACE INTO workspace_state (key, value)
690            VALUES ('current_task_id', ?)
691            "#,
692        )
693        .bind(id.to_string())
694        .execute(&mut *tx)
695        .await?;
696
697        tx.commit().await?;
698
699        Ok(SwitchTaskResponse {
700            previous_task,
701            current_task: CurrentTaskInfo {
702                id,
703                name: task_name,
704                status: task_status,
705            },
706        })
707    }
708
709    /// Create a subtask under the current task and switch to it (atomic operation)
710    /// Returns error if there is no current task
711    /// Returns response with subtask info and parent task info
712    pub async fn spawn_subtask(
713        &self,
714        name: &str,
715        spec: Option<&str>,
716    ) -> Result<SpawnSubtaskResponse> {
717        // Get current task
718        let current_task_id: Option<String> =
719            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
720                .fetch_optional(self.pool)
721                .await?;
722
723        let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
724            IntentError::InvalidInput("No current task to create subtask under".to_string()),
725        )?;
726
727        // Get parent task info
728        let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
729            .bind(parent_id)
730            .fetch_one(self.pool)
731            .await?;
732
733        // Create the subtask
734        let subtask = self.add_task(name, spec, Some(parent_id)).await?;
735
736        // Switch to the new subtask (sets status to doing and updates current_task_id)
737        self.switch_to_task(subtask.id).await?;
738
739        Ok(SpawnSubtaskResponse {
740            subtask: SubtaskInfo {
741                id: subtask.id,
742                name: subtask.name,
743                parent_id,
744                status: "doing".to_string(),
745            },
746            parent_task: ParentTaskInfo {
747                id: parent_id,
748                name: parent_name,
749            },
750        })
751    }
752
753    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
754    /// Returns tasks that were successfully transitioned
755    ///
756    /// # Arguments
757    /// * `max_count` - Maximum number of tasks to pick
758    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
759    ///
760    /// # Logic
761    /// 1. Check current 'doing' task count
762    /// 2. Calculate available capacity
763    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
764    /// 4. Transition selected tasks to 'doing'
765    pub async fn pick_next_tasks(
766        &self,
767        max_count: usize,
768        capacity_limit: usize,
769    ) -> Result<Vec<Task>> {
770        let mut tx = self.pool.begin().await?;
771
772        // Get current doing count
773        let doing_count: i64 =
774            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
775                .fetch_one(&mut *tx)
776                .await?;
777
778        // Calculate available capacity
779        let available = capacity_limit.saturating_sub(doing_count as usize);
780        if available == 0 {
781            return Ok(vec![]);
782        }
783
784        let limit = std::cmp::min(max_count, available);
785
786        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
787        let todo_tasks = sqlx::query_as::<_, Task>(
788            r#"
789            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
790            FROM tasks
791            WHERE status = 'todo'
792            ORDER BY
793                COALESCE(priority, 0) DESC,
794                COALESCE(complexity, 5) ASC,
795                id ASC
796            LIMIT ?
797            "#,
798        )
799        .bind(limit as i64)
800        .fetch_all(&mut *tx)
801        .await?;
802
803        if todo_tasks.is_empty() {
804            return Ok(vec![]);
805        }
806
807        let now = Utc::now();
808
809        // Transition selected tasks to 'doing'
810        for task in &todo_tasks {
811            sqlx::query(
812                r#"
813                UPDATE tasks
814                SET status = 'doing',
815                    first_doing_at = COALESCE(first_doing_at, ?)
816                WHERE id = ?
817                "#,
818            )
819            .bind(now)
820            .bind(task.id)
821            .execute(&mut *tx)
822            .await?;
823        }
824
825        tx.commit().await?;
826
827        // Fetch and return updated tasks in the same order
828        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
829        let placeholders = vec!["?"; task_ids.len()].join(",");
830        let query = format!(
831            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
832             FROM tasks WHERE id IN ({})
833             ORDER BY
834                 COALESCE(priority, 0) DESC,
835                 COALESCE(complexity, 5) ASC,
836                 id ASC",
837            placeholders
838        );
839
840        let mut q = sqlx::query_as::<_, Task>(&query);
841        for id in task_ids {
842            q = q.bind(id);
843        }
844
845        let updated_tasks = q.fetch_all(self.pool).await?;
846        Ok(updated_tasks)
847    }
848
849    /// Intelligently recommend the next task to work on based on context-aware priority model.
850    ///
851    /// Priority logic:
852    /// 1. First priority: Subtasks of the current focused task (depth-first)
853    /// 2. Second priority: Top-level tasks (breadth-first)
854    /// 3. No recommendation: Return appropriate empty state
855    ///
856    /// This command does NOT modify task status.
857    pub async fn pick_next(&self) -> Result<PickNextResponse> {
858        // Step 1: Check if there's a current focused task
859        let current_task_id: Option<String> =
860            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
861                .fetch_optional(self.pool)
862                .await?;
863
864        if let Some(current_id_str) = current_task_id {
865            if let Ok(current_id) = current_id_str.parse::<i64>() {
866                // First priority: Get todo subtasks of current focused task
867                let subtasks = sqlx::query_as::<_, Task>(
868                    r#"
869                    SELECT id, parent_id, name, spec, status, complexity, priority,
870                           first_todo_at, first_doing_at, first_done_at
871                    FROM tasks
872                    WHERE parent_id = ? AND status = 'todo'
873                    ORDER BY COALESCE(priority, 999999) ASC, id ASC
874                    LIMIT 1
875                    "#,
876                )
877                .bind(current_id)
878                .fetch_optional(self.pool)
879                .await?;
880
881                if let Some(task) = subtasks {
882                    return Ok(PickNextResponse::focused_subtask(task));
883                }
884            }
885        }
886
887        // Step 2: Second priority - get top-level todo tasks
888        let top_level_task = sqlx::query_as::<_, Task>(
889            r#"
890            SELECT id, parent_id, name, spec, status, complexity, priority,
891                   first_todo_at, first_doing_at, first_done_at
892            FROM tasks
893            WHERE parent_id IS NULL AND status = 'todo'
894            ORDER BY COALESCE(priority, 999999) ASC, id ASC
895            LIMIT 1
896            "#,
897        )
898        .fetch_optional(self.pool)
899        .await?;
900
901        if let Some(task) = top_level_task {
902            return Ok(PickNextResponse::top_level_task(task));
903        }
904
905        // Step 3: No recommendation - determine why
906        // Check if there are any tasks at all
907        let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
908            .fetch_one(self.pool)
909            .await?;
910
911        if total_tasks == 0 {
912            return Ok(PickNextResponse::no_tasks_in_project());
913        }
914
915        // Check if all tasks are completed
916        let todo_or_doing_count: i64 =
917            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
918                .fetch_one(self.pool)
919                .await?;
920
921        if todo_or_doing_count == 0 {
922            return Ok(PickNextResponse::all_tasks_completed());
923        }
924
925        // Otherwise, there are tasks but none available based on current context
926        Ok(PickNextResponse::no_available_todos())
927    }
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933    use crate::events::EventManager;
934    use crate::test_utils::test_helpers::TestContext;
935
936    #[tokio::test]
937    async fn test_add_task() {
938        let ctx = TestContext::new().await;
939        let manager = TaskManager::new(ctx.pool());
940
941        let task = manager.add_task("Test task", None, None).await.unwrap();
942
943        assert_eq!(task.name, "Test task");
944        assert_eq!(task.status, "todo");
945        assert!(task.first_todo_at.is_some());
946        assert!(task.first_doing_at.is_none());
947        assert!(task.first_done_at.is_none());
948    }
949
950    #[tokio::test]
951    async fn test_add_task_with_spec() {
952        let ctx = TestContext::new().await;
953        let manager = TaskManager::new(ctx.pool());
954
955        let spec = "This is a task specification";
956        let task = manager
957            .add_task("Test task", Some(spec), None)
958            .await
959            .unwrap();
960
961        assert_eq!(task.name, "Test task");
962        assert_eq!(task.spec.as_deref(), Some(spec));
963    }
964
965    #[tokio::test]
966    async fn test_add_task_with_parent() {
967        let ctx = TestContext::new().await;
968        let manager = TaskManager::new(ctx.pool());
969
970        let parent = manager.add_task("Parent task", None, None).await.unwrap();
971        let child = manager
972            .add_task("Child task", None, Some(parent.id))
973            .await
974            .unwrap();
975
976        assert_eq!(child.parent_id, Some(parent.id));
977    }
978
979    #[tokio::test]
980    async fn test_get_task() {
981        let ctx = TestContext::new().await;
982        let manager = TaskManager::new(ctx.pool());
983
984        let created = manager.add_task("Test task", None, None).await.unwrap();
985        let retrieved = manager.get_task(created.id).await.unwrap();
986
987        assert_eq!(created.id, retrieved.id);
988        assert_eq!(created.name, retrieved.name);
989    }
990
991    #[tokio::test]
992    async fn test_get_task_not_found() {
993        let ctx = TestContext::new().await;
994        let manager = TaskManager::new(ctx.pool());
995
996        let result = manager.get_task(999).await;
997        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
998    }
999
1000    #[tokio::test]
1001    async fn test_update_task_name() {
1002        let ctx = TestContext::new().await;
1003        let manager = TaskManager::new(ctx.pool());
1004
1005        let task = manager.add_task("Original name", None, None).await.unwrap();
1006        let updated = manager
1007            .update_task(task.id, Some("New name"), None, None, None, None, None)
1008            .await
1009            .unwrap();
1010
1011        assert_eq!(updated.name, "New name");
1012    }
1013
1014    #[tokio::test]
1015    async fn test_update_task_status() {
1016        let ctx = TestContext::new().await;
1017        let manager = TaskManager::new(ctx.pool());
1018
1019        let task = manager.add_task("Test task", None, None).await.unwrap();
1020        let updated = manager
1021            .update_task(task.id, None, None, None, Some("doing"), None, None)
1022            .await
1023            .unwrap();
1024
1025        assert_eq!(updated.status, "doing");
1026        assert!(updated.first_doing_at.is_some());
1027    }
1028
1029    #[tokio::test]
1030    async fn test_delete_task() {
1031        let ctx = TestContext::new().await;
1032        let manager = TaskManager::new(ctx.pool());
1033
1034        let task = manager.add_task("Test task", None, None).await.unwrap();
1035        manager.delete_task(task.id).await.unwrap();
1036
1037        let result = manager.get_task(task.id).await;
1038        assert!(result.is_err());
1039    }
1040
1041    #[tokio::test]
1042    async fn test_find_tasks_by_status() {
1043        let ctx = TestContext::new().await;
1044        let manager = TaskManager::new(ctx.pool());
1045
1046        manager.add_task("Todo task", None, None).await.unwrap();
1047        let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1048        manager
1049            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1050            .await
1051            .unwrap();
1052
1053        let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1054        let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1055
1056        assert_eq!(todo_tasks.len(), 1);
1057        assert_eq!(doing_tasks.len(), 1);
1058        assert_eq!(doing_tasks[0].status, "doing");
1059    }
1060
1061    #[tokio::test]
1062    async fn test_find_tasks_by_parent() {
1063        let ctx = TestContext::new().await;
1064        let manager = TaskManager::new(ctx.pool());
1065
1066        let parent = manager.add_task("Parent", None, None).await.unwrap();
1067        manager
1068            .add_task("Child 1", None, Some(parent.id))
1069            .await
1070            .unwrap();
1071        manager
1072            .add_task("Child 2", None, Some(parent.id))
1073            .await
1074            .unwrap();
1075
1076        let children = manager
1077            .find_tasks(None, Some(Some(parent.id)))
1078            .await
1079            .unwrap();
1080
1081        assert_eq!(children.len(), 2);
1082    }
1083
1084    #[tokio::test]
1085    async fn test_start_task() {
1086        let ctx = TestContext::new().await;
1087        let manager = TaskManager::new(ctx.pool());
1088
1089        let task = manager.add_task("Test task", None, None).await.unwrap();
1090        let started = manager.start_task(task.id, false).await.unwrap();
1091
1092        assert_eq!(started.task.status, "doing");
1093        assert!(started.task.first_doing_at.is_some());
1094
1095        // Verify it's set as current task
1096        let current: Option<String> =
1097            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1098                .fetch_optional(ctx.pool())
1099                .await
1100                .unwrap();
1101
1102        assert_eq!(current, Some(task.id.to_string()));
1103    }
1104
1105    #[tokio::test]
1106    async fn test_start_task_with_events() {
1107        let ctx = TestContext::new().await;
1108        let manager = TaskManager::new(ctx.pool());
1109
1110        let task = manager.add_task("Test task", None, None).await.unwrap();
1111
1112        // Add an event
1113        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1114            .bind(task.id)
1115            .bind("test")
1116            .bind("test event")
1117            .execute(ctx.pool())
1118            .await
1119            .unwrap();
1120
1121        let started = manager.start_task(task.id, true).await.unwrap();
1122
1123        assert!(started.events_summary.is_some());
1124        let summary = started.events_summary.unwrap();
1125        assert_eq!(summary.total_count, 1);
1126    }
1127
1128    #[tokio::test]
1129    async fn test_done_task() {
1130        let ctx = TestContext::new().await;
1131        let manager = TaskManager::new(ctx.pool());
1132
1133        let task = manager.add_task("Test task", None, None).await.unwrap();
1134        manager.start_task(task.id, false).await.unwrap();
1135        let response = manager.done_task().await.unwrap();
1136
1137        assert_eq!(response.completed_task.status, "done");
1138        assert!(response.completed_task.first_done_at.is_some());
1139        assert_eq!(response.workspace_status.current_task_id, None);
1140
1141        // Should be WORKSPACE_IS_CLEAR since it's the only task
1142        match response.next_step_suggestion {
1143            NextStepSuggestion::WorkspaceIsClear { .. } => {},
1144            _ => panic!("Expected WorkspaceIsClear suggestion"),
1145        }
1146
1147        // Verify current task is cleared
1148        let current: Option<String> =
1149            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1150                .fetch_optional(ctx.pool())
1151                .await
1152                .unwrap();
1153
1154        assert!(current.is_none());
1155    }
1156
1157    #[tokio::test]
1158    async fn test_done_task_with_uncompleted_children() {
1159        let ctx = TestContext::new().await;
1160        let manager = TaskManager::new(ctx.pool());
1161
1162        let parent = manager.add_task("Parent", None, None).await.unwrap();
1163        manager
1164            .add_task("Child", None, Some(parent.id))
1165            .await
1166            .unwrap();
1167
1168        // Set parent as current task
1169        manager.start_task(parent.id, false).await.unwrap();
1170
1171        let result = manager.done_task().await;
1172        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1173    }
1174
1175    #[tokio::test]
1176    async fn test_done_task_with_completed_children() {
1177        let ctx = TestContext::new().await;
1178        let manager = TaskManager::new(ctx.pool());
1179
1180        let parent = manager.add_task("Parent", None, None).await.unwrap();
1181        let child = manager
1182            .add_task("Child", None, Some(parent.id))
1183            .await
1184            .unwrap();
1185
1186        // Complete child first
1187        manager.start_task(child.id, false).await.unwrap();
1188        let child_response = manager.done_task().await.unwrap();
1189
1190        // Child completion should suggest parent is ready
1191        match child_response.next_step_suggestion {
1192            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1193                assert_eq!(parent_task_id, parent.id);
1194            },
1195            _ => panic!("Expected ParentIsReady suggestion"),
1196        }
1197
1198        // Now parent can be completed
1199        manager.start_task(parent.id, false).await.unwrap();
1200        let parent_response = manager.done_task().await.unwrap();
1201        assert_eq!(parent_response.completed_task.status, "done");
1202
1203        // Parent completion should indicate top-level task completed (since it had children)
1204        match parent_response.next_step_suggestion {
1205            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1206            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1207        }
1208    }
1209
1210    #[tokio::test]
1211    async fn test_circular_dependency() {
1212        let ctx = TestContext::new().await;
1213        let manager = TaskManager::new(ctx.pool());
1214
1215        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1216        let task2 = manager
1217            .add_task("Task 2", None, Some(task1.id))
1218            .await
1219            .unwrap();
1220
1221        // Try to make task1 a child of task2 (circular)
1222        let result = manager
1223            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1224            .await;
1225
1226        assert!(matches!(result, Err(IntentError::CircularDependency)));
1227    }
1228
1229    #[tokio::test]
1230    async fn test_invalid_parent_id() {
1231        let ctx = TestContext::new().await;
1232        let manager = TaskManager::new(ctx.pool());
1233
1234        let result = manager.add_task("Test", None, Some(999)).await;
1235        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1236    }
1237
1238    #[tokio::test]
1239    async fn test_update_task_complexity_and_priority() {
1240        let ctx = TestContext::new().await;
1241        let manager = TaskManager::new(ctx.pool());
1242
1243        let task = manager.add_task("Test task", None, None).await.unwrap();
1244        let updated = manager
1245            .update_task(task.id, None, None, None, None, Some(8), Some(10))
1246            .await
1247            .unwrap();
1248
1249        assert_eq!(updated.complexity, Some(8));
1250        assert_eq!(updated.priority, Some(10));
1251    }
1252
1253    #[tokio::test]
1254    async fn test_switch_to_task() {
1255        let ctx = TestContext::new().await;
1256        let manager = TaskManager::new(ctx.pool());
1257
1258        // Create a task
1259        let task = manager.add_task("Test task", None, None).await.unwrap();
1260        assert_eq!(task.status, "todo");
1261
1262        // Switch to it
1263        let response = manager.switch_to_task(task.id).await.unwrap();
1264        assert_eq!(response.current_task.id, task.id);
1265        assert_eq!(response.current_task.status, "doing");
1266        assert!(response.previous_task.is_none());
1267
1268        // Verify it's set as current task
1269        let current: Option<String> =
1270            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1271                .fetch_optional(ctx.pool())
1272                .await
1273                .unwrap();
1274
1275        assert_eq!(current, Some(task.id.to_string()));
1276    }
1277
1278    #[tokio::test]
1279    async fn test_switch_to_task_already_doing() {
1280        let ctx = TestContext::new().await;
1281        let manager = TaskManager::new(ctx.pool());
1282
1283        // Create and start a task
1284        let task = manager.add_task("Test task", None, None).await.unwrap();
1285        manager.start_task(task.id, false).await.unwrap();
1286
1287        // Switch to it again (should be idempotent)
1288        let response = manager.switch_to_task(task.id).await.unwrap();
1289        assert_eq!(response.current_task.id, task.id);
1290        assert_eq!(response.current_task.status, "doing");
1291    }
1292
1293    #[tokio::test]
1294    async fn test_spawn_subtask() {
1295        let ctx = TestContext::new().await;
1296        let manager = TaskManager::new(ctx.pool());
1297
1298        // Create and start a parent task
1299        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1300        manager.start_task(parent.id, false).await.unwrap();
1301
1302        // Spawn a subtask
1303        let response = manager
1304            .spawn_subtask("Child task", Some("Details"))
1305            .await
1306            .unwrap();
1307
1308        assert_eq!(response.subtask.parent_id, parent.id);
1309        assert_eq!(response.subtask.name, "Child task");
1310        assert_eq!(response.subtask.status, "doing");
1311        assert_eq!(response.parent_task.id, parent.id);
1312        assert_eq!(response.parent_task.name, "Parent task");
1313
1314        // Verify subtask is now the current task
1315        let current: Option<String> =
1316            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1317                .fetch_optional(ctx.pool())
1318                .await
1319                .unwrap();
1320
1321        assert_eq!(current, Some(response.subtask.id.to_string()));
1322
1323        // Verify subtask is in doing status
1324        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1325        assert_eq!(retrieved.status, "doing");
1326    }
1327
1328    #[tokio::test]
1329    async fn test_spawn_subtask_no_current_task() {
1330        let ctx = TestContext::new().await;
1331        let manager = TaskManager::new(ctx.pool());
1332
1333        // Try to spawn subtask without a current task
1334        let result = manager.spawn_subtask("Child", None).await;
1335        assert!(result.is_err());
1336    }
1337
1338    #[tokio::test]
1339    async fn test_pick_next_tasks_basic() {
1340        let ctx = TestContext::new().await;
1341        let manager = TaskManager::new(ctx.pool());
1342
1343        // Create 10 todo tasks
1344        for i in 1..=10 {
1345            manager
1346                .add_task(&format!("Task {}", i), None, None)
1347                .await
1348                .unwrap();
1349        }
1350
1351        // Pick 5 tasks with capacity limit of 5
1352        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1353
1354        assert_eq!(picked.len(), 5);
1355        for task in &picked {
1356            assert_eq!(task.status, "doing");
1357            assert!(task.first_doing_at.is_some());
1358        }
1359
1360        // Verify total doing count
1361        let doing_count: i64 =
1362            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1363                .fetch_one(ctx.pool())
1364                .await
1365                .unwrap();
1366
1367        assert_eq!(doing_count, 5);
1368    }
1369
1370    #[tokio::test]
1371    async fn test_pick_next_tasks_with_existing_doing() {
1372        let ctx = TestContext::new().await;
1373        let manager = TaskManager::new(ctx.pool());
1374
1375        // Create 10 todo tasks
1376        for i in 1..=10 {
1377            manager
1378                .add_task(&format!("Task {}", i), None, None)
1379                .await
1380                .unwrap();
1381        }
1382
1383        // Start 2 tasks
1384        let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1385        manager.start_task(tasks[0].id, false).await.unwrap();
1386        manager.start_task(tasks[1].id, false).await.unwrap();
1387
1388        // Pick more tasks with capacity limit of 5
1389        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1390
1391        // Should only pick 3 more (5 - 2 = 3)
1392        assert_eq!(picked.len(), 3);
1393
1394        // Verify total doing count
1395        let doing_count: i64 =
1396            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1397                .fetch_one(ctx.pool())
1398                .await
1399                .unwrap();
1400
1401        assert_eq!(doing_count, 5);
1402    }
1403
1404    #[tokio::test]
1405    async fn test_pick_next_tasks_at_capacity() {
1406        let ctx = TestContext::new().await;
1407        let manager = TaskManager::new(ctx.pool());
1408
1409        // Create 10 tasks
1410        for i in 1..=10 {
1411            manager
1412                .add_task(&format!("Task {}", i), None, None)
1413                .await
1414                .unwrap();
1415        }
1416
1417        // Fill capacity
1418        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1419        assert_eq!(first_batch.len(), 5);
1420
1421        // Try to pick more (should return empty)
1422        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1423        assert_eq!(second_batch.len(), 0);
1424    }
1425
1426    #[tokio::test]
1427    async fn test_pick_next_tasks_priority_ordering() {
1428        let ctx = TestContext::new().await;
1429        let manager = TaskManager::new(ctx.pool());
1430
1431        // Create tasks with different priorities
1432        let low = manager.add_task("Low priority", None, None).await.unwrap();
1433        manager
1434            .update_task(low.id, None, None, None, None, None, Some(1))
1435            .await
1436            .unwrap();
1437
1438        let high = manager.add_task("High priority", None, None).await.unwrap();
1439        manager
1440            .update_task(high.id, None, None, None, None, None, Some(10))
1441            .await
1442            .unwrap();
1443
1444        let medium = manager
1445            .add_task("Medium priority", None, None)
1446            .await
1447            .unwrap();
1448        manager
1449            .update_task(medium.id, None, None, None, None, None, Some(5))
1450            .await
1451            .unwrap();
1452
1453        // Pick tasks
1454        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1455
1456        // Should be ordered by priority DESC
1457        assert_eq!(picked.len(), 3);
1458        assert_eq!(picked[0].priority, Some(10)); // high
1459        assert_eq!(picked[1].priority, Some(5)); // medium
1460        assert_eq!(picked[2].priority, Some(1)); // low
1461    }
1462
1463    #[tokio::test]
1464    async fn test_pick_next_tasks_complexity_ordering() {
1465        let ctx = TestContext::new().await;
1466        let manager = TaskManager::new(ctx.pool());
1467
1468        // Create tasks with different complexities (same priority)
1469        let complex = manager.add_task("Complex", None, None).await.unwrap();
1470        manager
1471            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1472            .await
1473            .unwrap();
1474
1475        let simple = manager.add_task("Simple", None, None).await.unwrap();
1476        manager
1477            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1478            .await
1479            .unwrap();
1480
1481        let medium = manager.add_task("Medium", None, None).await.unwrap();
1482        manager
1483            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1484            .await
1485            .unwrap();
1486
1487        // Pick tasks
1488        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1489
1490        // Should be ordered by complexity ASC (simple first)
1491        assert_eq!(picked.len(), 3);
1492        assert_eq!(picked[0].complexity, Some(1)); // simple
1493        assert_eq!(picked[1].complexity, Some(5)); // medium
1494        assert_eq!(picked[2].complexity, Some(9)); // complex
1495    }
1496
1497    #[tokio::test]
1498    async fn test_done_task_sibling_tasks_remain() {
1499        let ctx = TestContext::new().await;
1500        let manager = TaskManager::new(ctx.pool());
1501
1502        // Create parent with multiple children
1503        let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1504        let child1 = manager
1505            .add_task("Child 1", None, Some(parent.id))
1506            .await
1507            .unwrap();
1508        let child2 = manager
1509            .add_task("Child 2", None, Some(parent.id))
1510            .await
1511            .unwrap();
1512        let _child3 = manager
1513            .add_task("Child 3", None, Some(parent.id))
1514            .await
1515            .unwrap();
1516
1517        // Complete first child
1518        manager.start_task(child1.id, false).await.unwrap();
1519        let response = manager.done_task().await.unwrap();
1520
1521        // Should indicate siblings remain
1522        match response.next_step_suggestion {
1523            NextStepSuggestion::SiblingTasksRemain {
1524                parent_task_id,
1525                remaining_siblings_count,
1526                ..
1527            } => {
1528                assert_eq!(parent_task_id, parent.id);
1529                assert_eq!(remaining_siblings_count, 2); // child2 and child3
1530            },
1531            _ => panic!("Expected SiblingTasksRemain suggestion"),
1532        }
1533
1534        // Complete second child
1535        manager.start_task(child2.id, false).await.unwrap();
1536        let response2 = manager.done_task().await.unwrap();
1537
1538        // Should still indicate siblings remain
1539        match response2.next_step_suggestion {
1540            NextStepSuggestion::SiblingTasksRemain {
1541                remaining_siblings_count,
1542                ..
1543            } => {
1544                assert_eq!(remaining_siblings_count, 1); // only child3
1545            },
1546            _ => panic!("Expected SiblingTasksRemain suggestion"),
1547        }
1548    }
1549
1550    #[tokio::test]
1551    async fn test_done_task_top_level_with_children() {
1552        let ctx = TestContext::new().await;
1553        let manager = TaskManager::new(ctx.pool());
1554
1555        // Create top-level task with children
1556        let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1557        let child = manager
1558            .add_task("Sub Task", None, Some(parent.id))
1559            .await
1560            .unwrap();
1561
1562        // Complete child first
1563        manager.start_task(child.id, false).await.unwrap();
1564        manager.done_task().await.unwrap();
1565
1566        // Complete parent
1567        manager.start_task(parent.id, false).await.unwrap();
1568        let response = manager.done_task().await.unwrap();
1569
1570        // Should be TOP_LEVEL_TASK_COMPLETED
1571        match response.next_step_suggestion {
1572            NextStepSuggestion::TopLevelTaskCompleted {
1573                completed_task_id,
1574                completed_task_name,
1575                ..
1576            } => {
1577                assert_eq!(completed_task_id, parent.id);
1578                assert_eq!(completed_task_name, "Epic Task");
1579            },
1580            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1581        }
1582    }
1583
1584    #[tokio::test]
1585    async fn test_done_task_no_parent_context() {
1586        let ctx = TestContext::new().await;
1587        let manager = TaskManager::new(ctx.pool());
1588
1589        // Create multiple standalone tasks
1590        let task1 = manager
1591            .add_task("Standalone Task 1", None, None)
1592            .await
1593            .unwrap();
1594        let _task2 = manager
1595            .add_task("Standalone Task 2", None, None)
1596            .await
1597            .unwrap();
1598
1599        // Complete first task
1600        manager.start_task(task1.id, false).await.unwrap();
1601        let response = manager.done_task().await.unwrap();
1602
1603        // Should be NO_PARENT_CONTEXT since task2 is still pending
1604        match response.next_step_suggestion {
1605            NextStepSuggestion::NoParentContext {
1606                completed_task_id,
1607                completed_task_name,
1608                ..
1609            } => {
1610                assert_eq!(completed_task_id, task1.id);
1611                assert_eq!(completed_task_name, "Standalone Task 1");
1612            },
1613            _ => panic!("Expected NoParentContext suggestion"),
1614        }
1615    }
1616
1617    #[tokio::test]
1618    async fn test_search_tasks_by_name() {
1619        let ctx = TestContext::new().await;
1620        let manager = TaskManager::new(ctx.pool());
1621
1622        // Create tasks with different names
1623        manager
1624            .add_task("Authentication bug fix", Some("Fix login issue"), None)
1625            .await
1626            .unwrap();
1627        manager
1628            .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1629            .await
1630            .unwrap();
1631        manager
1632            .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1633            .await
1634            .unwrap();
1635
1636        // Search for "authentication"
1637        let results = manager.search_tasks("authentication").await.unwrap();
1638
1639        assert_eq!(results.len(), 2);
1640        assert!(results[0]
1641            .task
1642            .name
1643            .to_lowercase()
1644            .contains("authentication"));
1645        assert!(results[1]
1646            .task
1647            .name
1648            .to_lowercase()
1649            .contains("authentication"));
1650
1651        // Check that match_snippet is present
1652        assert!(!results[0].match_snippet.is_empty());
1653    }
1654
1655    #[tokio::test]
1656    async fn test_search_tasks_by_spec() {
1657        let ctx = TestContext::new().await;
1658        let manager = TaskManager::new(ctx.pool());
1659
1660        // Create tasks
1661        manager
1662            .add_task("Task 1", Some("Implement JWT authentication"), None)
1663            .await
1664            .unwrap();
1665        manager
1666            .add_task("Task 2", Some("Add user registration"), None)
1667            .await
1668            .unwrap();
1669        manager
1670            .add_task("Task 3", Some("JWT token refresh"), None)
1671            .await
1672            .unwrap();
1673
1674        // Search for "JWT"
1675        let results = manager.search_tasks("JWT").await.unwrap();
1676
1677        assert_eq!(results.len(), 2);
1678        for result in &results {
1679            assert!(result
1680                .task
1681                .spec
1682                .as_ref()
1683                .unwrap()
1684                .to_uppercase()
1685                .contains("JWT"));
1686        }
1687    }
1688
1689    #[tokio::test]
1690    async fn test_search_tasks_with_advanced_query() {
1691        let ctx = TestContext::new().await;
1692        let manager = TaskManager::new(ctx.pool());
1693
1694        // Create tasks
1695        manager
1696            .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1697            .await
1698            .unwrap();
1699        manager
1700            .add_task("Feature", Some("Add authentication feature"), None)
1701            .await
1702            .unwrap();
1703        manager
1704            .add_task("Bug report", Some("Report critical database bug"), None)
1705            .await
1706            .unwrap();
1707
1708        // Search with AND operator
1709        let results = manager
1710            .search_tasks("authentication AND bug")
1711            .await
1712            .unwrap();
1713
1714        assert_eq!(results.len(), 1);
1715        assert!(results[0]
1716            .task
1717            .spec
1718            .as_ref()
1719            .unwrap()
1720            .contains("authentication"));
1721        assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1722    }
1723
1724    #[tokio::test]
1725    async fn test_search_tasks_no_results() {
1726        let ctx = TestContext::new().await;
1727        let manager = TaskManager::new(ctx.pool());
1728
1729        // Create tasks
1730        manager
1731            .add_task("Task 1", Some("Some description"), None)
1732            .await
1733            .unwrap();
1734
1735        // Search for non-existent term
1736        let results = manager.search_tasks("nonexistent").await.unwrap();
1737
1738        assert_eq!(results.len(), 0);
1739    }
1740
1741    #[tokio::test]
1742    async fn test_search_tasks_snippet_highlighting() {
1743        let ctx = TestContext::new().await;
1744        let manager = TaskManager::new(ctx.pool());
1745
1746        // Create task with keyword in spec
1747        manager
1748            .add_task(
1749                "Test task",
1750                Some("This is a description with the keyword authentication in the middle"),
1751                None,
1752            )
1753            .await
1754            .unwrap();
1755
1756        // Search for "authentication"
1757        let results = manager.search_tasks("authentication").await.unwrap();
1758
1759        assert_eq!(results.len(), 1);
1760        // Check that snippet contains highlighted keyword (marked with **)
1761        assert!(results[0].match_snippet.contains("**authentication**"));
1762    }
1763
1764    #[tokio::test]
1765    async fn test_pick_next_focused_subtask() {
1766        let ctx = TestContext::new().await;
1767        let manager = TaskManager::new(ctx.pool());
1768
1769        // Create parent task and set as current
1770        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1771        manager.start_task(parent.id, false).await.unwrap();
1772
1773        // Create subtasks with different priorities
1774        let subtask1 = manager
1775            .add_task("Subtask 1", None, Some(parent.id))
1776            .await
1777            .unwrap();
1778        let subtask2 = manager
1779            .add_task("Subtask 2", None, Some(parent.id))
1780            .await
1781            .unwrap();
1782
1783        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
1784        manager
1785            .update_task(subtask1.id, None, None, None, None, None, Some(2))
1786            .await
1787            .unwrap();
1788        manager
1789            .update_task(subtask2.id, None, None, None, None, None, Some(1))
1790            .await
1791            .unwrap();
1792
1793        // Pick next should recommend subtask2 (priority 1)
1794        let response = manager.pick_next().await.unwrap();
1795
1796        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1797        assert!(response.task.is_some());
1798        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1799        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1800    }
1801
1802    #[tokio::test]
1803    async fn test_pick_next_top_level_task() {
1804        let ctx = TestContext::new().await;
1805        let manager = TaskManager::new(ctx.pool());
1806
1807        // Create top-level tasks with different priorities
1808        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1809        let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1810
1811        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
1812        manager
1813            .update_task(task1.id, None, None, None, None, None, Some(5))
1814            .await
1815            .unwrap();
1816        manager
1817            .update_task(task2.id, None, None, None, None, None, Some(3))
1818            .await
1819            .unwrap();
1820
1821        // Pick next should recommend task2 (priority 3)
1822        let response = manager.pick_next().await.unwrap();
1823
1824        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1825        assert!(response.task.is_some());
1826        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1827        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1828    }
1829
1830    #[tokio::test]
1831    async fn test_pick_next_no_tasks() {
1832        let ctx = TestContext::new().await;
1833        let manager = TaskManager::new(ctx.pool());
1834
1835        // No tasks created
1836        let response = manager.pick_next().await.unwrap();
1837
1838        assert_eq!(response.suggestion_type, "NONE");
1839        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1840        assert!(response.message.is_some());
1841    }
1842
1843    #[tokio::test]
1844    async fn test_pick_next_all_completed() {
1845        let ctx = TestContext::new().await;
1846        let manager = TaskManager::new(ctx.pool());
1847
1848        // Create task and mark as done
1849        let task = manager.add_task("Task 1", None, None).await.unwrap();
1850        manager.start_task(task.id, false).await.unwrap();
1851        manager.done_task().await.unwrap();
1852
1853        // Pick next should indicate all tasks completed
1854        let response = manager.pick_next().await.unwrap();
1855
1856        assert_eq!(response.suggestion_type, "NONE");
1857        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1858        assert!(response.message.is_some());
1859    }
1860
1861    #[tokio::test]
1862    async fn test_pick_next_no_available_todos() {
1863        let ctx = TestContext::new().await;
1864        let manager = TaskManager::new(ctx.pool());
1865
1866        // Create a parent task that's in "doing" status
1867        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1868        manager.start_task(parent.id, false).await.unwrap();
1869
1870        // Create a subtask also in "doing" status (no "todo" subtasks)
1871        let subtask = manager
1872            .add_task("Subtask", None, Some(parent.id))
1873            .await
1874            .unwrap();
1875        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
1876        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1877            .bind(subtask.id)
1878            .execute(ctx.pool())
1879            .await
1880            .unwrap();
1881
1882        // Set subtask as current
1883        sqlx::query(
1884            "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
1885        )
1886        .bind(subtask.id.to_string())
1887        .execute(ctx.pool())
1888        .await
1889        .unwrap();
1890
1891        // Set parent to doing (not todo)
1892        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1893            .bind(parent.id)
1894            .execute(ctx.pool())
1895            .await
1896            .unwrap();
1897
1898        // Pick next should indicate no available todos
1899        let response = manager.pick_next().await.unwrap();
1900
1901        assert_eq!(response.suggestion_type, "NONE");
1902        assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
1903        assert!(response.message.is_some());
1904    }
1905
1906    #[tokio::test]
1907    async fn test_pick_next_priority_ordering() {
1908        let ctx = TestContext::new().await;
1909        let manager = TaskManager::new(ctx.pool());
1910
1911        // Create parent and set as current
1912        let parent = manager.add_task("Parent", None, None).await.unwrap();
1913        manager.start_task(parent.id, false).await.unwrap();
1914
1915        // Create multiple subtasks with various priorities
1916        let sub1 = manager
1917            .add_task("Priority 10", None, Some(parent.id))
1918            .await
1919            .unwrap();
1920        manager
1921            .update_task(sub1.id, None, None, None, None, None, Some(10))
1922            .await
1923            .unwrap();
1924
1925        let sub2 = manager
1926            .add_task("Priority 1", None, Some(parent.id))
1927            .await
1928            .unwrap();
1929        manager
1930            .update_task(sub2.id, None, None, None, None, None, Some(1))
1931            .await
1932            .unwrap();
1933
1934        let sub3 = manager
1935            .add_task("Priority 5", None, Some(parent.id))
1936            .await
1937            .unwrap();
1938        manager
1939            .update_task(sub3.id, None, None, None, None, None, Some(5))
1940            .await
1941            .unwrap();
1942
1943        // Pick next should recommend the task with priority 1 (lowest number)
1944        let response = manager.pick_next().await.unwrap();
1945
1946        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1947        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
1948        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
1949    }
1950
1951    #[tokio::test]
1952    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
1953        let ctx = TestContext::new().await;
1954        let manager = TaskManager::new(ctx.pool());
1955
1956        // Create parent without subtasks and set as current
1957        let parent = manager.add_task("Parent", None, None).await.unwrap();
1958        manager.start_task(parent.id, false).await.unwrap();
1959
1960        // Create another top-level task
1961        let top_level = manager
1962            .add_task("Top level task", None, None)
1963            .await
1964            .unwrap();
1965
1966        // Pick next should fall back to top-level task since parent has no todo subtasks
1967        let response = manager.pick_next().await.unwrap();
1968
1969        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1970        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
1971    }
1972
1973    // ===== Missing coverage tests =====
1974
1975    #[tokio::test]
1976    async fn test_get_task_with_events() {
1977        let ctx = TestContext::new().await;
1978        let task_mgr = TaskManager::new(ctx.pool());
1979        let event_mgr = EventManager::new(ctx.pool());
1980
1981        let task = task_mgr.add_task("Test", None, None).await.unwrap();
1982
1983        // Add some events
1984        event_mgr
1985            .add_event(task.id, "progress", "Event 1")
1986            .await
1987            .unwrap();
1988        event_mgr
1989            .add_event(task.id, "decision", "Event 2")
1990            .await
1991            .unwrap();
1992
1993        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1994
1995        assert_eq!(result.task.id, task.id);
1996        assert!(result.events_summary.is_some());
1997
1998        let summary = result.events_summary.unwrap();
1999        assert_eq!(summary.total_count, 2);
2000        assert_eq!(summary.recent_events.len(), 2);
2001        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
2002        assert_eq!(summary.recent_events[1].log_type, "progress");
2003    }
2004
2005    #[tokio::test]
2006    async fn test_get_task_with_events_nonexistent() {
2007        let ctx = TestContext::new().await;
2008        let task_mgr = TaskManager::new(ctx.pool());
2009
2010        let result = task_mgr.get_task_with_events(999).await;
2011        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2012    }
2013
2014    #[tokio::test]
2015    async fn test_get_task_with_many_events() {
2016        let ctx = TestContext::new().await;
2017        let task_mgr = TaskManager::new(ctx.pool());
2018        let event_mgr = EventManager::new(ctx.pool());
2019
2020        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2021
2022        // Add 20 events
2023        for i in 0..20 {
2024            event_mgr
2025                .add_event(task.id, "test", &format!("Event {}", i))
2026                .await
2027                .unwrap();
2028        }
2029
2030        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2031        let summary = result.events_summary.unwrap();
2032
2033        assert_eq!(summary.total_count, 20);
2034        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
2035    }
2036
2037    #[tokio::test]
2038    async fn test_get_task_with_no_events() {
2039        let ctx = TestContext::new().await;
2040        let task_mgr = TaskManager::new(ctx.pool());
2041
2042        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2043
2044        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2045        let summary = result.events_summary.unwrap();
2046
2047        assert_eq!(summary.total_count, 0);
2048        assert_eq!(summary.recent_events.len(), 0);
2049    }
2050
2051    #[tokio::test]
2052    async fn test_pick_next_tasks_zero_capacity() {
2053        let ctx = TestContext::new().await;
2054        let task_mgr = TaskManager::new(ctx.pool());
2055
2056        task_mgr.add_task("Task 1", None, None).await.unwrap();
2057
2058        // capacity_limit = 0 means no capacity available
2059        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2060        assert_eq!(results.len(), 0);
2061    }
2062
2063    #[tokio::test]
2064    async fn test_pick_next_tasks_capacity_exceeds_available() {
2065        let ctx = TestContext::new().await;
2066        let task_mgr = TaskManager::new(ctx.pool());
2067
2068        task_mgr.add_task("Task 1", None, None).await.unwrap();
2069        task_mgr.add_task("Task 2", None, None).await.unwrap();
2070
2071        // Request 10 tasks but only 2 available, capacity = 100
2072        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2073        assert_eq!(results.len(), 2); // Only returns available tasks
2074    }
2075
2076    // ========== task_context tests ==========
2077
2078    #[tokio::test]
2079    async fn test_get_task_context_root_task_no_relations() {
2080        let ctx = TestContext::new().await;
2081        let task_mgr = TaskManager::new(ctx.pool());
2082
2083        // Create a single root task with no relations
2084        let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2085
2086        let context = task_mgr.get_task_context(task.id).await.unwrap();
2087
2088        // Verify task itself
2089        assert_eq!(context.task.id, task.id);
2090        assert_eq!(context.task.name, "Root task");
2091
2092        // No ancestors (root task)
2093        assert_eq!(context.ancestors.len(), 0);
2094
2095        // No siblings
2096        assert_eq!(context.siblings.len(), 0);
2097
2098        // No children
2099        assert_eq!(context.children.len(), 0);
2100    }
2101
2102    #[tokio::test]
2103    async fn test_get_task_context_with_siblings() {
2104        let ctx = TestContext::new().await;
2105        let task_mgr = TaskManager::new(ctx.pool());
2106
2107        // Create multiple root tasks (siblings)
2108        let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2109        let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2110        let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2111
2112        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2113
2114        // Verify task itself
2115        assert_eq!(context.task.id, task2.id);
2116
2117        // No ancestors (root task)
2118        assert_eq!(context.ancestors.len(), 0);
2119
2120        // Should have 2 siblings
2121        assert_eq!(context.siblings.len(), 2);
2122        let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2123        assert!(sibling_ids.contains(&task1.id));
2124        assert!(sibling_ids.contains(&task3.id));
2125        assert!(!sibling_ids.contains(&task2.id)); // Should not include itself
2126
2127        // No children
2128        assert_eq!(context.children.len(), 0);
2129    }
2130
2131    #[tokio::test]
2132    async fn test_get_task_context_with_parent() {
2133        let ctx = TestContext::new().await;
2134        let task_mgr = TaskManager::new(ctx.pool());
2135
2136        // Create parent-child relationship
2137        let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2138        let child = task_mgr
2139            .add_task("Child task", None, Some(parent.id))
2140            .await
2141            .unwrap();
2142
2143        let context = task_mgr.get_task_context(child.id).await.unwrap();
2144
2145        // Verify task itself
2146        assert_eq!(context.task.id, child.id);
2147        assert_eq!(context.task.parent_id, Some(parent.id));
2148
2149        // Should have 1 ancestor (the parent)
2150        assert_eq!(context.ancestors.len(), 1);
2151        assert_eq!(context.ancestors[0].id, parent.id);
2152        assert_eq!(context.ancestors[0].name, "Parent task");
2153
2154        // No siblings
2155        assert_eq!(context.siblings.len(), 0);
2156
2157        // No children
2158        assert_eq!(context.children.len(), 0);
2159    }
2160
2161    #[tokio::test]
2162    async fn test_get_task_context_with_children() {
2163        let ctx = TestContext::new().await;
2164        let task_mgr = TaskManager::new(ctx.pool());
2165
2166        // Create parent with multiple children
2167        let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2168        let child1 = task_mgr
2169            .add_task("Child 1", None, Some(parent.id))
2170            .await
2171            .unwrap();
2172        let child2 = task_mgr
2173            .add_task("Child 2", None, Some(parent.id))
2174            .await
2175            .unwrap();
2176        let child3 = task_mgr
2177            .add_task("Child 3", None, Some(parent.id))
2178            .await
2179            .unwrap();
2180
2181        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2182
2183        // Verify task itself
2184        assert_eq!(context.task.id, parent.id);
2185
2186        // No ancestors (root task)
2187        assert_eq!(context.ancestors.len(), 0);
2188
2189        // No siblings
2190        assert_eq!(context.siblings.len(), 0);
2191
2192        // Should have 3 children
2193        assert_eq!(context.children.len(), 3);
2194        let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2195        assert!(child_ids.contains(&child1.id));
2196        assert!(child_ids.contains(&child2.id));
2197        assert!(child_ids.contains(&child3.id));
2198    }
2199
2200    #[tokio::test]
2201    async fn test_get_task_context_multi_level_hierarchy() {
2202        let ctx = TestContext::new().await;
2203        let task_mgr = TaskManager::new(ctx.pool());
2204
2205        // Create 3-level hierarchy: grandparent -> parent -> child
2206        let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2207        let parent = task_mgr
2208            .add_task("Parent", None, Some(grandparent.id))
2209            .await
2210            .unwrap();
2211        let child = task_mgr
2212            .add_task("Child", None, Some(parent.id))
2213            .await
2214            .unwrap();
2215
2216        let context = task_mgr.get_task_context(child.id).await.unwrap();
2217
2218        // Verify task itself
2219        assert_eq!(context.task.id, child.id);
2220
2221        // Should have 2 ancestors (parent and grandparent, ordered from immediate to root)
2222        assert_eq!(context.ancestors.len(), 2);
2223        assert_eq!(context.ancestors[0].id, parent.id);
2224        assert_eq!(context.ancestors[0].name, "Parent");
2225        assert_eq!(context.ancestors[1].id, grandparent.id);
2226        assert_eq!(context.ancestors[1].name, "Grandparent");
2227
2228        // No siblings
2229        assert_eq!(context.siblings.len(), 0);
2230
2231        // No children
2232        assert_eq!(context.children.len(), 0);
2233    }
2234
2235    #[tokio::test]
2236    async fn test_get_task_context_complex_family_tree() {
2237        let ctx = TestContext::new().await;
2238        let task_mgr = TaskManager::new(ctx.pool());
2239
2240        // Create complex structure:
2241        // Root
2242        //  ├─ Child1
2243        //  │   ├─ Grandchild1
2244        //  │   └─ Grandchild2 (target)
2245        //  └─ Child2
2246
2247        let root = task_mgr.add_task("Root", None, None).await.unwrap();
2248        let child1 = task_mgr
2249            .add_task("Child1", None, Some(root.id))
2250            .await
2251            .unwrap();
2252        let child2 = task_mgr
2253            .add_task("Child2", None, Some(root.id))
2254            .await
2255            .unwrap();
2256        let grandchild1 = task_mgr
2257            .add_task("Grandchild1", None, Some(child1.id))
2258            .await
2259            .unwrap();
2260        let grandchild2 = task_mgr
2261            .add_task("Grandchild2", None, Some(child1.id))
2262            .await
2263            .unwrap();
2264
2265        // Get context for grandchild2
2266        let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2267
2268        // Verify task itself
2269        assert_eq!(context.task.id, grandchild2.id);
2270
2271        // Should have 2 ancestors: child1 and root
2272        assert_eq!(context.ancestors.len(), 2);
2273        assert_eq!(context.ancestors[0].id, child1.id);
2274        assert_eq!(context.ancestors[1].id, root.id);
2275
2276        // Should have 1 sibling: grandchild1
2277        assert_eq!(context.siblings.len(), 1);
2278        assert_eq!(context.siblings[0].id, grandchild1.id);
2279
2280        // No children
2281        assert_eq!(context.children.len(), 0);
2282
2283        // Now get context for child1 to verify it sees both grandchildren
2284        let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2285        assert_eq!(context_child1.ancestors.len(), 1);
2286        assert_eq!(context_child1.ancestors[0].id, root.id);
2287        assert_eq!(context_child1.siblings.len(), 1);
2288        assert_eq!(context_child1.siblings[0].id, child2.id);
2289        assert_eq!(context_child1.children.len(), 2);
2290    }
2291
2292    #[tokio::test]
2293    async fn test_get_task_context_respects_priority_ordering() {
2294        let ctx = TestContext::new().await;
2295        let task_mgr = TaskManager::new(ctx.pool());
2296
2297        // Create parent with children having different priorities
2298        let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2299
2300        // Add children with priorities (lower number = higher priority)
2301        let child_low = task_mgr
2302            .add_task("Low priority", None, Some(parent.id))
2303            .await
2304            .unwrap();
2305        let _ = task_mgr
2306            .update_task(child_low.id, None, None, None, None, None, Some(10))
2307            .await
2308            .unwrap();
2309
2310        let child_high = task_mgr
2311            .add_task("High priority", None, Some(parent.id))
2312            .await
2313            .unwrap();
2314        let _ = task_mgr
2315            .update_task(child_high.id, None, None, None, None, None, Some(1))
2316            .await
2317            .unwrap();
2318
2319        let child_medium = task_mgr
2320            .add_task("Medium priority", None, Some(parent.id))
2321            .await
2322            .unwrap();
2323        let _ = task_mgr
2324            .update_task(child_medium.id, None, None, None, None, None, Some(5))
2325            .await
2326            .unwrap();
2327
2328        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2329
2330        // Children should be ordered by priority (1, 5, 10)
2331        assert_eq!(context.children.len(), 3);
2332        assert_eq!(context.children[0].priority, Some(1));
2333        assert_eq!(context.children[1].priority, Some(5));
2334        assert_eq!(context.children[2].priority, Some(10));
2335    }
2336
2337    #[tokio::test]
2338    async fn test_get_task_context_nonexistent_task() {
2339        let ctx = TestContext::new().await;
2340        let task_mgr = TaskManager::new(ctx.pool());
2341
2342        let result = task_mgr.get_task_context(99999).await;
2343        assert!(result.is_err());
2344        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2345    }
2346
2347    #[tokio::test]
2348    async fn test_get_task_context_handles_null_priority() {
2349        let ctx = TestContext::new().await;
2350        let task_mgr = TaskManager::new(ctx.pool());
2351
2352        // Create siblings with mixed null and set priorities
2353        let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2354        let _ = task_mgr
2355            .update_task(task1.id, None, None, None, None, None, Some(1))
2356            .await
2357            .unwrap();
2358
2359        let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2360        // task2 has NULL priority
2361
2362        let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2363        let _ = task_mgr
2364            .update_task(task3.id, None, None, None, None, None, Some(5))
2365            .await
2366            .unwrap();
2367
2368        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2369
2370        // Should have 2 siblings, ordered by priority (non-null first, then null)
2371        assert_eq!(context.siblings.len(), 2);
2372        // Task with priority 1 should come first
2373        assert_eq!(context.siblings[0].id, task1.id);
2374        assert_eq!(context.siblings[0].priority, Some(1));
2375        // Task with priority 5 should come second
2376        assert_eq!(context.siblings[1].id, task3.id);
2377        assert_eq!(context.siblings[1].priority, Some(5));
2378    }
2379}