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) DESC,
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) DESC,
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 {
1140            if let Ok(current_id) = current_id_str.parse::<i64>() {
1141                // First priority: Get todo subtasks of current focused task
1142                // Exclude tasks blocked by incomplete dependencies
1143                let 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 = 'todo'
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) = subtasks {
1164                    return Ok(PickNextResponse::focused_subtask(task));
1165                }
1166            }
1167        }
1168
1169        // Step 2: Second priority - get top-level todo tasks
1170        // Exclude tasks blocked by incomplete dependencies
1171        let top_level_task = sqlx::query_as::<_, Task>(
1172            r#"
1173            SELECT id, parent_id, name, spec, status, complexity, priority,
1174                   first_todo_at, first_doing_at, first_done_at, active_form
1175            FROM tasks
1176            WHERE parent_id IS NULL AND status = 'todo'
1177              AND NOT EXISTS (
1178                SELECT 1 FROM dependencies d
1179                JOIN tasks bt ON d.blocking_task_id = bt.id
1180                WHERE d.blocked_task_id = tasks.id
1181                  AND bt.status != 'done'
1182              )
1183            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1184            LIMIT 1
1185            "#,
1186        )
1187        .fetch_optional(self.pool)
1188        .await?;
1189
1190        if let Some(task) = top_level_task {
1191            return Ok(PickNextResponse::top_level_task(task));
1192        }
1193
1194        // Step 3: No recommendation - determine why
1195        // Check if there are any tasks at all
1196        let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
1197            .fetch_one(self.pool)
1198            .await?;
1199
1200        if total_tasks == 0 {
1201            return Ok(PickNextResponse::no_tasks_in_project());
1202        }
1203
1204        // Check if all tasks are completed
1205        let todo_or_doing_count: i64 =
1206            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1207                .fetch_one(self.pool)
1208                .await?;
1209
1210        if todo_or_doing_count == 0 {
1211            return Ok(PickNextResponse::all_tasks_completed());
1212        }
1213
1214        // Otherwise, there are tasks but none available based on current context
1215        Ok(PickNextResponse::no_available_todos())
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use crate::events::EventManager;
1223    use crate::test_utils::test_helpers::TestContext;
1224
1225    #[tokio::test]
1226    async fn test_add_task() {
1227        let ctx = TestContext::new().await;
1228        let manager = TaskManager::new(ctx.pool());
1229
1230        let task = manager.add_task("Test task", None, None).await.unwrap();
1231
1232        assert_eq!(task.name, "Test task");
1233        assert_eq!(task.status, "todo");
1234        assert!(task.first_todo_at.is_some());
1235        assert!(task.first_doing_at.is_none());
1236        assert!(task.first_done_at.is_none());
1237    }
1238
1239    #[tokio::test]
1240    async fn test_add_task_with_spec() {
1241        let ctx = TestContext::new().await;
1242        let manager = TaskManager::new(ctx.pool());
1243
1244        let spec = "This is a task specification";
1245        let task = manager
1246            .add_task("Test task", Some(spec), None)
1247            .await
1248            .unwrap();
1249
1250        assert_eq!(task.name, "Test task");
1251        assert_eq!(task.spec.as_deref(), Some(spec));
1252    }
1253
1254    #[tokio::test]
1255    async fn test_add_task_with_parent() {
1256        let ctx = TestContext::new().await;
1257        let manager = TaskManager::new(ctx.pool());
1258
1259        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1260        let child = manager
1261            .add_task("Child task", None, Some(parent.id))
1262            .await
1263            .unwrap();
1264
1265        assert_eq!(child.parent_id, Some(parent.id));
1266    }
1267
1268    #[tokio::test]
1269    async fn test_get_task() {
1270        let ctx = TestContext::new().await;
1271        let manager = TaskManager::new(ctx.pool());
1272
1273        let created = manager.add_task("Test task", None, None).await.unwrap();
1274        let retrieved = manager.get_task(created.id).await.unwrap();
1275
1276        assert_eq!(created.id, retrieved.id);
1277        assert_eq!(created.name, retrieved.name);
1278    }
1279
1280    #[tokio::test]
1281    async fn test_get_task_not_found() {
1282        let ctx = TestContext::new().await;
1283        let manager = TaskManager::new(ctx.pool());
1284
1285        let result = manager.get_task(999).await;
1286        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1287    }
1288
1289    #[tokio::test]
1290    async fn test_update_task_name() {
1291        let ctx = TestContext::new().await;
1292        let manager = TaskManager::new(ctx.pool());
1293
1294        let task = manager.add_task("Original name", None, None).await.unwrap();
1295        let updated = manager
1296            .update_task(task.id, Some("New name"), None, None, None, None, None)
1297            .await
1298            .unwrap();
1299
1300        assert_eq!(updated.name, "New name");
1301    }
1302
1303    #[tokio::test]
1304    async fn test_update_task_status() {
1305        let ctx = TestContext::new().await;
1306        let manager = TaskManager::new(ctx.pool());
1307
1308        let task = manager.add_task("Test task", None, None).await.unwrap();
1309        let updated = manager
1310            .update_task(task.id, None, None, None, Some("doing"), None, None)
1311            .await
1312            .unwrap();
1313
1314        assert_eq!(updated.status, "doing");
1315        assert!(updated.first_doing_at.is_some());
1316    }
1317
1318    #[tokio::test]
1319    async fn test_delete_task() {
1320        let ctx = TestContext::new().await;
1321        let manager = TaskManager::new(ctx.pool());
1322
1323        let task = manager.add_task("Test task", None, None).await.unwrap();
1324        manager.delete_task(task.id).await.unwrap();
1325
1326        let result = manager.get_task(task.id).await;
1327        assert!(result.is_err());
1328    }
1329
1330    #[tokio::test]
1331    async fn test_find_tasks_by_status() {
1332        let ctx = TestContext::new().await;
1333        let manager = TaskManager::new(ctx.pool());
1334
1335        manager.add_task("Todo task", None, None).await.unwrap();
1336        let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1337        manager
1338            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1339            .await
1340            .unwrap();
1341
1342        let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1343        let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1344
1345        assert_eq!(todo_tasks.len(), 1);
1346        assert_eq!(doing_tasks.len(), 1);
1347        assert_eq!(doing_tasks[0].status, "doing");
1348    }
1349
1350    #[tokio::test]
1351    async fn test_find_tasks_by_parent() {
1352        let ctx = TestContext::new().await;
1353        let manager = TaskManager::new(ctx.pool());
1354
1355        let parent = manager.add_task("Parent", None, None).await.unwrap();
1356        manager
1357            .add_task("Child 1", None, Some(parent.id))
1358            .await
1359            .unwrap();
1360        manager
1361            .add_task("Child 2", None, Some(parent.id))
1362            .await
1363            .unwrap();
1364
1365        let children = manager
1366            .find_tasks(None, Some(Some(parent.id)))
1367            .await
1368            .unwrap();
1369
1370        assert_eq!(children.len(), 2);
1371    }
1372
1373    #[tokio::test]
1374    async fn test_start_task() {
1375        let ctx = TestContext::new().await;
1376        let manager = TaskManager::new(ctx.pool());
1377
1378        let task = manager.add_task("Test task", None, None).await.unwrap();
1379        let started = manager.start_task(task.id, false).await.unwrap();
1380
1381        assert_eq!(started.task.status, "doing");
1382        assert!(started.task.first_doing_at.is_some());
1383
1384        // Verify it's set as current task
1385        let current: Option<String> =
1386            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1387                .fetch_optional(ctx.pool())
1388                .await
1389                .unwrap();
1390
1391        assert_eq!(current, Some(task.id.to_string()));
1392    }
1393
1394    #[tokio::test]
1395    async fn test_start_task_with_events() {
1396        let ctx = TestContext::new().await;
1397        let manager = TaskManager::new(ctx.pool());
1398
1399        let task = manager.add_task("Test task", None, None).await.unwrap();
1400
1401        // Add an event
1402        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1403            .bind(task.id)
1404            .bind("test")
1405            .bind("test event")
1406            .execute(ctx.pool())
1407            .await
1408            .unwrap();
1409
1410        let started = manager.start_task(task.id, true).await.unwrap();
1411
1412        assert!(started.events_summary.is_some());
1413        let summary = started.events_summary.unwrap();
1414        assert_eq!(summary.total_count, 1);
1415    }
1416
1417    #[tokio::test]
1418    async fn test_done_task() {
1419        let ctx = TestContext::new().await;
1420        let manager = TaskManager::new(ctx.pool());
1421
1422        let task = manager.add_task("Test task", None, None).await.unwrap();
1423        manager.start_task(task.id, false).await.unwrap();
1424        let response = manager.done_task().await.unwrap();
1425
1426        assert_eq!(response.completed_task.status, "done");
1427        assert!(response.completed_task.first_done_at.is_some());
1428        assert_eq!(response.workspace_status.current_task_id, None);
1429
1430        // Should be WORKSPACE_IS_CLEAR since it's the only task
1431        match response.next_step_suggestion {
1432            NextStepSuggestion::WorkspaceIsClear { .. } => {},
1433            _ => panic!("Expected WorkspaceIsClear suggestion"),
1434        }
1435
1436        // Verify current task is cleared
1437        let current: Option<String> =
1438            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1439                .fetch_optional(ctx.pool())
1440                .await
1441                .unwrap();
1442
1443        assert!(current.is_none());
1444    }
1445
1446    #[tokio::test]
1447    async fn test_done_task_with_uncompleted_children() {
1448        let ctx = TestContext::new().await;
1449        let manager = TaskManager::new(ctx.pool());
1450
1451        let parent = manager.add_task("Parent", None, None).await.unwrap();
1452        manager
1453            .add_task("Child", None, Some(parent.id))
1454            .await
1455            .unwrap();
1456
1457        // Set parent as current task
1458        manager.start_task(parent.id, false).await.unwrap();
1459
1460        let result = manager.done_task().await;
1461        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1462    }
1463
1464    #[tokio::test]
1465    async fn test_done_task_with_completed_children() {
1466        let ctx = TestContext::new().await;
1467        let manager = TaskManager::new(ctx.pool());
1468
1469        let parent = manager.add_task("Parent", None, None).await.unwrap();
1470        let child = manager
1471            .add_task("Child", None, Some(parent.id))
1472            .await
1473            .unwrap();
1474
1475        // Complete child first
1476        manager.start_task(child.id, false).await.unwrap();
1477        let child_response = manager.done_task().await.unwrap();
1478
1479        // Child completion should suggest parent is ready
1480        match child_response.next_step_suggestion {
1481            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1482                assert_eq!(parent_task_id, parent.id);
1483            },
1484            _ => panic!("Expected ParentIsReady suggestion"),
1485        }
1486
1487        // Now parent can be completed
1488        manager.start_task(parent.id, false).await.unwrap();
1489        let parent_response = manager.done_task().await.unwrap();
1490        assert_eq!(parent_response.completed_task.status, "done");
1491
1492        // Parent completion should indicate top-level task completed (since it had children)
1493        match parent_response.next_step_suggestion {
1494            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1495            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1496        }
1497    }
1498
1499    #[tokio::test]
1500    async fn test_circular_dependency() {
1501        let ctx = TestContext::new().await;
1502        let manager = TaskManager::new(ctx.pool());
1503
1504        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1505        let task2 = manager
1506            .add_task("Task 2", None, Some(task1.id))
1507            .await
1508            .unwrap();
1509
1510        // Try to make task1 a child of task2 (circular)
1511        let result = manager
1512            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1513            .await;
1514
1515        assert!(matches!(
1516            result,
1517            Err(IntentError::CircularDependency { .. })
1518        ));
1519    }
1520
1521    #[tokio::test]
1522    async fn test_invalid_parent_id() {
1523        let ctx = TestContext::new().await;
1524        let manager = TaskManager::new(ctx.pool());
1525
1526        let result = manager.add_task("Test", None, Some(999)).await;
1527        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1528    }
1529
1530    #[tokio::test]
1531    async fn test_update_task_complexity_and_priority() {
1532        let ctx = TestContext::new().await;
1533        let manager = TaskManager::new(ctx.pool());
1534
1535        let task = manager.add_task("Test task", None, None).await.unwrap();
1536        let updated = manager
1537            .update_task(task.id, None, None, None, None, Some(8), Some(10))
1538            .await
1539            .unwrap();
1540
1541        assert_eq!(updated.complexity, Some(8));
1542        assert_eq!(updated.priority, Some(10));
1543    }
1544
1545    #[tokio::test]
1546    async fn test_spawn_subtask() {
1547        let ctx = TestContext::new().await;
1548        let manager = TaskManager::new(ctx.pool());
1549
1550        // Create and start a parent task
1551        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1552        manager.start_task(parent.id, false).await.unwrap();
1553
1554        // Spawn a subtask
1555        let response = manager
1556            .spawn_subtask("Child task", Some("Details"))
1557            .await
1558            .unwrap();
1559
1560        assert_eq!(response.subtask.parent_id, parent.id);
1561        assert_eq!(response.subtask.name, "Child task");
1562        assert_eq!(response.subtask.status, "doing");
1563        assert_eq!(response.parent_task.id, parent.id);
1564        assert_eq!(response.parent_task.name, "Parent task");
1565
1566        // Verify subtask is now the current task
1567        let current: Option<String> =
1568            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1569                .fetch_optional(ctx.pool())
1570                .await
1571                .unwrap();
1572
1573        assert_eq!(current, Some(response.subtask.id.to_string()));
1574
1575        // Verify subtask is in doing status
1576        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1577        assert_eq!(retrieved.status, "doing");
1578    }
1579
1580    #[tokio::test]
1581    async fn test_spawn_subtask_no_current_task() {
1582        let ctx = TestContext::new().await;
1583        let manager = TaskManager::new(ctx.pool());
1584
1585        // Try to spawn subtask without a current task
1586        let result = manager.spawn_subtask("Child", None).await;
1587        assert!(result.is_err());
1588    }
1589
1590    #[tokio::test]
1591    async fn test_pick_next_tasks_basic() {
1592        let ctx = TestContext::new().await;
1593        let manager = TaskManager::new(ctx.pool());
1594
1595        // Create 10 todo tasks
1596        for i in 1..=10 {
1597            manager
1598                .add_task(&format!("Task {}", i), None, None)
1599                .await
1600                .unwrap();
1601        }
1602
1603        // Pick 5 tasks with capacity limit of 5
1604        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1605
1606        assert_eq!(picked.len(), 5);
1607        for task in &picked {
1608            assert_eq!(task.status, "doing");
1609            assert!(task.first_doing_at.is_some());
1610        }
1611
1612        // Verify total doing count
1613        let doing_count: i64 =
1614            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1615                .fetch_one(ctx.pool())
1616                .await
1617                .unwrap();
1618
1619        assert_eq!(doing_count, 5);
1620    }
1621
1622    #[tokio::test]
1623    async fn test_pick_next_tasks_with_existing_doing() {
1624        let ctx = TestContext::new().await;
1625        let manager = TaskManager::new(ctx.pool());
1626
1627        // Create 10 todo tasks
1628        for i in 1..=10 {
1629            manager
1630                .add_task(&format!("Task {}", i), None, None)
1631                .await
1632                .unwrap();
1633        }
1634
1635        // Start 2 tasks
1636        let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1637        manager.start_task(tasks[0].id, false).await.unwrap();
1638        manager.start_task(tasks[1].id, false).await.unwrap();
1639
1640        // Pick more tasks with capacity limit of 5
1641        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1642
1643        // Should only pick 3 more (5 - 2 = 3)
1644        assert_eq!(picked.len(), 3);
1645
1646        // Verify total doing count
1647        let doing_count: i64 =
1648            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1649                .fetch_one(ctx.pool())
1650                .await
1651                .unwrap();
1652
1653        assert_eq!(doing_count, 5);
1654    }
1655
1656    #[tokio::test]
1657    async fn test_pick_next_tasks_at_capacity() {
1658        let ctx = TestContext::new().await;
1659        let manager = TaskManager::new(ctx.pool());
1660
1661        // Create 10 tasks
1662        for i in 1..=10 {
1663            manager
1664                .add_task(&format!("Task {}", i), None, None)
1665                .await
1666                .unwrap();
1667        }
1668
1669        // Fill capacity
1670        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1671        assert_eq!(first_batch.len(), 5);
1672
1673        // Try to pick more (should return empty)
1674        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1675        assert_eq!(second_batch.len(), 0);
1676    }
1677
1678    #[tokio::test]
1679    async fn test_pick_next_tasks_priority_ordering() {
1680        let ctx = TestContext::new().await;
1681        let manager = TaskManager::new(ctx.pool());
1682
1683        // Create tasks with different priorities
1684        let low = manager.add_task("Low priority", None, None).await.unwrap();
1685        manager
1686            .update_task(low.id, None, None, None, None, None, Some(1))
1687            .await
1688            .unwrap();
1689
1690        let high = manager.add_task("High priority", None, None).await.unwrap();
1691        manager
1692            .update_task(high.id, None, None, None, None, None, Some(10))
1693            .await
1694            .unwrap();
1695
1696        let medium = manager
1697            .add_task("Medium priority", None, None)
1698            .await
1699            .unwrap();
1700        manager
1701            .update_task(medium.id, None, None, None, None, None, Some(5))
1702            .await
1703            .unwrap();
1704
1705        // Pick tasks
1706        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1707
1708        // Should be ordered by priority DESC
1709        assert_eq!(picked.len(), 3);
1710        assert_eq!(picked[0].priority, Some(10)); // high
1711        assert_eq!(picked[1].priority, Some(5)); // medium
1712        assert_eq!(picked[2].priority, Some(1)); // low
1713    }
1714
1715    #[tokio::test]
1716    async fn test_pick_next_tasks_complexity_ordering() {
1717        let ctx = TestContext::new().await;
1718        let manager = TaskManager::new(ctx.pool());
1719
1720        // Create tasks with different complexities (same priority)
1721        let complex = manager.add_task("Complex", None, None).await.unwrap();
1722        manager
1723            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1724            .await
1725            .unwrap();
1726
1727        let simple = manager.add_task("Simple", None, None).await.unwrap();
1728        manager
1729            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1730            .await
1731            .unwrap();
1732
1733        let medium = manager.add_task("Medium", None, None).await.unwrap();
1734        manager
1735            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1736            .await
1737            .unwrap();
1738
1739        // Pick tasks
1740        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1741
1742        // Should be ordered by complexity ASC (simple first)
1743        assert_eq!(picked.len(), 3);
1744        assert_eq!(picked[0].complexity, Some(1)); // simple
1745        assert_eq!(picked[1].complexity, Some(5)); // medium
1746        assert_eq!(picked[2].complexity, Some(9)); // complex
1747    }
1748
1749    #[tokio::test]
1750    async fn test_done_task_sibling_tasks_remain() {
1751        let ctx = TestContext::new().await;
1752        let manager = TaskManager::new(ctx.pool());
1753
1754        // Create parent with multiple children
1755        let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1756        let child1 = manager
1757            .add_task("Child 1", None, Some(parent.id))
1758            .await
1759            .unwrap();
1760        let child2 = manager
1761            .add_task("Child 2", None, Some(parent.id))
1762            .await
1763            .unwrap();
1764        let _child3 = manager
1765            .add_task("Child 3", None, Some(parent.id))
1766            .await
1767            .unwrap();
1768
1769        // Complete first child
1770        manager.start_task(child1.id, false).await.unwrap();
1771        let response = manager.done_task().await.unwrap();
1772
1773        // Should indicate siblings remain
1774        match response.next_step_suggestion {
1775            NextStepSuggestion::SiblingTasksRemain {
1776                parent_task_id,
1777                remaining_siblings_count,
1778                ..
1779            } => {
1780                assert_eq!(parent_task_id, parent.id);
1781                assert_eq!(remaining_siblings_count, 2); // child2 and child3
1782            },
1783            _ => panic!("Expected SiblingTasksRemain suggestion"),
1784        }
1785
1786        // Complete second child
1787        manager.start_task(child2.id, false).await.unwrap();
1788        let response2 = manager.done_task().await.unwrap();
1789
1790        // Should still indicate siblings remain
1791        match response2.next_step_suggestion {
1792            NextStepSuggestion::SiblingTasksRemain {
1793                remaining_siblings_count,
1794                ..
1795            } => {
1796                assert_eq!(remaining_siblings_count, 1); // only child3
1797            },
1798            _ => panic!("Expected SiblingTasksRemain suggestion"),
1799        }
1800    }
1801
1802    #[tokio::test]
1803    async fn test_done_task_top_level_with_children() {
1804        let ctx = TestContext::new().await;
1805        let manager = TaskManager::new(ctx.pool());
1806
1807        // Create top-level task with children
1808        let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1809        let child = manager
1810            .add_task("Sub Task", None, Some(parent.id))
1811            .await
1812            .unwrap();
1813
1814        // Complete child first
1815        manager.start_task(child.id, false).await.unwrap();
1816        manager.done_task().await.unwrap();
1817
1818        // Complete parent
1819        manager.start_task(parent.id, false).await.unwrap();
1820        let response = manager.done_task().await.unwrap();
1821
1822        // Should be TOP_LEVEL_TASK_COMPLETED
1823        match response.next_step_suggestion {
1824            NextStepSuggestion::TopLevelTaskCompleted {
1825                completed_task_id,
1826                completed_task_name,
1827                ..
1828            } => {
1829                assert_eq!(completed_task_id, parent.id);
1830                assert_eq!(completed_task_name, "Epic Task");
1831            },
1832            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1833        }
1834    }
1835
1836    #[tokio::test]
1837    async fn test_done_task_no_parent_context() {
1838        let ctx = TestContext::new().await;
1839        let manager = TaskManager::new(ctx.pool());
1840
1841        // Create multiple standalone tasks
1842        let task1 = manager
1843            .add_task("Standalone Task 1", None, None)
1844            .await
1845            .unwrap();
1846        let _task2 = manager
1847            .add_task("Standalone Task 2", None, None)
1848            .await
1849            .unwrap();
1850
1851        // Complete first task
1852        manager.start_task(task1.id, false).await.unwrap();
1853        let response = manager.done_task().await.unwrap();
1854
1855        // Should be NO_PARENT_CONTEXT since task2 is still pending
1856        match response.next_step_suggestion {
1857            NextStepSuggestion::NoParentContext {
1858                completed_task_id,
1859                completed_task_name,
1860                ..
1861            } => {
1862                assert_eq!(completed_task_id, task1.id);
1863                assert_eq!(completed_task_name, "Standalone Task 1");
1864            },
1865            _ => panic!("Expected NoParentContext suggestion"),
1866        }
1867    }
1868
1869    #[tokio::test]
1870    async fn test_search_tasks_by_name() {
1871        let ctx = TestContext::new().await;
1872        let manager = TaskManager::new(ctx.pool());
1873
1874        // Create tasks with different names
1875        manager
1876            .add_task("Authentication bug fix", Some("Fix login issue"), None)
1877            .await
1878            .unwrap();
1879        manager
1880            .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1881            .await
1882            .unwrap();
1883        manager
1884            .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1885            .await
1886            .unwrap();
1887
1888        // Search for "authentication"
1889        let results = manager.search_tasks("authentication").await.unwrap();
1890
1891        assert_eq!(results.len(), 2);
1892        assert!(results[0]
1893            .task
1894            .name
1895            .to_lowercase()
1896            .contains("authentication"));
1897        assert!(results[1]
1898            .task
1899            .name
1900            .to_lowercase()
1901            .contains("authentication"));
1902
1903        // Check that match_snippet is present
1904        assert!(!results[0].match_snippet.is_empty());
1905    }
1906
1907    #[tokio::test]
1908    async fn test_search_tasks_by_spec() {
1909        let ctx = TestContext::new().await;
1910        let manager = TaskManager::new(ctx.pool());
1911
1912        // Create tasks
1913        manager
1914            .add_task("Task 1", Some("Implement JWT authentication"), None)
1915            .await
1916            .unwrap();
1917        manager
1918            .add_task("Task 2", Some("Add user registration"), None)
1919            .await
1920            .unwrap();
1921        manager
1922            .add_task("Task 3", Some("JWT token refresh"), None)
1923            .await
1924            .unwrap();
1925
1926        // Search for "JWT"
1927        let results = manager.search_tasks("JWT").await.unwrap();
1928
1929        assert_eq!(results.len(), 2);
1930        for result in &results {
1931            assert!(result
1932                .task
1933                .spec
1934                .as_ref()
1935                .unwrap()
1936                .to_uppercase()
1937                .contains("JWT"));
1938        }
1939    }
1940
1941    #[tokio::test]
1942    async fn test_search_tasks_with_advanced_query() {
1943        let ctx = TestContext::new().await;
1944        let manager = TaskManager::new(ctx.pool());
1945
1946        // Create tasks
1947        manager
1948            .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1949            .await
1950            .unwrap();
1951        manager
1952            .add_task("Feature", Some("Add authentication feature"), None)
1953            .await
1954            .unwrap();
1955        manager
1956            .add_task("Bug report", Some("Report critical database bug"), None)
1957            .await
1958            .unwrap();
1959
1960        // Search with AND operator
1961        let results = manager
1962            .search_tasks("authentication AND bug")
1963            .await
1964            .unwrap();
1965
1966        assert_eq!(results.len(), 1);
1967        assert!(results[0]
1968            .task
1969            .spec
1970            .as_ref()
1971            .unwrap()
1972            .contains("authentication"));
1973        assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1974    }
1975
1976    #[tokio::test]
1977    async fn test_search_tasks_no_results() {
1978        let ctx = TestContext::new().await;
1979        let manager = TaskManager::new(ctx.pool());
1980
1981        // Create tasks
1982        manager
1983            .add_task("Task 1", Some("Some description"), None)
1984            .await
1985            .unwrap();
1986
1987        // Search for non-existent term
1988        let results = manager.search_tasks("nonexistent").await.unwrap();
1989
1990        assert_eq!(results.len(), 0);
1991    }
1992
1993    #[tokio::test]
1994    async fn test_search_tasks_snippet_highlighting() {
1995        let ctx = TestContext::new().await;
1996        let manager = TaskManager::new(ctx.pool());
1997
1998        // Create task with keyword in spec
1999        manager
2000            .add_task(
2001                "Test task",
2002                Some("This is a description with the keyword authentication in the middle"),
2003                None,
2004            )
2005            .await
2006            .unwrap();
2007
2008        // Search for "authentication"
2009        let results = manager.search_tasks("authentication").await.unwrap();
2010
2011        assert_eq!(results.len(), 1);
2012        // Check that snippet contains highlighted keyword (marked with **)
2013        assert!(results[0].match_snippet.contains("**authentication**"));
2014    }
2015
2016    #[tokio::test]
2017    async fn test_pick_next_focused_subtask() {
2018        let ctx = TestContext::new().await;
2019        let manager = TaskManager::new(ctx.pool());
2020
2021        // Create parent task and set as current
2022        let parent = manager.add_task("Parent task", None, None).await.unwrap();
2023        manager.start_task(parent.id, false).await.unwrap();
2024
2025        // Create subtasks with different priorities
2026        let subtask1 = manager
2027            .add_task("Subtask 1", None, Some(parent.id))
2028            .await
2029            .unwrap();
2030        let subtask2 = manager
2031            .add_task("Subtask 2", None, Some(parent.id))
2032            .await
2033            .unwrap();
2034
2035        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
2036        manager
2037            .update_task(subtask1.id, None, None, None, None, None, Some(2))
2038            .await
2039            .unwrap();
2040        manager
2041            .update_task(subtask2.id, None, None, None, None, None, Some(1))
2042            .await
2043            .unwrap();
2044
2045        // Pick next should recommend subtask2 (priority 1)
2046        let response = manager.pick_next().await.unwrap();
2047
2048        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2049        assert!(response.task.is_some());
2050        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2051        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2052    }
2053
2054    #[tokio::test]
2055    async fn test_pick_next_top_level_task() {
2056        let ctx = TestContext::new().await;
2057        let manager = TaskManager::new(ctx.pool());
2058
2059        // Create top-level tasks with different priorities
2060        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
2061        let task2 = manager.add_task("Task 2", None, None).await.unwrap();
2062
2063        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
2064        manager
2065            .update_task(task1.id, None, None, None, None, None, Some(5))
2066            .await
2067            .unwrap();
2068        manager
2069            .update_task(task2.id, None, None, None, None, None, Some(3))
2070            .await
2071            .unwrap();
2072
2073        // Pick next should recommend task2 (priority 3)
2074        let response = manager.pick_next().await.unwrap();
2075
2076        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2077        assert!(response.task.is_some());
2078        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2079        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2080    }
2081
2082    #[tokio::test]
2083    async fn test_pick_next_no_tasks() {
2084        let ctx = TestContext::new().await;
2085        let manager = TaskManager::new(ctx.pool());
2086
2087        // No tasks created
2088        let response = manager.pick_next().await.unwrap();
2089
2090        assert_eq!(response.suggestion_type, "NONE");
2091        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2092        assert!(response.message.is_some());
2093    }
2094
2095    #[tokio::test]
2096    async fn test_pick_next_all_completed() {
2097        let ctx = TestContext::new().await;
2098        let manager = TaskManager::new(ctx.pool());
2099
2100        // Create task and mark as done
2101        let task = manager.add_task("Task 1", None, None).await.unwrap();
2102        manager.start_task(task.id, false).await.unwrap();
2103        manager.done_task().await.unwrap();
2104
2105        // Pick next should indicate all tasks completed
2106        let response = manager.pick_next().await.unwrap();
2107
2108        assert_eq!(response.suggestion_type, "NONE");
2109        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2110        assert!(response.message.is_some());
2111    }
2112
2113    #[tokio::test]
2114    async fn test_pick_next_no_available_todos() {
2115        let ctx = TestContext::new().await;
2116        let manager = TaskManager::new(ctx.pool());
2117
2118        // Create a parent task that's in "doing" status
2119        let parent = manager.add_task("Parent task", None, None).await.unwrap();
2120        manager.start_task(parent.id, false).await.unwrap();
2121
2122        // Create a subtask also in "doing" status (no "todo" subtasks)
2123        let subtask = manager
2124            .add_task("Subtask", None, Some(parent.id))
2125            .await
2126            .unwrap();
2127        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
2128        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2129            .bind(subtask.id)
2130            .execute(ctx.pool())
2131            .await
2132            .unwrap();
2133
2134        // Set subtask as current
2135        sqlx::query(
2136            "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
2137        )
2138        .bind(subtask.id.to_string())
2139        .execute(ctx.pool())
2140        .await
2141        .unwrap();
2142
2143        // Set parent to doing (not todo)
2144        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2145            .bind(parent.id)
2146            .execute(ctx.pool())
2147            .await
2148            .unwrap();
2149
2150        // Pick next should indicate no available todos
2151        let response = manager.pick_next().await.unwrap();
2152
2153        assert_eq!(response.suggestion_type, "NONE");
2154        assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
2155        assert!(response.message.is_some());
2156    }
2157
2158    #[tokio::test]
2159    async fn test_pick_next_priority_ordering() {
2160        let ctx = TestContext::new().await;
2161        let manager = TaskManager::new(ctx.pool());
2162
2163        // Create parent and set as current
2164        let parent = manager.add_task("Parent", None, None).await.unwrap();
2165        manager.start_task(parent.id, false).await.unwrap();
2166
2167        // Create multiple subtasks with various priorities
2168        let sub1 = manager
2169            .add_task("Priority 10", None, Some(parent.id))
2170            .await
2171            .unwrap();
2172        manager
2173            .update_task(sub1.id, None, None, None, None, None, Some(10))
2174            .await
2175            .unwrap();
2176
2177        let sub2 = manager
2178            .add_task("Priority 1", None, Some(parent.id))
2179            .await
2180            .unwrap();
2181        manager
2182            .update_task(sub2.id, None, None, None, None, None, Some(1))
2183            .await
2184            .unwrap();
2185
2186        let sub3 = manager
2187            .add_task("Priority 5", None, Some(parent.id))
2188            .await
2189            .unwrap();
2190        manager
2191            .update_task(sub3.id, None, None, None, None, None, Some(5))
2192            .await
2193            .unwrap();
2194
2195        // Pick next should recommend the task with priority 1 (lowest number)
2196        let response = manager.pick_next().await.unwrap();
2197
2198        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2199        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2200        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2201    }
2202
2203    #[tokio::test]
2204    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2205        let ctx = TestContext::new().await;
2206        let manager = TaskManager::new(ctx.pool());
2207
2208        // Create parent without subtasks and set as current
2209        let parent = manager.add_task("Parent", None, None).await.unwrap();
2210        manager.start_task(parent.id, false).await.unwrap();
2211
2212        // Create another top-level task
2213        let top_level = manager
2214            .add_task("Top level task", None, None)
2215            .await
2216            .unwrap();
2217
2218        // Pick next should fall back to top-level task since parent has no todo subtasks
2219        let response = manager.pick_next().await.unwrap();
2220
2221        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2222        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2223    }
2224
2225    // ===== Missing coverage tests =====
2226
2227    #[tokio::test]
2228    async fn test_get_task_with_events() {
2229        let ctx = TestContext::new().await;
2230        let task_mgr = TaskManager::new(ctx.pool());
2231        let event_mgr = EventManager::new(ctx.pool());
2232
2233        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2234
2235        // Add some events
2236        event_mgr
2237            .add_event(task.id, "progress", "Event 1")
2238            .await
2239            .unwrap();
2240        event_mgr
2241            .add_event(task.id, "decision", "Event 2")
2242            .await
2243            .unwrap();
2244
2245        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2246
2247        assert_eq!(result.task.id, task.id);
2248        assert!(result.events_summary.is_some());
2249
2250        let summary = result.events_summary.unwrap();
2251        assert_eq!(summary.total_count, 2);
2252        assert_eq!(summary.recent_events.len(), 2);
2253        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
2254        assert_eq!(summary.recent_events[1].log_type, "progress");
2255    }
2256
2257    #[tokio::test]
2258    async fn test_get_task_with_events_nonexistent() {
2259        let ctx = TestContext::new().await;
2260        let task_mgr = TaskManager::new(ctx.pool());
2261
2262        let result = task_mgr.get_task_with_events(999).await;
2263        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2264    }
2265
2266    #[tokio::test]
2267    async fn test_get_task_with_many_events() {
2268        let ctx = TestContext::new().await;
2269        let task_mgr = TaskManager::new(ctx.pool());
2270        let event_mgr = EventManager::new(ctx.pool());
2271
2272        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2273
2274        // Add 20 events
2275        for i in 0..20 {
2276            event_mgr
2277                .add_event(task.id, "test", &format!("Event {}", i))
2278                .await
2279                .unwrap();
2280        }
2281
2282        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2283        let summary = result.events_summary.unwrap();
2284
2285        assert_eq!(summary.total_count, 20);
2286        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
2287    }
2288
2289    #[tokio::test]
2290    async fn test_get_task_with_no_events() {
2291        let ctx = TestContext::new().await;
2292        let task_mgr = TaskManager::new(ctx.pool());
2293
2294        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2295
2296        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2297        let summary = result.events_summary.unwrap();
2298
2299        assert_eq!(summary.total_count, 0);
2300        assert_eq!(summary.recent_events.len(), 0);
2301    }
2302
2303    #[tokio::test]
2304    async fn test_pick_next_tasks_zero_capacity() {
2305        let ctx = TestContext::new().await;
2306        let task_mgr = TaskManager::new(ctx.pool());
2307
2308        task_mgr.add_task("Task 1", None, None).await.unwrap();
2309
2310        // capacity_limit = 0 means no capacity available
2311        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2312        assert_eq!(results.len(), 0);
2313    }
2314
2315    #[tokio::test]
2316    async fn test_pick_next_tasks_capacity_exceeds_available() {
2317        let ctx = TestContext::new().await;
2318        let task_mgr = TaskManager::new(ctx.pool());
2319
2320        task_mgr.add_task("Task 1", None, None).await.unwrap();
2321        task_mgr.add_task("Task 2", None, None).await.unwrap();
2322
2323        // Request 10 tasks but only 2 available, capacity = 100
2324        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2325        assert_eq!(results.len(), 2); // Only returns available tasks
2326    }
2327
2328    // ========== task_context tests ==========
2329
2330    #[tokio::test]
2331    async fn test_get_task_context_root_task_no_relations() {
2332        let ctx = TestContext::new().await;
2333        let task_mgr = TaskManager::new(ctx.pool());
2334
2335        // Create a single root task with no relations
2336        let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2337
2338        let context = task_mgr.get_task_context(task.id).await.unwrap();
2339
2340        // Verify task itself
2341        assert_eq!(context.task.id, task.id);
2342        assert_eq!(context.task.name, "Root task");
2343
2344        // No ancestors (root task)
2345        assert_eq!(context.ancestors.len(), 0);
2346
2347        // No siblings
2348        assert_eq!(context.siblings.len(), 0);
2349
2350        // No children
2351        assert_eq!(context.children.len(), 0);
2352    }
2353
2354    #[tokio::test]
2355    async fn test_get_task_context_with_siblings() {
2356        let ctx = TestContext::new().await;
2357        let task_mgr = TaskManager::new(ctx.pool());
2358
2359        // Create multiple root tasks (siblings)
2360        let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2361        let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2362        let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2363
2364        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2365
2366        // Verify task itself
2367        assert_eq!(context.task.id, task2.id);
2368
2369        // No ancestors (root task)
2370        assert_eq!(context.ancestors.len(), 0);
2371
2372        // Should have 2 siblings
2373        assert_eq!(context.siblings.len(), 2);
2374        let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2375        assert!(sibling_ids.contains(&task1.id));
2376        assert!(sibling_ids.contains(&task3.id));
2377        assert!(!sibling_ids.contains(&task2.id)); // Should not include itself
2378
2379        // No children
2380        assert_eq!(context.children.len(), 0);
2381    }
2382
2383    #[tokio::test]
2384    async fn test_get_task_context_with_parent() {
2385        let ctx = TestContext::new().await;
2386        let task_mgr = TaskManager::new(ctx.pool());
2387
2388        // Create parent-child relationship
2389        let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2390        let child = task_mgr
2391            .add_task("Child task", None, Some(parent.id))
2392            .await
2393            .unwrap();
2394
2395        let context = task_mgr.get_task_context(child.id).await.unwrap();
2396
2397        // Verify task itself
2398        assert_eq!(context.task.id, child.id);
2399        assert_eq!(context.task.parent_id, Some(parent.id));
2400
2401        // Should have 1 ancestor (the parent)
2402        assert_eq!(context.ancestors.len(), 1);
2403        assert_eq!(context.ancestors[0].id, parent.id);
2404        assert_eq!(context.ancestors[0].name, "Parent task");
2405
2406        // No siblings
2407        assert_eq!(context.siblings.len(), 0);
2408
2409        // No children
2410        assert_eq!(context.children.len(), 0);
2411    }
2412
2413    #[tokio::test]
2414    async fn test_get_task_context_with_children() {
2415        let ctx = TestContext::new().await;
2416        let task_mgr = TaskManager::new(ctx.pool());
2417
2418        // Create parent with multiple children
2419        let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2420        let child1 = task_mgr
2421            .add_task("Child 1", None, Some(parent.id))
2422            .await
2423            .unwrap();
2424        let child2 = task_mgr
2425            .add_task("Child 2", None, Some(parent.id))
2426            .await
2427            .unwrap();
2428        let child3 = task_mgr
2429            .add_task("Child 3", None, Some(parent.id))
2430            .await
2431            .unwrap();
2432
2433        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2434
2435        // Verify task itself
2436        assert_eq!(context.task.id, parent.id);
2437
2438        // No ancestors (root task)
2439        assert_eq!(context.ancestors.len(), 0);
2440
2441        // No siblings
2442        assert_eq!(context.siblings.len(), 0);
2443
2444        // Should have 3 children
2445        assert_eq!(context.children.len(), 3);
2446        let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2447        assert!(child_ids.contains(&child1.id));
2448        assert!(child_ids.contains(&child2.id));
2449        assert!(child_ids.contains(&child3.id));
2450    }
2451
2452    #[tokio::test]
2453    async fn test_get_task_context_multi_level_hierarchy() {
2454        let ctx = TestContext::new().await;
2455        let task_mgr = TaskManager::new(ctx.pool());
2456
2457        // Create 3-level hierarchy: grandparent -> parent -> child
2458        let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2459        let parent = task_mgr
2460            .add_task("Parent", None, Some(grandparent.id))
2461            .await
2462            .unwrap();
2463        let child = task_mgr
2464            .add_task("Child", None, Some(parent.id))
2465            .await
2466            .unwrap();
2467
2468        let context = task_mgr.get_task_context(child.id).await.unwrap();
2469
2470        // Verify task itself
2471        assert_eq!(context.task.id, child.id);
2472
2473        // Should have 2 ancestors (parent and grandparent, ordered from immediate to root)
2474        assert_eq!(context.ancestors.len(), 2);
2475        assert_eq!(context.ancestors[0].id, parent.id);
2476        assert_eq!(context.ancestors[0].name, "Parent");
2477        assert_eq!(context.ancestors[1].id, grandparent.id);
2478        assert_eq!(context.ancestors[1].name, "Grandparent");
2479
2480        // No siblings
2481        assert_eq!(context.siblings.len(), 0);
2482
2483        // No children
2484        assert_eq!(context.children.len(), 0);
2485    }
2486
2487    #[tokio::test]
2488    async fn test_get_task_context_complex_family_tree() {
2489        let ctx = TestContext::new().await;
2490        let task_mgr = TaskManager::new(ctx.pool());
2491
2492        // Create complex structure:
2493        // Root
2494        //  ├─ Child1
2495        //  │   ├─ Grandchild1
2496        //  │   └─ Grandchild2 (target)
2497        //  └─ Child2
2498
2499        let root = task_mgr.add_task("Root", None, None).await.unwrap();
2500        let child1 = task_mgr
2501            .add_task("Child1", None, Some(root.id))
2502            .await
2503            .unwrap();
2504        let child2 = task_mgr
2505            .add_task("Child2", None, Some(root.id))
2506            .await
2507            .unwrap();
2508        let grandchild1 = task_mgr
2509            .add_task("Grandchild1", None, Some(child1.id))
2510            .await
2511            .unwrap();
2512        let grandchild2 = task_mgr
2513            .add_task("Grandchild2", None, Some(child1.id))
2514            .await
2515            .unwrap();
2516
2517        // Get context for grandchild2
2518        let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2519
2520        // Verify task itself
2521        assert_eq!(context.task.id, grandchild2.id);
2522
2523        // Should have 2 ancestors: child1 and root
2524        assert_eq!(context.ancestors.len(), 2);
2525        assert_eq!(context.ancestors[0].id, child1.id);
2526        assert_eq!(context.ancestors[1].id, root.id);
2527
2528        // Should have 1 sibling: grandchild1
2529        assert_eq!(context.siblings.len(), 1);
2530        assert_eq!(context.siblings[0].id, grandchild1.id);
2531
2532        // No children
2533        assert_eq!(context.children.len(), 0);
2534
2535        // Now get context for child1 to verify it sees both grandchildren
2536        let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2537        assert_eq!(context_child1.ancestors.len(), 1);
2538        assert_eq!(context_child1.ancestors[0].id, root.id);
2539        assert_eq!(context_child1.siblings.len(), 1);
2540        assert_eq!(context_child1.siblings[0].id, child2.id);
2541        assert_eq!(context_child1.children.len(), 2);
2542    }
2543
2544    #[tokio::test]
2545    async fn test_get_task_context_respects_priority_ordering() {
2546        let ctx = TestContext::new().await;
2547        let task_mgr = TaskManager::new(ctx.pool());
2548
2549        // Create parent with children having different priorities
2550        let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2551
2552        // Add children with priorities (lower number = higher priority)
2553        let child_low = task_mgr
2554            .add_task("Low priority", None, Some(parent.id))
2555            .await
2556            .unwrap();
2557        let _ = task_mgr
2558            .update_task(child_low.id, None, None, None, None, None, Some(10))
2559            .await
2560            .unwrap();
2561
2562        let child_high = task_mgr
2563            .add_task("High priority", None, Some(parent.id))
2564            .await
2565            .unwrap();
2566        let _ = task_mgr
2567            .update_task(child_high.id, None, None, None, None, None, Some(1))
2568            .await
2569            .unwrap();
2570
2571        let child_medium = task_mgr
2572            .add_task("Medium priority", None, Some(parent.id))
2573            .await
2574            .unwrap();
2575        let _ = task_mgr
2576            .update_task(child_medium.id, None, None, None, None, None, Some(5))
2577            .await
2578            .unwrap();
2579
2580        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2581
2582        // Children should be ordered by priority (1, 5, 10)
2583        assert_eq!(context.children.len(), 3);
2584        assert_eq!(context.children[0].priority, Some(1));
2585        assert_eq!(context.children[1].priority, Some(5));
2586        assert_eq!(context.children[2].priority, Some(10));
2587    }
2588
2589    #[tokio::test]
2590    async fn test_get_task_context_nonexistent_task() {
2591        let ctx = TestContext::new().await;
2592        let task_mgr = TaskManager::new(ctx.pool());
2593
2594        let result = task_mgr.get_task_context(99999).await;
2595        assert!(result.is_err());
2596        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2597    }
2598
2599    #[tokio::test]
2600    async fn test_get_task_context_handles_null_priority() {
2601        let ctx = TestContext::new().await;
2602        let task_mgr = TaskManager::new(ctx.pool());
2603
2604        // Create siblings with mixed null and set priorities
2605        let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2606        let _ = task_mgr
2607            .update_task(task1.id, None, None, None, None, None, Some(1))
2608            .await
2609            .unwrap();
2610
2611        let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2612        // task2 has NULL priority
2613
2614        let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2615        let _ = task_mgr
2616            .update_task(task3.id, None, None, None, None, None, Some(5))
2617            .await
2618            .unwrap();
2619
2620        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2621
2622        // Should have 2 siblings, ordered by priority (non-null first, then null)
2623        assert_eq!(context.siblings.len(), 2);
2624        // Task with priority 1 should come first
2625        assert_eq!(context.siblings[0].id, task1.id);
2626        assert_eq!(context.siblings[0].priority, Some(1));
2627        // Task with priority 5 should come second
2628        assert_eq!(context.siblings[1].id, task3.id);
2629        assert_eq!(context.siblings[1].priority, Some(5));
2630    }
2631}