intent_engine/
tasks.rs

1use crate::db::models::{Event, EventsSummary, Task, TaskWithEvents};
2use crate::error::{IntentError, Result};
3use chrono::Utc;
4use sqlx::SqlitePool;
5
6pub struct TaskManager<'a> {
7    pool: &'a SqlitePool,
8}
9
10impl<'a> TaskManager<'a> {
11    pub fn new(pool: &'a SqlitePool) -> Self {
12        Self { pool }
13    }
14
15    /// Add a new task
16    pub async fn add_task(
17        &self,
18        name: &str,
19        spec: Option<&str>,
20        parent_id: Option<i64>,
21    ) -> Result<Task> {
22        // Check for circular dependency if parent_id is provided
23        if let Some(pid) = parent_id {
24            self.check_task_exists(pid).await?;
25        }
26
27        let now = Utc::now();
28
29        let result = sqlx::query(
30            r#"
31            INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
32            VALUES (?, ?, ?, 'todo', ?)
33            "#,
34        )
35        .bind(name)
36        .bind(spec)
37        .bind(parent_id)
38        .bind(now)
39        .execute(self.pool)
40        .await?;
41
42        let id = result.last_insert_rowid();
43        self.get_task(id).await
44    }
45
46    /// Get a task by ID
47    pub async fn get_task(&self, id: i64) -> Result<Task> {
48        let task = sqlx::query_as::<_, Task>(
49            r#"
50            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
51            FROM tasks
52            WHERE id = ?
53            "#,
54        )
55        .bind(id)
56        .fetch_optional(self.pool)
57        .await?
58        .ok_or(IntentError::TaskNotFound(id))?;
59
60        Ok(task)
61    }
62
63    /// Get a task with events summary
64    pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
65        let task = self.get_task(id).await?;
66        let events_summary = self.get_events_summary(id).await?;
67
68        Ok(TaskWithEvents {
69            task,
70            events_summary: Some(events_summary),
71        })
72    }
73
74    /// Get events summary for a task
75    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
76        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
77            .bind(task_id)
78            .fetch_one(self.pool)
79            .await?;
80
81        let recent_events = sqlx::query_as::<_, Event>(
82            r#"
83            SELECT id, task_id, timestamp, log_type, discussion_data
84            FROM events
85            WHERE task_id = ?
86            ORDER BY timestamp DESC
87            LIMIT 10
88            "#,
89        )
90        .bind(task_id)
91        .fetch_all(self.pool)
92        .await?;
93
94        Ok(EventsSummary {
95            total_count,
96            recent_events,
97        })
98    }
99
100    /// Update a task
101    #[allow(clippy::too_many_arguments)]
102    pub async fn update_task(
103        &self,
104        id: i64,
105        name: Option<&str>,
106        spec: Option<&str>,
107        parent_id: Option<Option<i64>>,
108        status: Option<&str>,
109        complexity: Option<i32>,
110        priority: Option<i32>,
111    ) -> Result<Task> {
112        // Check task exists
113        let task = self.get_task(id).await?;
114
115        // Validate status if provided
116        if let Some(s) = status {
117            if !["todo", "doing", "done"].contains(&s) {
118                return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
119            }
120        }
121
122        // Check for circular dependency if parent_id is being changed
123        if let Some(Some(pid)) = parent_id {
124            if pid == id {
125                return Err(IntentError::CircularDependency);
126            }
127            self.check_task_exists(pid).await?;
128            self.check_circular_dependency(id, pid).await?;
129        }
130
131        // Build dynamic update query
132        let mut query = String::from("UPDATE tasks SET ");
133        let mut updates = Vec::new();
134
135        if let Some(n) = name {
136            updates.push(format!("name = '{}'", n.replace('\'', "''")));
137        }
138
139        if let Some(s) = spec {
140            updates.push(format!("spec = '{}'", s.replace('\'', "''")));
141        }
142
143        if let Some(pid) = parent_id {
144            match pid {
145                Some(p) => updates.push(format!("parent_id = {}", p)),
146                None => updates.push("parent_id = NULL".to_string()),
147            }
148        }
149
150        if let Some(c) = complexity {
151            updates.push(format!("complexity = {}", c));
152        }
153
154        if let Some(p) = priority {
155            updates.push(format!("priority = {}", p));
156        }
157
158        if let Some(s) = status {
159            updates.push(format!("status = '{}'", s));
160
161            // Update timestamp fields based on status
162            let now = Utc::now();
163            match s {
164                "todo" if task.first_todo_at.is_none() => {
165                    updates.push(format!("first_todo_at = '{}'", now.to_rfc3339()));
166                }
167                "doing" if task.first_doing_at.is_none() => {
168                    updates.push(format!("first_doing_at = '{}'", now.to_rfc3339()));
169                }
170                "done" if task.first_done_at.is_none() => {
171                    updates.push(format!("first_done_at = '{}'", now.to_rfc3339()));
172                }
173                _ => {}
174            }
175        }
176
177        if updates.is_empty() {
178            return Ok(task);
179        }
180
181        query.push_str(&updates.join(", "));
182        query.push_str(&format!(" WHERE id = {}", id));
183
184        sqlx::query(&query).execute(self.pool).await?;
185
186        self.get_task(id).await
187    }
188
189    /// Delete a task
190    pub async fn delete_task(&self, id: i64) -> Result<()> {
191        self.check_task_exists(id).await?;
192
193        sqlx::query("DELETE FROM tasks WHERE id = ?")
194            .bind(id)
195            .execute(self.pool)
196            .await?;
197
198        Ok(())
199    }
200
201    /// Find tasks with optional filters
202    pub async fn find_tasks(
203        &self,
204        status: Option<&str>,
205        parent_id: Option<Option<i64>>,
206    ) -> Result<Vec<Task>> {
207        let mut query = String::from(
208            "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at FROM tasks WHERE 1=1"
209        );
210        let mut conditions = Vec::new();
211
212        if let Some(s) = status {
213            query.push_str(" AND status = ?");
214            conditions.push(s.to_string());
215        }
216
217        if let Some(pid) = parent_id {
218            if let Some(p) = pid {
219                query.push_str(" AND parent_id = ?");
220                conditions.push(p.to_string());
221            } else {
222                query.push_str(" AND parent_id IS NULL");
223            }
224        }
225
226        query.push_str(" ORDER BY id");
227
228        let mut q = sqlx::query_as::<_, Task>(&query);
229        for cond in conditions {
230            q = q.bind(cond);
231        }
232
233        let tasks = q.fetch_all(self.pool).await?;
234        Ok(tasks)
235    }
236
237    /// Start a task (atomic: update status + set current)
238    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
239        let mut tx = self.pool.begin().await?;
240
241        let now = Utc::now();
242
243        // Update task status to doing
244        sqlx::query(
245            r#"
246            UPDATE tasks
247            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
248            WHERE id = ?
249            "#,
250        )
251        .bind(now)
252        .bind(id)
253        .execute(&mut *tx)
254        .await?;
255
256        // Set as current task
257        sqlx::query(
258            r#"
259            INSERT OR REPLACE INTO workspace_state (key, value)
260            VALUES ('current_task_id', ?)
261            "#,
262        )
263        .bind(id.to_string())
264        .execute(&mut *tx)
265        .await?;
266
267        tx.commit().await?;
268
269        if with_events {
270            self.get_task_with_events(id).await
271        } else {
272            let task = self.get_task(id).await?;
273            Ok(TaskWithEvents {
274                task,
275                events_summary: None,
276            })
277        }
278    }
279
280    /// Complete a task (atomic: check children + update status + clear current if needed)
281    pub async fn done_task(&self, id: i64) -> Result<Task> {
282        let mut tx = self.pool.begin().await?;
283
284        // Check if all children are done
285        let uncompleted_children: i64 = sqlx::query_scalar(
286            "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
287        )
288        .bind(id)
289        .fetch_one(&mut *tx)
290        .await?;
291
292        if uncompleted_children > 0 {
293            return Err(IntentError::UncompletedChildren);
294        }
295
296        let now = Utc::now();
297
298        // Update task status to done
299        sqlx::query(
300            r#"
301            UPDATE tasks
302            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
303            WHERE id = ?
304            "#,
305        )
306        .bind(now)
307        .bind(id)
308        .execute(&mut *tx)
309        .await?;
310
311        // Check if this was the current task and clear it
312        let current_task_id: Option<String> =
313            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
314                .fetch_optional(&mut *tx)
315                .await?;
316
317        if let Some(current) = current_task_id {
318            if current == id.to_string() {
319                sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
320                    .execute(&mut *tx)
321                    .await?;
322            }
323        }
324
325        tx.commit().await?;
326
327        self.get_task(id).await
328    }
329
330    /// Check if a task exists
331    async fn check_task_exists(&self, id: i64) -> Result<()> {
332        let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
333            .bind(id)
334            .fetch_one(self.pool)
335            .await?;
336
337        if !exists {
338            return Err(IntentError::TaskNotFound(id));
339        }
340
341        Ok(())
342    }
343
344    /// Check for circular dependencies
345    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
346        let mut current_id = new_parent_id;
347
348        loop {
349            if current_id == task_id {
350                return Err(IntentError::CircularDependency);
351            }
352
353            let parent: Option<i64> =
354                sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
355                    .bind(current_id)
356                    .fetch_optional(self.pool)
357                    .await?;
358
359            match parent {
360                Some(pid) => current_id = pid,
361                None => break,
362            }
363        }
364
365        Ok(())
366    }
367
368    /// Switch to a specific task (atomic: update status to doing + set as current)
369    /// If the task is not in 'doing' status, it will be transitioned to 'doing'
370    pub async fn switch_to_task(&self, id: i64) -> Result<TaskWithEvents> {
371        // Verify task exists
372        self.check_task_exists(id).await?;
373
374        let mut tx = self.pool.begin().await?;
375        let now = Utc::now();
376
377        // Update task to doing status if not already
378        sqlx::query(
379            r#"
380            UPDATE tasks
381            SET status = 'doing',
382                first_doing_at = COALESCE(first_doing_at, ?)
383            WHERE id = ? AND status != 'doing'
384            "#,
385        )
386        .bind(now)
387        .bind(id)
388        .execute(&mut *tx)
389        .await?;
390
391        // Set as current task
392        sqlx::query(
393            r#"
394            INSERT OR REPLACE INTO workspace_state (key, value)
395            VALUES ('current_task_id', ?)
396            "#,
397        )
398        .bind(id.to_string())
399        .execute(&mut *tx)
400        .await?;
401
402        tx.commit().await?;
403
404        // Return task with events
405        self.get_task_with_events(id).await
406    }
407
408    /// Create a subtask under the current task and switch to it (atomic operation)
409    /// Returns error if there is no current task
410    pub async fn spawn_subtask(&self, name: &str, spec: Option<&str>) -> Result<Task> {
411        // Get current task
412        let current_task_id: Option<String> =
413            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
414                .fetch_optional(self.pool)
415                .await?;
416
417        let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
418            IntentError::InvalidInput("No current task to create subtask under".to_string()),
419        )?;
420
421        // Create the subtask
422        let subtask = self.add_task(name, spec, Some(parent_id)).await?;
423
424        // Switch to the new subtask (returns updated task with status "doing")
425        let task_with_events = self.switch_to_task(subtask.id).await?;
426
427        Ok(task_with_events.task)
428    }
429
430    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
431    /// Returns tasks that were successfully transitioned
432    ///
433    /// # Arguments
434    /// * `max_count` - Maximum number of tasks to pick
435    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
436    ///
437    /// # Logic
438    /// 1. Check current 'doing' task count
439    /// 2. Calculate available capacity
440    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
441    /// 4. Transition selected tasks to 'doing'
442    pub async fn pick_next_tasks(
443        &self,
444        max_count: usize,
445        capacity_limit: usize,
446    ) -> Result<Vec<Task>> {
447        let mut tx = self.pool.begin().await?;
448
449        // Get current doing count
450        let doing_count: i64 =
451            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
452                .fetch_one(&mut *tx)
453                .await?;
454
455        // Calculate available capacity
456        let available = capacity_limit.saturating_sub(doing_count as usize);
457        if available == 0 {
458            return Ok(vec![]);
459        }
460
461        let limit = std::cmp::min(max_count, available);
462
463        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
464        let todo_tasks = sqlx::query_as::<_, Task>(
465            r#"
466            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
467            FROM tasks
468            WHERE status = 'todo'
469            ORDER BY
470                COALESCE(priority, 0) DESC,
471                COALESCE(complexity, 5) ASC,
472                id ASC
473            LIMIT ?
474            "#,
475        )
476        .bind(limit as i64)
477        .fetch_all(&mut *tx)
478        .await?;
479
480        if todo_tasks.is_empty() {
481            return Ok(vec![]);
482        }
483
484        let now = Utc::now();
485
486        // Transition selected tasks to 'doing'
487        for task in &todo_tasks {
488            sqlx::query(
489                r#"
490                UPDATE tasks
491                SET status = 'doing',
492                    first_doing_at = COALESCE(first_doing_at, ?)
493                WHERE id = ?
494                "#,
495            )
496            .bind(now)
497            .bind(task.id)
498            .execute(&mut *tx)
499            .await?;
500        }
501
502        tx.commit().await?;
503
504        // Fetch and return updated tasks in the same order
505        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
506        let placeholders = vec!["?"; task_ids.len()].join(",");
507        let query = format!(
508            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
509             FROM tasks WHERE id IN ({})
510             ORDER BY
511                 COALESCE(priority, 0) DESC,
512                 COALESCE(complexity, 5) ASC,
513                 id ASC",
514            placeholders
515        );
516
517        let mut q = sqlx::query_as::<_, Task>(&query);
518        for id in task_ids {
519            q = q.bind(id);
520        }
521
522        let updated_tasks = q.fetch_all(self.pool).await?;
523        Ok(updated_tasks)
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::test_utils::test_helpers::TestContext;
531
532    #[tokio::test]
533    async fn test_add_task() {
534        let ctx = TestContext::new().await;
535        let manager = TaskManager::new(ctx.pool());
536
537        let task = manager.add_task("Test task", None, None).await.unwrap();
538
539        assert_eq!(task.name, "Test task");
540        assert_eq!(task.status, "todo");
541        assert!(task.first_todo_at.is_some());
542        assert!(task.first_doing_at.is_none());
543        assert!(task.first_done_at.is_none());
544    }
545
546    #[tokio::test]
547    async fn test_add_task_with_spec() {
548        let ctx = TestContext::new().await;
549        let manager = TaskManager::new(ctx.pool());
550
551        let spec = "This is a task specification";
552        let task = manager
553            .add_task("Test task", Some(spec), None)
554            .await
555            .unwrap();
556
557        assert_eq!(task.name, "Test task");
558        assert_eq!(task.spec.as_deref(), Some(spec));
559    }
560
561    #[tokio::test]
562    async fn test_add_task_with_parent() {
563        let ctx = TestContext::new().await;
564        let manager = TaskManager::new(ctx.pool());
565
566        let parent = manager.add_task("Parent task", None, None).await.unwrap();
567        let child = manager
568            .add_task("Child task", None, Some(parent.id))
569            .await
570            .unwrap();
571
572        assert_eq!(child.parent_id, Some(parent.id));
573    }
574
575    #[tokio::test]
576    async fn test_get_task() {
577        let ctx = TestContext::new().await;
578        let manager = TaskManager::new(ctx.pool());
579
580        let created = manager.add_task("Test task", None, None).await.unwrap();
581        let retrieved = manager.get_task(created.id).await.unwrap();
582
583        assert_eq!(created.id, retrieved.id);
584        assert_eq!(created.name, retrieved.name);
585    }
586
587    #[tokio::test]
588    async fn test_get_task_not_found() {
589        let ctx = TestContext::new().await;
590        let manager = TaskManager::new(ctx.pool());
591
592        let result = manager.get_task(999).await;
593        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
594    }
595
596    #[tokio::test]
597    async fn test_update_task_name() {
598        let ctx = TestContext::new().await;
599        let manager = TaskManager::new(ctx.pool());
600
601        let task = manager.add_task("Original name", None, None).await.unwrap();
602        let updated = manager
603            .update_task(task.id, Some("New name"), None, None, None, None, None)
604            .await
605            .unwrap();
606
607        assert_eq!(updated.name, "New name");
608    }
609
610    #[tokio::test]
611    async fn test_update_task_status() {
612        let ctx = TestContext::new().await;
613        let manager = TaskManager::new(ctx.pool());
614
615        let task = manager.add_task("Test task", None, None).await.unwrap();
616        let updated = manager
617            .update_task(task.id, None, None, None, Some("doing"), None, None)
618            .await
619            .unwrap();
620
621        assert_eq!(updated.status, "doing");
622        assert!(updated.first_doing_at.is_some());
623    }
624
625    #[tokio::test]
626    async fn test_delete_task() {
627        let ctx = TestContext::new().await;
628        let manager = TaskManager::new(ctx.pool());
629
630        let task = manager.add_task("Test task", None, None).await.unwrap();
631        manager.delete_task(task.id).await.unwrap();
632
633        let result = manager.get_task(task.id).await;
634        assert!(result.is_err());
635    }
636
637    #[tokio::test]
638    async fn test_find_tasks_by_status() {
639        let ctx = TestContext::new().await;
640        let manager = TaskManager::new(ctx.pool());
641
642        manager.add_task("Todo task", None, None).await.unwrap();
643        let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
644        manager
645            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
646            .await
647            .unwrap();
648
649        let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
650        let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
651
652        assert_eq!(todo_tasks.len(), 1);
653        assert_eq!(doing_tasks.len(), 1);
654        assert_eq!(doing_tasks[0].status, "doing");
655    }
656
657    #[tokio::test]
658    async fn test_find_tasks_by_parent() {
659        let ctx = TestContext::new().await;
660        let manager = TaskManager::new(ctx.pool());
661
662        let parent = manager.add_task("Parent", None, None).await.unwrap();
663        manager
664            .add_task("Child 1", None, Some(parent.id))
665            .await
666            .unwrap();
667        manager
668            .add_task("Child 2", None, Some(parent.id))
669            .await
670            .unwrap();
671
672        let children = manager
673            .find_tasks(None, Some(Some(parent.id)))
674            .await
675            .unwrap();
676
677        assert_eq!(children.len(), 2);
678    }
679
680    #[tokio::test]
681    async fn test_start_task() {
682        let ctx = TestContext::new().await;
683        let manager = TaskManager::new(ctx.pool());
684
685        let task = manager.add_task("Test task", None, None).await.unwrap();
686        let started = manager.start_task(task.id, false).await.unwrap();
687
688        assert_eq!(started.task.status, "doing");
689        assert!(started.task.first_doing_at.is_some());
690
691        // Verify it's set as current task
692        let current: Option<String> =
693            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
694                .fetch_optional(ctx.pool())
695                .await
696                .unwrap();
697
698        assert_eq!(current, Some(task.id.to_string()));
699    }
700
701    #[tokio::test]
702    async fn test_start_task_with_events() {
703        let ctx = TestContext::new().await;
704        let manager = TaskManager::new(ctx.pool());
705
706        let task = manager.add_task("Test task", None, None).await.unwrap();
707
708        // Add an event
709        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
710            .bind(task.id)
711            .bind("test")
712            .bind("test event")
713            .execute(ctx.pool())
714            .await
715            .unwrap();
716
717        let started = manager.start_task(task.id, true).await.unwrap();
718
719        assert!(started.events_summary.is_some());
720        let summary = started.events_summary.unwrap();
721        assert_eq!(summary.total_count, 1);
722    }
723
724    #[tokio::test]
725    async fn test_done_task() {
726        let ctx = TestContext::new().await;
727        let manager = TaskManager::new(ctx.pool());
728
729        let task = manager.add_task("Test task", None, None).await.unwrap();
730        manager.start_task(task.id, false).await.unwrap();
731        let done = manager.done_task(task.id).await.unwrap();
732
733        assert_eq!(done.status, "done");
734        assert!(done.first_done_at.is_some());
735
736        // Verify current task is cleared
737        let current: Option<String> =
738            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
739                .fetch_optional(ctx.pool())
740                .await
741                .unwrap();
742
743        assert!(current.is_none());
744    }
745
746    #[tokio::test]
747    async fn test_done_task_with_uncompleted_children() {
748        let ctx = TestContext::new().await;
749        let manager = TaskManager::new(ctx.pool());
750
751        let parent = manager.add_task("Parent", None, None).await.unwrap();
752        manager
753            .add_task("Child", None, Some(parent.id))
754            .await
755            .unwrap();
756
757        let result = manager.done_task(parent.id).await;
758        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
759    }
760
761    #[tokio::test]
762    async fn test_done_task_with_completed_children() {
763        let ctx = TestContext::new().await;
764        let manager = TaskManager::new(ctx.pool());
765
766        let parent = manager.add_task("Parent", None, None).await.unwrap();
767        let child = manager
768            .add_task("Child", None, Some(parent.id))
769            .await
770            .unwrap();
771
772        // Complete child first
773        manager.done_task(child.id).await.unwrap();
774
775        // Now parent can be completed
776        let result = manager.done_task(parent.id).await;
777        assert!(result.is_ok());
778    }
779
780    #[tokio::test]
781    async fn test_circular_dependency() {
782        let ctx = TestContext::new().await;
783        let manager = TaskManager::new(ctx.pool());
784
785        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
786        let task2 = manager
787            .add_task("Task 2", None, Some(task1.id))
788            .await
789            .unwrap();
790
791        // Try to make task1 a child of task2 (circular)
792        let result = manager
793            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
794            .await;
795
796        assert!(matches!(result, Err(IntentError::CircularDependency)));
797    }
798
799    #[tokio::test]
800    async fn test_invalid_parent_id() {
801        let ctx = TestContext::new().await;
802        let manager = TaskManager::new(ctx.pool());
803
804        let result = manager.add_task("Test", None, Some(999)).await;
805        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
806    }
807
808    #[tokio::test]
809    async fn test_update_task_complexity_and_priority() {
810        let ctx = TestContext::new().await;
811        let manager = TaskManager::new(ctx.pool());
812
813        let task = manager.add_task("Test task", None, None).await.unwrap();
814        let updated = manager
815            .update_task(task.id, None, None, None, None, Some(8), Some(10))
816            .await
817            .unwrap();
818
819        assert_eq!(updated.complexity, Some(8));
820        assert_eq!(updated.priority, Some(10));
821    }
822
823    #[tokio::test]
824    async fn test_switch_to_task() {
825        let ctx = TestContext::new().await;
826        let manager = TaskManager::new(ctx.pool());
827
828        // Create a task
829        let task = manager.add_task("Test task", None, None).await.unwrap();
830        assert_eq!(task.status, "todo");
831
832        // Switch to it
833        let switched = manager.switch_to_task(task.id).await.unwrap();
834        assert_eq!(switched.task.status, "doing");
835        assert!(switched.task.first_doing_at.is_some());
836
837        // Verify it's set as current task
838        let current: Option<String> =
839            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
840                .fetch_optional(ctx.pool())
841                .await
842                .unwrap();
843
844        assert_eq!(current, Some(task.id.to_string()));
845    }
846
847    #[tokio::test]
848    async fn test_switch_to_task_already_doing() {
849        let ctx = TestContext::new().await;
850        let manager = TaskManager::new(ctx.pool());
851
852        // Create and start a task
853        let task = manager.add_task("Test task", None, None).await.unwrap();
854        manager.start_task(task.id, false).await.unwrap();
855
856        // Switch to it again (should be idempotent)
857        let switched = manager.switch_to_task(task.id).await.unwrap();
858        assert_eq!(switched.task.status, "doing");
859    }
860
861    #[tokio::test]
862    async fn test_spawn_subtask() {
863        let ctx = TestContext::new().await;
864        let manager = TaskManager::new(ctx.pool());
865
866        // Create and start a parent task
867        let parent = manager.add_task("Parent task", None, None).await.unwrap();
868        manager.start_task(parent.id, false).await.unwrap();
869
870        // Spawn a subtask
871        let subtask = manager
872            .spawn_subtask("Child task", Some("Details"))
873            .await
874            .unwrap();
875
876        assert_eq!(subtask.parent_id, Some(parent.id));
877        assert_eq!(subtask.name, "Child task");
878        assert_eq!(subtask.spec.as_deref(), Some("Details"));
879
880        // Verify subtask is now the current task
881        let current: Option<String> =
882            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
883                .fetch_optional(ctx.pool())
884                .await
885                .unwrap();
886
887        assert_eq!(current, Some(subtask.id.to_string()));
888
889        // Verify subtask is in doing status
890        let retrieved = manager.get_task(subtask.id).await.unwrap();
891        assert_eq!(retrieved.status, "doing");
892    }
893
894    #[tokio::test]
895    async fn test_spawn_subtask_no_current_task() {
896        let ctx = TestContext::new().await;
897        let manager = TaskManager::new(ctx.pool());
898
899        // Try to spawn subtask without a current task
900        let result = manager.spawn_subtask("Child", None).await;
901        assert!(result.is_err());
902    }
903
904    #[tokio::test]
905    async fn test_pick_next_tasks_basic() {
906        let ctx = TestContext::new().await;
907        let manager = TaskManager::new(ctx.pool());
908
909        // Create 10 todo tasks
910        for i in 1..=10 {
911            manager
912                .add_task(&format!("Task {}", i), None, None)
913                .await
914                .unwrap();
915        }
916
917        // Pick 5 tasks with capacity limit of 5
918        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
919
920        assert_eq!(picked.len(), 5);
921        for task in &picked {
922            assert_eq!(task.status, "doing");
923            assert!(task.first_doing_at.is_some());
924        }
925
926        // Verify total doing count
927        let doing_count: i64 =
928            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
929                .fetch_one(ctx.pool())
930                .await
931                .unwrap();
932
933        assert_eq!(doing_count, 5);
934    }
935
936    #[tokio::test]
937    async fn test_pick_next_tasks_with_existing_doing() {
938        let ctx = TestContext::new().await;
939        let manager = TaskManager::new(ctx.pool());
940
941        // Create 10 todo tasks
942        for i in 1..=10 {
943            manager
944                .add_task(&format!("Task {}", i), None, None)
945                .await
946                .unwrap();
947        }
948
949        // Start 2 tasks
950        let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
951        manager.start_task(tasks[0].id, false).await.unwrap();
952        manager.start_task(tasks[1].id, false).await.unwrap();
953
954        // Pick more tasks with capacity limit of 5
955        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
956
957        // Should only pick 3 more (5 - 2 = 3)
958        assert_eq!(picked.len(), 3);
959
960        // Verify total doing count
961        let doing_count: i64 =
962            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
963                .fetch_one(ctx.pool())
964                .await
965                .unwrap();
966
967        assert_eq!(doing_count, 5);
968    }
969
970    #[tokio::test]
971    async fn test_pick_next_tasks_at_capacity() {
972        let ctx = TestContext::new().await;
973        let manager = TaskManager::new(ctx.pool());
974
975        // Create 10 tasks
976        for i in 1..=10 {
977            manager
978                .add_task(&format!("Task {}", i), None, None)
979                .await
980                .unwrap();
981        }
982
983        // Fill capacity
984        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
985        assert_eq!(first_batch.len(), 5);
986
987        // Try to pick more (should return empty)
988        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
989        assert_eq!(second_batch.len(), 0);
990    }
991
992    #[tokio::test]
993    async fn test_pick_next_tasks_priority_ordering() {
994        let ctx = TestContext::new().await;
995        let manager = TaskManager::new(ctx.pool());
996
997        // Create tasks with different priorities
998        let low = manager.add_task("Low priority", None, None).await.unwrap();
999        manager
1000            .update_task(low.id, None, None, None, None, None, Some(1))
1001            .await
1002            .unwrap();
1003
1004        let high = manager.add_task("High priority", None, None).await.unwrap();
1005        manager
1006            .update_task(high.id, None, None, None, None, None, Some(10))
1007            .await
1008            .unwrap();
1009
1010        let medium = manager
1011            .add_task("Medium priority", None, None)
1012            .await
1013            .unwrap();
1014        manager
1015            .update_task(medium.id, None, None, None, None, None, Some(5))
1016            .await
1017            .unwrap();
1018
1019        // Pick tasks
1020        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1021
1022        // Should be ordered by priority DESC
1023        assert_eq!(picked.len(), 3);
1024        assert_eq!(picked[0].priority, Some(10)); // high
1025        assert_eq!(picked[1].priority, Some(5)); // medium
1026        assert_eq!(picked[2].priority, Some(1)); // low
1027    }
1028
1029    #[tokio::test]
1030    async fn test_pick_next_tasks_complexity_ordering() {
1031        let ctx = TestContext::new().await;
1032        let manager = TaskManager::new(ctx.pool());
1033
1034        // Create tasks with different complexities (same priority)
1035        let complex = manager.add_task("Complex", None, None).await.unwrap();
1036        manager
1037            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1038            .await
1039            .unwrap();
1040
1041        let simple = manager.add_task("Simple", None, None).await.unwrap();
1042        manager
1043            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1044            .await
1045            .unwrap();
1046
1047        let medium = manager.add_task("Medium", None, None).await.unwrap();
1048        manager
1049            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1050            .await
1051            .unwrap();
1052
1053        // Pick tasks
1054        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1055
1056        // Should be ordered by complexity ASC (simple first)
1057        assert_eq!(picked.len(), 3);
1058        assert_eq!(picked[0].complexity, Some(1)); // simple
1059        assert_eq!(picked[1].complexity, Some(5)); // medium
1060        assert_eq!(picked[2].complexity, Some(9)); // complex
1061    }
1062}