1use crate::db::models::{
2 DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, PickNextResponse, Task,
3 TaskSearchResult, TaskWithEvents, WorkspaceStatus,
4};
5use crate::error::{IntentError, Result};
6use chrono::Utc;
7use sqlx::{Row, SqlitePool};
8
9pub struct TaskManager<'a> {
10 pool: &'a SqlitePool,
11}
12
13impl<'a> TaskManager<'a> {
14 pub fn new(pool: &'a SqlitePool) -> Self {
15 Self { pool }
16 }
17
18 pub async fn add_task(
20 &self,
21 name: &str,
22 spec: Option<&str>,
23 parent_id: Option<i64>,
24 ) -> Result<Task> {
25 if let Some(pid) = parent_id {
27 self.check_task_exists(pid).await?;
28 }
29
30 let now = Utc::now();
31
32 let result = sqlx::query(
33 r#"
34 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
35 VALUES (?, ?, ?, 'todo', ?)
36 "#,
37 )
38 .bind(name)
39 .bind(spec)
40 .bind(parent_id)
41 .bind(now)
42 .execute(self.pool)
43 .await?;
44
45 let id = result.last_insert_rowid();
46 self.get_task(id).await
47 }
48
49 pub async fn get_task(&self, id: i64) -> Result<Task> {
51 let task = sqlx::query_as::<_, Task>(
52 r#"
53 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
54 FROM tasks
55 WHERE id = ?
56 "#,
57 )
58 .bind(id)
59 .fetch_optional(self.pool)
60 .await?
61 .ok_or(IntentError::TaskNotFound(id))?;
62
63 Ok(task)
64 }
65
66 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
68 let task = self.get_task(id).await?;
69 let events_summary = self.get_events_summary(id).await?;
70
71 Ok(TaskWithEvents {
72 task,
73 events_summary: Some(events_summary),
74 })
75 }
76
77 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
79 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
80 .bind(task_id)
81 .fetch_one(self.pool)
82 .await?;
83
84 let recent_events = sqlx::query_as::<_, Event>(
85 r#"
86 SELECT id, task_id, timestamp, log_type, discussion_data
87 FROM events
88 WHERE task_id = ?
89 ORDER BY timestamp DESC
90 LIMIT 10
91 "#,
92 )
93 .bind(task_id)
94 .fetch_all(self.pool)
95 .await?;
96
97 Ok(EventsSummary {
98 total_count,
99 recent_events,
100 })
101 }
102
103 #[allow(clippy::too_many_arguments)]
105 pub async fn update_task(
106 &self,
107 id: i64,
108 name: Option<&str>,
109 spec: Option<&str>,
110 parent_id: Option<Option<i64>>,
111 status: Option<&str>,
112 complexity: Option<i32>,
113 priority: Option<i32>,
114 ) -> Result<Task> {
115 let task = self.get_task(id).await?;
117
118 if let Some(s) = status {
120 if !["todo", "doing", "done"].contains(&s) {
121 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
122 }
123 }
124
125 if let Some(Some(pid)) = parent_id {
127 if pid == id {
128 return Err(IntentError::CircularDependency);
129 }
130 self.check_task_exists(pid).await?;
131 self.check_circular_dependency(id, pid).await?;
132 }
133
134 let mut query = String::from("UPDATE tasks SET ");
136 let mut updates = Vec::new();
137
138 if let Some(n) = name {
139 updates.push(format!("name = '{}'", n.replace('\'', "''")));
140 }
141
142 if let Some(s) = spec {
143 updates.push(format!("spec = '{}'", s.replace('\'', "''")));
144 }
145
146 if let Some(pid) = parent_id {
147 match pid {
148 Some(p) => updates.push(format!("parent_id = {}", p)),
149 None => updates.push("parent_id = NULL".to_string()),
150 }
151 }
152
153 if let Some(c) = complexity {
154 updates.push(format!("complexity = {}", c));
155 }
156
157 if let Some(p) = priority {
158 updates.push(format!("priority = {}", p));
159 }
160
161 if let Some(s) = status {
162 updates.push(format!("status = '{}'", s));
163
164 let now = Utc::now();
166 match s {
167 "todo" if task.first_todo_at.is_none() => {
168 updates.push(format!("first_todo_at = '{}'", now.to_rfc3339()));
169 }
170 "doing" if task.first_doing_at.is_none() => {
171 updates.push(format!("first_doing_at = '{}'", now.to_rfc3339()));
172 }
173 "done" if task.first_done_at.is_none() => {
174 updates.push(format!("first_done_at = '{}'", now.to_rfc3339()));
175 }
176 _ => {}
177 }
178 }
179
180 if updates.is_empty() {
181 return Ok(task);
182 }
183
184 query.push_str(&updates.join(", "));
185 query.push_str(&format!(" WHERE id = {}", id));
186
187 sqlx::query(&query).execute(self.pool).await?;
188
189 self.get_task(id).await
190 }
191
192 pub async fn delete_task(&self, id: i64) -> Result<()> {
194 self.check_task_exists(id).await?;
195
196 sqlx::query("DELETE FROM tasks WHERE id = ?")
197 .bind(id)
198 .execute(self.pool)
199 .await?;
200
201 Ok(())
202 }
203
204 pub async fn find_tasks(
206 &self,
207 status: Option<&str>,
208 parent_id: Option<Option<i64>>,
209 ) -> Result<Vec<Task>> {
210 let mut query = String::from(
211 "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"
212 );
213 let mut conditions = Vec::new();
214
215 if let Some(s) = status {
216 query.push_str(" AND status = ?");
217 conditions.push(s.to_string());
218 }
219
220 if let Some(pid) = parent_id {
221 if let Some(p) = pid {
222 query.push_str(" AND parent_id = ?");
223 conditions.push(p.to_string());
224 } else {
225 query.push_str(" AND parent_id IS NULL");
226 }
227 }
228
229 query.push_str(" ORDER BY id");
230
231 let mut q = sqlx::query_as::<_, Task>(&query);
232 for cond in conditions {
233 q = q.bind(cond);
234 }
235
236 let tasks = q.fetch_all(self.pool).await?;
237 Ok(tasks)
238 }
239
240 pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
243 let escaped_query = self.escape_fts_query(query);
245
246 let results = sqlx::query(
250 r#"
251 SELECT
252 t.id,
253 t.parent_id,
254 t.name,
255 t.spec,
256 t.status,
257 t.complexity,
258 t.priority,
259 t.first_todo_at,
260 t.first_doing_at,
261 t.first_done_at,
262 COALESCE(
263 snippet(tasks_fts, 1, '**', '**', '...', 15),
264 snippet(tasks_fts, 0, '**', '**', '...', 15)
265 ) as match_snippet
266 FROM tasks_fts
267 INNER JOIN tasks t ON tasks_fts.rowid = t.id
268 WHERE tasks_fts MATCH ?
269 ORDER BY rank
270 "#,
271 )
272 .bind(&escaped_query)
273 .fetch_all(self.pool)
274 .await?;
275
276 let mut search_results = Vec::new();
277 for row in results {
278 let task = Task {
279 id: row.get("id"),
280 parent_id: row.get("parent_id"),
281 name: row.get("name"),
282 spec: row.get("spec"),
283 status: row.get("status"),
284 complexity: row.get("complexity"),
285 priority: row.get("priority"),
286 first_todo_at: row.get("first_todo_at"),
287 first_doing_at: row.get("first_doing_at"),
288 first_done_at: row.get("first_done_at"),
289 };
290 let match_snippet: String = row.get("match_snippet");
291
292 search_results.push(TaskSearchResult {
293 task,
294 match_snippet,
295 });
296 }
297
298 Ok(search_results)
299 }
300
301 fn escape_fts_query(&self, query: &str) -> String {
303 query.replace('"', "\"\"")
307 }
308
309 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
311 let mut tx = self.pool.begin().await?;
312
313 let now = Utc::now();
314
315 sqlx::query(
317 r#"
318 UPDATE tasks
319 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
320 WHERE id = ?
321 "#,
322 )
323 .bind(now)
324 .bind(id)
325 .execute(&mut *tx)
326 .await?;
327
328 sqlx::query(
330 r#"
331 INSERT OR REPLACE INTO workspace_state (key, value)
332 VALUES ('current_task_id', ?)
333 "#,
334 )
335 .bind(id.to_string())
336 .execute(&mut *tx)
337 .await?;
338
339 tx.commit().await?;
340
341 if with_events {
342 self.get_task_with_events(id).await
343 } else {
344 let task = self.get_task(id).await?;
345 Ok(TaskWithEvents {
346 task,
347 events_summary: None,
348 })
349 }
350 }
351
352 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
356 let mut tx = self.pool.begin().await?;
357
358 let current_task_id: Option<String> =
360 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
361 .fetch_optional(&mut *tx)
362 .await?;
363
364 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
365 IntentError::InvalidInput(
366 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
367 ),
368 )?;
369
370 let task_info: (String, Option<i64>) =
372 sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
373 .bind(id)
374 .fetch_one(&mut *tx)
375 .await?;
376 let (task_name, parent_id) = task_info;
377
378 let uncompleted_children: i64 = sqlx::query_scalar(
380 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
381 )
382 .bind(id)
383 .fetch_one(&mut *tx)
384 .await?;
385
386 if uncompleted_children > 0 {
387 return Err(IntentError::UncompletedChildren);
388 }
389
390 let now = Utc::now();
391
392 sqlx::query(
394 r#"
395 UPDATE tasks
396 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
397 WHERE id = ?
398 "#,
399 )
400 .bind(now)
401 .bind(id)
402 .execute(&mut *tx)
403 .await?;
404
405 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
407 .execute(&mut *tx)
408 .await?;
409
410 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
412 let remaining_siblings: i64 = sqlx::query_scalar(
414 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
415 )
416 .bind(parent_task_id)
417 .bind(id)
418 .fetch_one(&mut *tx)
419 .await?;
420
421 if remaining_siblings == 0 {
422 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
424 .bind(parent_task_id)
425 .fetch_one(&mut *tx)
426 .await?;
427
428 NextStepSuggestion::ParentIsReady {
429 message: format!(
430 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
431 parent_task_id, parent_name
432 ),
433 parent_task_id,
434 parent_task_name: parent_name,
435 }
436 } else {
437 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
439 .bind(parent_task_id)
440 .fetch_one(&mut *tx)
441 .await?;
442
443 NextStepSuggestion::SiblingTasksRemain {
444 message: format!(
445 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
446 id, parent_task_id, parent_name
447 ),
448 parent_task_id,
449 parent_task_name: parent_name,
450 remaining_siblings_count: remaining_siblings,
451 }
452 }
453 } else {
454 let child_count: i64 =
456 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
457 .bind(id)
458 .fetch_one(&mut *tx)
459 .await?;
460
461 if child_count > 0 {
462 NextStepSuggestion::TopLevelTaskCompleted {
464 message: format!(
465 "Top-level task #{} '{}' has been completed. Well done!",
466 id, task_name
467 ),
468 completed_task_id: id,
469 completed_task_name: task_name.clone(),
470 }
471 } else {
472 let remaining_tasks: i64 = sqlx::query_scalar(
474 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
475 )
476 .bind(id)
477 .fetch_one(&mut *tx)
478 .await?;
479
480 if remaining_tasks == 0 {
481 NextStepSuggestion::WorkspaceIsClear {
482 message: format!(
483 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
484 id
485 ),
486 completed_task_id: id,
487 }
488 } else {
489 NextStepSuggestion::NoParentContext {
490 message: format!("Task #{} '{}' has been completed.", id, task_name),
491 completed_task_id: id,
492 completed_task_name: task_name.clone(),
493 }
494 }
495 }
496 };
497
498 tx.commit().await?;
499
500 let completed_task = self.get_task(id).await?;
501
502 Ok(DoneTaskResponse {
503 completed_task,
504 workspace_status: WorkspaceStatus {
505 current_task_id: None,
506 },
507 next_step_suggestion,
508 })
509 }
510
511 async fn check_task_exists(&self, id: i64) -> Result<()> {
513 let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
514 .bind(id)
515 .fetch_one(self.pool)
516 .await?;
517
518 if !exists {
519 return Err(IntentError::TaskNotFound(id));
520 }
521
522 Ok(())
523 }
524
525 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
527 let mut current_id = new_parent_id;
528
529 loop {
530 if current_id == task_id {
531 return Err(IntentError::CircularDependency);
532 }
533
534 let parent: Option<i64> =
535 sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
536 .bind(current_id)
537 .fetch_optional(self.pool)
538 .await?;
539
540 match parent {
541 Some(pid) => current_id = pid,
542 None => break,
543 }
544 }
545
546 Ok(())
547 }
548
549 pub async fn switch_to_task(&self, id: i64) -> Result<TaskWithEvents> {
552 self.check_task_exists(id).await?;
554
555 let mut tx = self.pool.begin().await?;
556 let now = Utc::now();
557
558 sqlx::query(
560 r#"
561 UPDATE tasks
562 SET status = 'doing',
563 first_doing_at = COALESCE(first_doing_at, ?)
564 WHERE id = ? AND status != 'doing'
565 "#,
566 )
567 .bind(now)
568 .bind(id)
569 .execute(&mut *tx)
570 .await?;
571
572 sqlx::query(
574 r#"
575 INSERT OR REPLACE INTO workspace_state (key, value)
576 VALUES ('current_task_id', ?)
577 "#,
578 )
579 .bind(id.to_string())
580 .execute(&mut *tx)
581 .await?;
582
583 tx.commit().await?;
584
585 self.get_task_with_events(id).await
587 }
588
589 pub async fn spawn_subtask(&self, name: &str, spec: Option<&str>) -> Result<Task> {
592 let current_task_id: Option<String> =
594 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
595 .fetch_optional(self.pool)
596 .await?;
597
598 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
599 IntentError::InvalidInput("No current task to create subtask under".to_string()),
600 )?;
601
602 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
604
605 let task_with_events = self.switch_to_task(subtask.id).await?;
607
608 Ok(task_with_events.task)
609 }
610
611 pub async fn pick_next_tasks(
624 &self,
625 max_count: usize,
626 capacity_limit: usize,
627 ) -> Result<Vec<Task>> {
628 let mut tx = self.pool.begin().await?;
629
630 let doing_count: i64 =
632 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
633 .fetch_one(&mut *tx)
634 .await?;
635
636 let available = capacity_limit.saturating_sub(doing_count as usize);
638 if available == 0 {
639 return Ok(vec![]);
640 }
641
642 let limit = std::cmp::min(max_count, available);
643
644 let todo_tasks = sqlx::query_as::<_, Task>(
646 r#"
647 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
648 FROM tasks
649 WHERE status = 'todo'
650 ORDER BY
651 COALESCE(priority, 0) DESC,
652 COALESCE(complexity, 5) ASC,
653 id ASC
654 LIMIT ?
655 "#,
656 )
657 .bind(limit as i64)
658 .fetch_all(&mut *tx)
659 .await?;
660
661 if todo_tasks.is_empty() {
662 return Ok(vec![]);
663 }
664
665 let now = Utc::now();
666
667 for task in &todo_tasks {
669 sqlx::query(
670 r#"
671 UPDATE tasks
672 SET status = 'doing',
673 first_doing_at = COALESCE(first_doing_at, ?)
674 WHERE id = ?
675 "#,
676 )
677 .bind(now)
678 .bind(task.id)
679 .execute(&mut *tx)
680 .await?;
681 }
682
683 tx.commit().await?;
684
685 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
687 let placeholders = vec!["?"; task_ids.len()].join(",");
688 let query = format!(
689 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
690 FROM tasks WHERE id IN ({})
691 ORDER BY
692 COALESCE(priority, 0) DESC,
693 COALESCE(complexity, 5) ASC,
694 id ASC",
695 placeholders
696 );
697
698 let mut q = sqlx::query_as::<_, Task>(&query);
699 for id in task_ids {
700 q = q.bind(id);
701 }
702
703 let updated_tasks = q.fetch_all(self.pool).await?;
704 Ok(updated_tasks)
705 }
706
707 pub async fn pick_next(&self) -> Result<PickNextResponse> {
716 let current_task_id: Option<String> =
718 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
719 .fetch_optional(self.pool)
720 .await?;
721
722 if let Some(current_id_str) = current_task_id {
723 if let Ok(current_id) = current_id_str.parse::<i64>() {
724 let subtasks = sqlx::query_as::<_, Task>(
726 r#"
727 SELECT id, parent_id, name, spec, status, complexity, priority,
728 first_todo_at, first_doing_at, first_done_at
729 FROM tasks
730 WHERE parent_id = ? AND status = 'todo'
731 ORDER BY COALESCE(priority, 999999) ASC, id ASC
732 LIMIT 1
733 "#,
734 )
735 .bind(current_id)
736 .fetch_optional(self.pool)
737 .await?;
738
739 if let Some(task) = subtasks {
740 return Ok(PickNextResponse::focused_subtask(task));
741 }
742 }
743 }
744
745 let top_level_task = sqlx::query_as::<_, Task>(
747 r#"
748 SELECT id, parent_id, name, spec, status, complexity, priority,
749 first_todo_at, first_doing_at, first_done_at
750 FROM tasks
751 WHERE parent_id IS NULL AND status = 'todo'
752 ORDER BY COALESCE(priority, 999999) ASC, id ASC
753 LIMIT 1
754 "#,
755 )
756 .fetch_optional(self.pool)
757 .await?;
758
759 if let Some(task) = top_level_task {
760 return Ok(PickNextResponse::top_level_task(task));
761 }
762
763 let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
766 .fetch_one(self.pool)
767 .await?;
768
769 if total_tasks == 0 {
770 return Ok(PickNextResponse::no_tasks_in_project());
771 }
772
773 let todo_or_doing_count: i64 =
775 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
776 .fetch_one(self.pool)
777 .await?;
778
779 if todo_or_doing_count == 0 {
780 return Ok(PickNextResponse::all_tasks_completed());
781 }
782
783 Ok(PickNextResponse::no_available_todos())
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791 use crate::test_utils::test_helpers::TestContext;
792
793 #[tokio::test]
794 async fn test_add_task() {
795 let ctx = TestContext::new().await;
796 let manager = TaskManager::new(ctx.pool());
797
798 let task = manager.add_task("Test task", None, None).await.unwrap();
799
800 assert_eq!(task.name, "Test task");
801 assert_eq!(task.status, "todo");
802 assert!(task.first_todo_at.is_some());
803 assert!(task.first_doing_at.is_none());
804 assert!(task.first_done_at.is_none());
805 }
806
807 #[tokio::test]
808 async fn test_add_task_with_spec() {
809 let ctx = TestContext::new().await;
810 let manager = TaskManager::new(ctx.pool());
811
812 let spec = "This is a task specification";
813 let task = manager
814 .add_task("Test task", Some(spec), None)
815 .await
816 .unwrap();
817
818 assert_eq!(task.name, "Test task");
819 assert_eq!(task.spec.as_deref(), Some(spec));
820 }
821
822 #[tokio::test]
823 async fn test_add_task_with_parent() {
824 let ctx = TestContext::new().await;
825 let manager = TaskManager::new(ctx.pool());
826
827 let parent = manager.add_task("Parent task", None, None).await.unwrap();
828 let child = manager
829 .add_task("Child task", None, Some(parent.id))
830 .await
831 .unwrap();
832
833 assert_eq!(child.parent_id, Some(parent.id));
834 }
835
836 #[tokio::test]
837 async fn test_get_task() {
838 let ctx = TestContext::new().await;
839 let manager = TaskManager::new(ctx.pool());
840
841 let created = manager.add_task("Test task", None, None).await.unwrap();
842 let retrieved = manager.get_task(created.id).await.unwrap();
843
844 assert_eq!(created.id, retrieved.id);
845 assert_eq!(created.name, retrieved.name);
846 }
847
848 #[tokio::test]
849 async fn test_get_task_not_found() {
850 let ctx = TestContext::new().await;
851 let manager = TaskManager::new(ctx.pool());
852
853 let result = manager.get_task(999).await;
854 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
855 }
856
857 #[tokio::test]
858 async fn test_update_task_name() {
859 let ctx = TestContext::new().await;
860 let manager = TaskManager::new(ctx.pool());
861
862 let task = manager.add_task("Original name", None, None).await.unwrap();
863 let updated = manager
864 .update_task(task.id, Some("New name"), None, None, None, None, None)
865 .await
866 .unwrap();
867
868 assert_eq!(updated.name, "New name");
869 }
870
871 #[tokio::test]
872 async fn test_update_task_status() {
873 let ctx = TestContext::new().await;
874 let manager = TaskManager::new(ctx.pool());
875
876 let task = manager.add_task("Test task", None, None).await.unwrap();
877 let updated = manager
878 .update_task(task.id, None, None, None, Some("doing"), None, None)
879 .await
880 .unwrap();
881
882 assert_eq!(updated.status, "doing");
883 assert!(updated.first_doing_at.is_some());
884 }
885
886 #[tokio::test]
887 async fn test_delete_task() {
888 let ctx = TestContext::new().await;
889 let manager = TaskManager::new(ctx.pool());
890
891 let task = manager.add_task("Test task", None, None).await.unwrap();
892 manager.delete_task(task.id).await.unwrap();
893
894 let result = manager.get_task(task.id).await;
895 assert!(result.is_err());
896 }
897
898 #[tokio::test]
899 async fn test_find_tasks_by_status() {
900 let ctx = TestContext::new().await;
901 let manager = TaskManager::new(ctx.pool());
902
903 manager.add_task("Todo task", None, None).await.unwrap();
904 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
905 manager
906 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
907 .await
908 .unwrap();
909
910 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
911 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
912
913 assert_eq!(todo_tasks.len(), 1);
914 assert_eq!(doing_tasks.len(), 1);
915 assert_eq!(doing_tasks[0].status, "doing");
916 }
917
918 #[tokio::test]
919 async fn test_find_tasks_by_parent() {
920 let ctx = TestContext::new().await;
921 let manager = TaskManager::new(ctx.pool());
922
923 let parent = manager.add_task("Parent", None, None).await.unwrap();
924 manager
925 .add_task("Child 1", None, Some(parent.id))
926 .await
927 .unwrap();
928 manager
929 .add_task("Child 2", None, Some(parent.id))
930 .await
931 .unwrap();
932
933 let children = manager
934 .find_tasks(None, Some(Some(parent.id)))
935 .await
936 .unwrap();
937
938 assert_eq!(children.len(), 2);
939 }
940
941 #[tokio::test]
942 async fn test_start_task() {
943 let ctx = TestContext::new().await;
944 let manager = TaskManager::new(ctx.pool());
945
946 let task = manager.add_task("Test task", None, None).await.unwrap();
947 let started = manager.start_task(task.id, false).await.unwrap();
948
949 assert_eq!(started.task.status, "doing");
950 assert!(started.task.first_doing_at.is_some());
951
952 let current: Option<String> =
954 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
955 .fetch_optional(ctx.pool())
956 .await
957 .unwrap();
958
959 assert_eq!(current, Some(task.id.to_string()));
960 }
961
962 #[tokio::test]
963 async fn test_start_task_with_events() {
964 let ctx = TestContext::new().await;
965 let manager = TaskManager::new(ctx.pool());
966
967 let task = manager.add_task("Test task", None, None).await.unwrap();
968
969 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
971 .bind(task.id)
972 .bind("test")
973 .bind("test event")
974 .execute(ctx.pool())
975 .await
976 .unwrap();
977
978 let started = manager.start_task(task.id, true).await.unwrap();
979
980 assert!(started.events_summary.is_some());
981 let summary = started.events_summary.unwrap();
982 assert_eq!(summary.total_count, 1);
983 }
984
985 #[tokio::test]
986 async fn test_done_task() {
987 let ctx = TestContext::new().await;
988 let manager = TaskManager::new(ctx.pool());
989
990 let task = manager.add_task("Test task", None, None).await.unwrap();
991 manager.start_task(task.id, false).await.unwrap();
992 let response = manager.done_task().await.unwrap();
993
994 assert_eq!(response.completed_task.status, "done");
995 assert!(response.completed_task.first_done_at.is_some());
996 assert_eq!(response.workspace_status.current_task_id, None);
997
998 match response.next_step_suggestion {
1000 NextStepSuggestion::WorkspaceIsClear { .. } => {}
1001 _ => panic!("Expected WorkspaceIsClear suggestion"),
1002 }
1003
1004 let current: Option<String> =
1006 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1007 .fetch_optional(ctx.pool())
1008 .await
1009 .unwrap();
1010
1011 assert!(current.is_none());
1012 }
1013
1014 #[tokio::test]
1015 async fn test_done_task_with_uncompleted_children() {
1016 let ctx = TestContext::new().await;
1017 let manager = TaskManager::new(ctx.pool());
1018
1019 let parent = manager.add_task("Parent", None, None).await.unwrap();
1020 manager
1021 .add_task("Child", None, Some(parent.id))
1022 .await
1023 .unwrap();
1024
1025 manager.start_task(parent.id, false).await.unwrap();
1027
1028 let result = manager.done_task().await;
1029 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1030 }
1031
1032 #[tokio::test]
1033 async fn test_done_task_with_completed_children() {
1034 let ctx = TestContext::new().await;
1035 let manager = TaskManager::new(ctx.pool());
1036
1037 let parent = manager.add_task("Parent", None, None).await.unwrap();
1038 let child = manager
1039 .add_task("Child", None, Some(parent.id))
1040 .await
1041 .unwrap();
1042
1043 manager.start_task(child.id, false).await.unwrap();
1045 let child_response = manager.done_task().await.unwrap();
1046
1047 match child_response.next_step_suggestion {
1049 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1050 assert_eq!(parent_task_id, parent.id);
1051 }
1052 _ => panic!("Expected ParentIsReady suggestion"),
1053 }
1054
1055 manager.start_task(parent.id, false).await.unwrap();
1057 let parent_response = manager.done_task().await.unwrap();
1058 assert_eq!(parent_response.completed_task.status, "done");
1059
1060 match parent_response.next_step_suggestion {
1062 NextStepSuggestion::TopLevelTaskCompleted { .. } => {}
1063 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1064 }
1065 }
1066
1067 #[tokio::test]
1068 async fn test_circular_dependency() {
1069 let ctx = TestContext::new().await;
1070 let manager = TaskManager::new(ctx.pool());
1071
1072 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1073 let task2 = manager
1074 .add_task("Task 2", None, Some(task1.id))
1075 .await
1076 .unwrap();
1077
1078 let result = manager
1080 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1081 .await;
1082
1083 assert!(matches!(result, Err(IntentError::CircularDependency)));
1084 }
1085
1086 #[tokio::test]
1087 async fn test_invalid_parent_id() {
1088 let ctx = TestContext::new().await;
1089 let manager = TaskManager::new(ctx.pool());
1090
1091 let result = manager.add_task("Test", None, Some(999)).await;
1092 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1093 }
1094
1095 #[tokio::test]
1096 async fn test_update_task_complexity_and_priority() {
1097 let ctx = TestContext::new().await;
1098 let manager = TaskManager::new(ctx.pool());
1099
1100 let task = manager.add_task("Test task", None, None).await.unwrap();
1101 let updated = manager
1102 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1103 .await
1104 .unwrap();
1105
1106 assert_eq!(updated.complexity, Some(8));
1107 assert_eq!(updated.priority, Some(10));
1108 }
1109
1110 #[tokio::test]
1111 async fn test_switch_to_task() {
1112 let ctx = TestContext::new().await;
1113 let manager = TaskManager::new(ctx.pool());
1114
1115 let task = manager.add_task("Test task", None, None).await.unwrap();
1117 assert_eq!(task.status, "todo");
1118
1119 let switched = manager.switch_to_task(task.id).await.unwrap();
1121 assert_eq!(switched.task.status, "doing");
1122 assert!(switched.task.first_doing_at.is_some());
1123
1124 let current: Option<String> =
1126 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1127 .fetch_optional(ctx.pool())
1128 .await
1129 .unwrap();
1130
1131 assert_eq!(current, Some(task.id.to_string()));
1132 }
1133
1134 #[tokio::test]
1135 async fn test_switch_to_task_already_doing() {
1136 let ctx = TestContext::new().await;
1137 let manager = TaskManager::new(ctx.pool());
1138
1139 let task = manager.add_task("Test task", None, None).await.unwrap();
1141 manager.start_task(task.id, false).await.unwrap();
1142
1143 let switched = manager.switch_to_task(task.id).await.unwrap();
1145 assert_eq!(switched.task.status, "doing");
1146 }
1147
1148 #[tokio::test]
1149 async fn test_spawn_subtask() {
1150 let ctx = TestContext::new().await;
1151 let manager = TaskManager::new(ctx.pool());
1152
1153 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1155 manager.start_task(parent.id, false).await.unwrap();
1156
1157 let subtask = manager
1159 .spawn_subtask("Child task", Some("Details"))
1160 .await
1161 .unwrap();
1162
1163 assert_eq!(subtask.parent_id, Some(parent.id));
1164 assert_eq!(subtask.name, "Child task");
1165 assert_eq!(subtask.spec.as_deref(), Some("Details"));
1166
1167 let current: Option<String> =
1169 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1170 .fetch_optional(ctx.pool())
1171 .await
1172 .unwrap();
1173
1174 assert_eq!(current, Some(subtask.id.to_string()));
1175
1176 let retrieved = manager.get_task(subtask.id).await.unwrap();
1178 assert_eq!(retrieved.status, "doing");
1179 }
1180
1181 #[tokio::test]
1182 async fn test_spawn_subtask_no_current_task() {
1183 let ctx = TestContext::new().await;
1184 let manager = TaskManager::new(ctx.pool());
1185
1186 let result = manager.spawn_subtask("Child", None).await;
1188 assert!(result.is_err());
1189 }
1190
1191 #[tokio::test]
1192 async fn test_pick_next_tasks_basic() {
1193 let ctx = TestContext::new().await;
1194 let manager = TaskManager::new(ctx.pool());
1195
1196 for i in 1..=10 {
1198 manager
1199 .add_task(&format!("Task {}", i), None, None)
1200 .await
1201 .unwrap();
1202 }
1203
1204 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1206
1207 assert_eq!(picked.len(), 5);
1208 for task in &picked {
1209 assert_eq!(task.status, "doing");
1210 assert!(task.first_doing_at.is_some());
1211 }
1212
1213 let doing_count: i64 =
1215 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1216 .fetch_one(ctx.pool())
1217 .await
1218 .unwrap();
1219
1220 assert_eq!(doing_count, 5);
1221 }
1222
1223 #[tokio::test]
1224 async fn test_pick_next_tasks_with_existing_doing() {
1225 let ctx = TestContext::new().await;
1226 let manager = TaskManager::new(ctx.pool());
1227
1228 for i in 1..=10 {
1230 manager
1231 .add_task(&format!("Task {}", i), None, None)
1232 .await
1233 .unwrap();
1234 }
1235
1236 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1238 manager.start_task(tasks[0].id, false).await.unwrap();
1239 manager.start_task(tasks[1].id, false).await.unwrap();
1240
1241 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1243
1244 assert_eq!(picked.len(), 3);
1246
1247 let doing_count: i64 =
1249 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1250 .fetch_one(ctx.pool())
1251 .await
1252 .unwrap();
1253
1254 assert_eq!(doing_count, 5);
1255 }
1256
1257 #[tokio::test]
1258 async fn test_pick_next_tasks_at_capacity() {
1259 let ctx = TestContext::new().await;
1260 let manager = TaskManager::new(ctx.pool());
1261
1262 for i in 1..=10 {
1264 manager
1265 .add_task(&format!("Task {}", i), None, None)
1266 .await
1267 .unwrap();
1268 }
1269
1270 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1272 assert_eq!(first_batch.len(), 5);
1273
1274 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1276 assert_eq!(second_batch.len(), 0);
1277 }
1278
1279 #[tokio::test]
1280 async fn test_pick_next_tasks_priority_ordering() {
1281 let ctx = TestContext::new().await;
1282 let manager = TaskManager::new(ctx.pool());
1283
1284 let low = manager.add_task("Low priority", None, None).await.unwrap();
1286 manager
1287 .update_task(low.id, None, None, None, None, None, Some(1))
1288 .await
1289 .unwrap();
1290
1291 let high = manager.add_task("High priority", None, None).await.unwrap();
1292 manager
1293 .update_task(high.id, None, None, None, None, None, Some(10))
1294 .await
1295 .unwrap();
1296
1297 let medium = manager
1298 .add_task("Medium priority", None, None)
1299 .await
1300 .unwrap();
1301 manager
1302 .update_task(medium.id, None, None, None, None, None, Some(5))
1303 .await
1304 .unwrap();
1305
1306 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1308
1309 assert_eq!(picked.len(), 3);
1311 assert_eq!(picked[0].priority, Some(10)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(1)); }
1315
1316 #[tokio::test]
1317 async fn test_pick_next_tasks_complexity_ordering() {
1318 let ctx = TestContext::new().await;
1319 let manager = TaskManager::new(ctx.pool());
1320
1321 let complex = manager.add_task("Complex", None, None).await.unwrap();
1323 manager
1324 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1325 .await
1326 .unwrap();
1327
1328 let simple = manager.add_task("Simple", None, None).await.unwrap();
1329 manager
1330 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1331 .await
1332 .unwrap();
1333
1334 let medium = manager.add_task("Medium", None, None).await.unwrap();
1335 manager
1336 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1337 .await
1338 .unwrap();
1339
1340 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1342
1343 assert_eq!(picked.len(), 3);
1345 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1349
1350 #[tokio::test]
1351 async fn test_done_task_sibling_tasks_remain() {
1352 let ctx = TestContext::new().await;
1353 let manager = TaskManager::new(ctx.pool());
1354
1355 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1357 let child1 = manager
1358 .add_task("Child 1", None, Some(parent.id))
1359 .await
1360 .unwrap();
1361 let child2 = manager
1362 .add_task("Child 2", None, Some(parent.id))
1363 .await
1364 .unwrap();
1365 let _child3 = manager
1366 .add_task("Child 3", None, Some(parent.id))
1367 .await
1368 .unwrap();
1369
1370 manager.start_task(child1.id, false).await.unwrap();
1372 let response = manager.done_task().await.unwrap();
1373
1374 match response.next_step_suggestion {
1376 NextStepSuggestion::SiblingTasksRemain {
1377 parent_task_id,
1378 remaining_siblings_count,
1379 ..
1380 } => {
1381 assert_eq!(parent_task_id, parent.id);
1382 assert_eq!(remaining_siblings_count, 2); }
1384 _ => panic!("Expected SiblingTasksRemain suggestion"),
1385 }
1386
1387 manager.start_task(child2.id, false).await.unwrap();
1389 let response2 = manager.done_task().await.unwrap();
1390
1391 match response2.next_step_suggestion {
1393 NextStepSuggestion::SiblingTasksRemain {
1394 remaining_siblings_count,
1395 ..
1396 } => {
1397 assert_eq!(remaining_siblings_count, 1); }
1399 _ => panic!("Expected SiblingTasksRemain suggestion"),
1400 }
1401 }
1402
1403 #[tokio::test]
1404 async fn test_done_task_top_level_with_children() {
1405 let ctx = TestContext::new().await;
1406 let manager = TaskManager::new(ctx.pool());
1407
1408 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1410 let child = manager
1411 .add_task("Sub Task", None, Some(parent.id))
1412 .await
1413 .unwrap();
1414
1415 manager.start_task(child.id, false).await.unwrap();
1417 manager.done_task().await.unwrap();
1418
1419 manager.start_task(parent.id, false).await.unwrap();
1421 let response = manager.done_task().await.unwrap();
1422
1423 match response.next_step_suggestion {
1425 NextStepSuggestion::TopLevelTaskCompleted {
1426 completed_task_id,
1427 completed_task_name,
1428 ..
1429 } => {
1430 assert_eq!(completed_task_id, parent.id);
1431 assert_eq!(completed_task_name, "Epic Task");
1432 }
1433 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1434 }
1435 }
1436
1437 #[tokio::test]
1438 async fn test_done_task_no_parent_context() {
1439 let ctx = TestContext::new().await;
1440 let manager = TaskManager::new(ctx.pool());
1441
1442 let task1 = manager
1444 .add_task("Standalone Task 1", None, None)
1445 .await
1446 .unwrap();
1447 let _task2 = manager
1448 .add_task("Standalone Task 2", None, None)
1449 .await
1450 .unwrap();
1451
1452 manager.start_task(task1.id, false).await.unwrap();
1454 let response = manager.done_task().await.unwrap();
1455
1456 match response.next_step_suggestion {
1458 NextStepSuggestion::NoParentContext {
1459 completed_task_id,
1460 completed_task_name,
1461 ..
1462 } => {
1463 assert_eq!(completed_task_id, task1.id);
1464 assert_eq!(completed_task_name, "Standalone Task 1");
1465 }
1466 _ => panic!("Expected NoParentContext suggestion"),
1467 }
1468 }
1469
1470 #[tokio::test]
1471 async fn test_search_tasks_by_name() {
1472 let ctx = TestContext::new().await;
1473 let manager = TaskManager::new(ctx.pool());
1474
1475 manager
1477 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1478 .await
1479 .unwrap();
1480 manager
1481 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1482 .await
1483 .unwrap();
1484 manager
1485 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1486 .await
1487 .unwrap();
1488
1489 let results = manager.search_tasks("authentication").await.unwrap();
1491
1492 assert_eq!(results.len(), 2);
1493 assert!(results[0]
1494 .task
1495 .name
1496 .to_lowercase()
1497 .contains("authentication"));
1498 assert!(results[1]
1499 .task
1500 .name
1501 .to_lowercase()
1502 .contains("authentication"));
1503
1504 assert!(!results[0].match_snippet.is_empty());
1506 }
1507
1508 #[tokio::test]
1509 async fn test_search_tasks_by_spec() {
1510 let ctx = TestContext::new().await;
1511 let manager = TaskManager::new(ctx.pool());
1512
1513 manager
1515 .add_task("Task 1", Some("Implement JWT authentication"), None)
1516 .await
1517 .unwrap();
1518 manager
1519 .add_task("Task 2", Some("Add user registration"), None)
1520 .await
1521 .unwrap();
1522 manager
1523 .add_task("Task 3", Some("JWT token refresh"), None)
1524 .await
1525 .unwrap();
1526
1527 let results = manager.search_tasks("JWT").await.unwrap();
1529
1530 assert_eq!(results.len(), 2);
1531 for result in &results {
1532 assert!(result
1533 .task
1534 .spec
1535 .as_ref()
1536 .unwrap()
1537 .to_uppercase()
1538 .contains("JWT"));
1539 }
1540 }
1541
1542 #[tokio::test]
1543 async fn test_search_tasks_with_advanced_query() {
1544 let ctx = TestContext::new().await;
1545 let manager = TaskManager::new(ctx.pool());
1546
1547 manager
1549 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1550 .await
1551 .unwrap();
1552 manager
1553 .add_task("Feature", Some("Add authentication feature"), None)
1554 .await
1555 .unwrap();
1556 manager
1557 .add_task("Bug report", Some("Report critical database bug"), None)
1558 .await
1559 .unwrap();
1560
1561 let results = manager
1563 .search_tasks("authentication AND bug")
1564 .await
1565 .unwrap();
1566
1567 assert_eq!(results.len(), 1);
1568 assert!(results[0]
1569 .task
1570 .spec
1571 .as_ref()
1572 .unwrap()
1573 .contains("authentication"));
1574 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1575 }
1576
1577 #[tokio::test]
1578 async fn test_search_tasks_no_results() {
1579 let ctx = TestContext::new().await;
1580 let manager = TaskManager::new(ctx.pool());
1581
1582 manager
1584 .add_task("Task 1", Some("Some description"), None)
1585 .await
1586 .unwrap();
1587
1588 let results = manager.search_tasks("nonexistent").await.unwrap();
1590
1591 assert_eq!(results.len(), 0);
1592 }
1593
1594 #[tokio::test]
1595 async fn test_search_tasks_snippet_highlighting() {
1596 let ctx = TestContext::new().await;
1597 let manager = TaskManager::new(ctx.pool());
1598
1599 manager
1601 .add_task(
1602 "Test task",
1603 Some("This is a description with the keyword authentication in the middle"),
1604 None,
1605 )
1606 .await
1607 .unwrap();
1608
1609 let results = manager.search_tasks("authentication").await.unwrap();
1611
1612 assert_eq!(results.len(), 1);
1613 assert!(results[0].match_snippet.contains("**authentication**"));
1615 }
1616
1617 #[tokio::test]
1618 async fn test_pick_next_focused_subtask() {
1619 let ctx = TestContext::new().await;
1620 let manager = TaskManager::new(ctx.pool());
1621
1622 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1624 manager.start_task(parent.id, false).await.unwrap();
1625
1626 let subtask1 = manager
1628 .add_task("Subtask 1", None, Some(parent.id))
1629 .await
1630 .unwrap();
1631 let subtask2 = manager
1632 .add_task("Subtask 2", None, Some(parent.id))
1633 .await
1634 .unwrap();
1635
1636 manager
1638 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1639 .await
1640 .unwrap();
1641 manager
1642 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1643 .await
1644 .unwrap();
1645
1646 let response = manager.pick_next().await.unwrap();
1648
1649 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1650 assert!(response.task.is_some());
1651 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1652 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1653 }
1654
1655 #[tokio::test]
1656 async fn test_pick_next_top_level_task() {
1657 let ctx = TestContext::new().await;
1658 let manager = TaskManager::new(ctx.pool());
1659
1660 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1662 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1663
1664 manager
1666 .update_task(task1.id, None, None, None, None, None, Some(5))
1667 .await
1668 .unwrap();
1669 manager
1670 .update_task(task2.id, None, None, None, None, None, Some(3))
1671 .await
1672 .unwrap();
1673
1674 let response = manager.pick_next().await.unwrap();
1676
1677 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1678 assert!(response.task.is_some());
1679 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1680 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1681 }
1682
1683 #[tokio::test]
1684 async fn test_pick_next_no_tasks() {
1685 let ctx = TestContext::new().await;
1686 let manager = TaskManager::new(ctx.pool());
1687
1688 let response = manager.pick_next().await.unwrap();
1690
1691 assert_eq!(response.suggestion_type, "NONE");
1692 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1693 assert!(response.message.is_some());
1694 }
1695
1696 #[tokio::test]
1697 async fn test_pick_next_all_completed() {
1698 let ctx = TestContext::new().await;
1699 let manager = TaskManager::new(ctx.pool());
1700
1701 let task = manager.add_task("Task 1", None, None).await.unwrap();
1703 manager.start_task(task.id, false).await.unwrap();
1704 manager.done_task().await.unwrap();
1705
1706 let response = manager.pick_next().await.unwrap();
1708
1709 assert_eq!(response.suggestion_type, "NONE");
1710 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1711 assert!(response.message.is_some());
1712 }
1713
1714 #[tokio::test]
1715 async fn test_pick_next_no_available_todos() {
1716 let ctx = TestContext::new().await;
1717 let manager = TaskManager::new(ctx.pool());
1718
1719 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1721 manager.start_task(parent.id, false).await.unwrap();
1722
1723 let subtask = manager
1725 .add_task("Subtask", None, Some(parent.id))
1726 .await
1727 .unwrap();
1728 manager.switch_to_task(subtask.id).await.unwrap();
1729
1730 let response = manager.pick_next().await.unwrap();
1732
1733 assert_eq!(response.suggestion_type, "NONE");
1734 assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
1735 assert!(response.message.is_some());
1736 }
1737
1738 #[tokio::test]
1739 async fn test_pick_next_priority_ordering() {
1740 let ctx = TestContext::new().await;
1741 let manager = TaskManager::new(ctx.pool());
1742
1743 let parent = manager.add_task("Parent", None, None).await.unwrap();
1745 manager.start_task(parent.id, false).await.unwrap();
1746
1747 let sub1 = manager
1749 .add_task("Priority 10", None, Some(parent.id))
1750 .await
1751 .unwrap();
1752 manager
1753 .update_task(sub1.id, None, None, None, None, None, Some(10))
1754 .await
1755 .unwrap();
1756
1757 let sub2 = manager
1758 .add_task("Priority 1", None, Some(parent.id))
1759 .await
1760 .unwrap();
1761 manager
1762 .update_task(sub2.id, None, None, None, None, None, Some(1))
1763 .await
1764 .unwrap();
1765
1766 let sub3 = manager
1767 .add_task("Priority 5", None, Some(parent.id))
1768 .await
1769 .unwrap();
1770 manager
1771 .update_task(sub3.id, None, None, None, None, None, Some(5))
1772 .await
1773 .unwrap();
1774
1775 let response = manager.pick_next().await.unwrap();
1777
1778 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1779 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
1780 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
1781 }
1782
1783 #[tokio::test]
1784 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
1785 let ctx = TestContext::new().await;
1786 let manager = TaskManager::new(ctx.pool());
1787
1788 let parent = manager.add_task("Parent", None, None).await.unwrap();
1790 manager.start_task(parent.id, false).await.unwrap();
1791
1792 let top_level = manager
1794 .add_task("Top level task", None, None)
1795 .await
1796 .unwrap();
1797
1798 let response = manager.pick_next().await.unwrap();
1800
1801 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1802 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
1803 }
1804}