intent_engine/
tasks.rs

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