intent_engine/
tasks.rs

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