intent_engine/
tasks.rs

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