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