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 pub async fn add_task(
17 &self,
18 name: &str,
19 spec: Option<&str>,
20 parent_id: Option<i64>,
21 ) -> Result<Task> {
22 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 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 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 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 #[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 let task = self.get_task(id).await?;
114
115 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 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 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 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 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 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 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 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 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 pub async fn done_task(&self, id: i64) -> Result<Task> {
282 let mut tx = self.pool.begin().await?;
283
284 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 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 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 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 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 pub async fn switch_to_task(&self, id: i64) -> Result<TaskWithEvents> {
371 self.check_task_exists(id).await?;
373
374 let mut tx = self.pool.begin().await?;
375 let now = Utc::now();
376
377 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 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 self.get_task_with_events(id).await
406 }
407
408 pub async fn spawn_subtask(&self, name: &str, spec: Option<&str>) -> Result<Task> {
411 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 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
423
424 let task_with_events = self.switch_to_task(subtask.id).await?;
426
427 Ok(task_with_events.task)
428 }
429
430 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 let doing_count: i64 =
451 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
452 .fetch_one(&mut *tx)
453 .await?;
454
455 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 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 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 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 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 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 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 manager.done_task(child.id).await.unwrap();
774
775 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 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 let task = manager.add_task("Test task", None, None).await.unwrap();
830 assert_eq!(task.status, "todo");
831
832 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 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 let task = manager.add_task("Test task", None, None).await.unwrap();
854 manager.start_task(task.id, false).await.unwrap();
855
856 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 let parent = manager.add_task("Parent task", None, None).await.unwrap();
868 manager.start_task(parent.id, false).await.unwrap();
869
870 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 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 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 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 for i in 1..=10 {
911 manager
912 .add_task(&format!("Task {}", i), None, None)
913 .await
914 .unwrap();
915 }
916
917 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 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 for i in 1..=10 {
943 manager
944 .add_task(&format!("Task {}", i), None, None)
945 .await
946 .unwrap();
947 }
948
949 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 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
956
957 assert_eq!(picked.len(), 3);
959
960 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 for i in 1..=10 {
977 manager
978 .add_task(&format!("Task {}", i), None, None)
979 .await
980 .unwrap();
981 }
982
983 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
985 assert_eq!(first_batch.len(), 5);
986
987 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 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 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1021
1022 assert_eq!(picked.len(), 3);
1024 assert_eq!(picked[0].priority, Some(10)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(1)); }
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 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 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1055
1056 assert_eq!(picked.len(), 3);
1058 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1062}