intent_engine/
tasks.rs

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