intent_engine/
tasks.rs

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