1use crate::db::models::{
2 CurrentTaskInfo, DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, ParentTaskInfo,
3 PickNextResponse, PreviousTaskInfo, SpawnSubtaskResponse, SubtaskInfo, SwitchTaskResponse,
4 Task, TaskSearchResult, TaskWithEvents, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::{Row, SqlitePool};
9
10pub struct TaskManager<'a> {
11 pool: &'a SqlitePool,
12}
13
14impl<'a> TaskManager<'a> {
15 pub fn new(pool: &'a SqlitePool) -> Self {
16 Self { pool }
17 }
18
19 pub async fn add_task(
21 &self,
22 name: &str,
23 spec: Option<&str>,
24 parent_id: Option<i64>,
25 ) -> Result<Task> {
26 if let Some(pid) = parent_id {
28 self.check_task_exists(pid).await?;
29 }
30
31 let now = Utc::now();
32
33 let result = sqlx::query(
34 r#"
35 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
36 VALUES (?, ?, ?, 'todo', ?)
37 "#,
38 )
39 .bind(name)
40 .bind(spec)
41 .bind(parent_id)
42 .bind(now)
43 .execute(self.pool)
44 .await?;
45
46 let id = result.last_insert_rowid();
47 self.get_task(id).await
48 }
49
50 pub async fn get_task(&self, id: i64) -> Result<Task> {
52 let task = sqlx::query_as::<_, Task>(
53 r#"
54 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
55 FROM tasks
56 WHERE id = ?
57 "#,
58 )
59 .bind(id)
60 .fetch_optional(self.pool)
61 .await?
62 .ok_or(IntentError::TaskNotFound(id))?;
63
64 Ok(task)
65 }
66
67 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
69 let task = self.get_task(id).await?;
70 let events_summary = self.get_events_summary(id).await?;
71
72 Ok(TaskWithEvents {
73 task,
74 events_summary: Some(events_summary),
75 })
76 }
77
78 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
80 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
81 .bind(task_id)
82 .fetch_one(self.pool)
83 .await?;
84
85 let recent_events = sqlx::query_as::<_, Event>(
86 r#"
87 SELECT id, task_id, timestamp, log_type, discussion_data
88 FROM events
89 WHERE task_id = ?
90 ORDER BY timestamp DESC
91 LIMIT 10
92 "#,
93 )
94 .bind(task_id)
95 .fetch_all(self.pool)
96 .await?;
97
98 Ok(EventsSummary {
99 total_count,
100 recent_events,
101 })
102 }
103
104 #[allow(clippy::too_many_arguments)]
106 pub async fn update_task(
107 &self,
108 id: i64,
109 name: Option<&str>,
110 spec: Option<&str>,
111 parent_id: Option<Option<i64>>,
112 status: Option<&str>,
113 complexity: Option<i32>,
114 priority: Option<i32>,
115 ) -> Result<Task> {
116 let task = self.get_task(id).await?;
118
119 if let Some(s) = status {
121 if !["todo", "doing", "done"].contains(&s) {
122 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
123 }
124 }
125
126 if let Some(Some(pid)) = parent_id {
128 if pid == id {
129 return Err(IntentError::CircularDependency);
130 }
131 self.check_task_exists(pid).await?;
132 self.check_circular_dependency(id, pid).await?;
133 }
134
135 let mut query = String::from("UPDATE tasks SET ");
137 let mut updates = Vec::new();
138
139 if let Some(n) = name {
140 updates.push(format!("name = '{}'", n.replace('\'', "''")));
141 }
142
143 if let Some(s) = spec {
144 updates.push(format!("spec = '{}'", s.replace('\'', "''")));
145 }
146
147 if let Some(pid) = parent_id {
148 match pid {
149 Some(p) => updates.push(format!("parent_id = {}", p)),
150 None => updates.push("parent_id = NULL".to_string()),
151 }
152 }
153
154 if let Some(c) = complexity {
155 updates.push(format!("complexity = {}", c));
156 }
157
158 if let Some(p) = priority {
159 updates.push(format!("priority = {}", p));
160 }
161
162 if let Some(s) = status {
163 updates.push(format!("status = '{}'", s));
164
165 let now = Utc::now();
167 match s {
168 "todo" if task.first_todo_at.is_none() => {
169 updates.push(format!("first_todo_at = '{}'", now.to_rfc3339()));
170 },
171 "doing" if task.first_doing_at.is_none() => {
172 updates.push(format!("first_doing_at = '{}'", now.to_rfc3339()));
173 },
174 "done" if task.first_done_at.is_none() => {
175 updates.push(format!("first_done_at = '{}'", now.to_rfc3339()));
176 },
177 _ => {},
178 }
179 }
180
181 if updates.is_empty() {
182 return Ok(task);
183 }
184
185 query.push_str(&updates.join(", "));
186 query.push_str(&format!(" WHERE id = {}", id));
187
188 sqlx::query(&query).execute(self.pool).await?;
189
190 self.get_task(id).await
191 }
192
193 pub async fn delete_task(&self, id: i64) -> Result<()> {
195 self.check_task_exists(id).await?;
196
197 sqlx::query("DELETE FROM tasks WHERE id = ?")
198 .bind(id)
199 .execute(self.pool)
200 .await?;
201
202 Ok(())
203 }
204
205 pub async fn find_tasks(
207 &self,
208 status: Option<&str>,
209 parent_id: Option<Option<i64>>,
210 ) -> Result<Vec<Task>> {
211 let mut query = String::from(
212 "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"
213 );
214 let mut conditions = Vec::new();
215
216 if let Some(s) = status {
217 query.push_str(" AND status = ?");
218 conditions.push(s.to_string());
219 }
220
221 if let Some(pid) = parent_id {
222 if let Some(p) = pid {
223 query.push_str(" AND parent_id = ?");
224 conditions.push(p.to_string());
225 } else {
226 query.push_str(" AND parent_id IS NULL");
227 }
228 }
229
230 query.push_str(" ORDER BY id");
231
232 let mut q = sqlx::query_as::<_, Task>(&query);
233 for cond in conditions {
234 q = q.bind(cond);
235 }
236
237 let tasks = q.fetch_all(self.pool).await?;
238 Ok(tasks)
239 }
240
241 pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
244 let escaped_query = self.escape_fts_query(query);
246
247 let results = sqlx::query(
251 r#"
252 SELECT
253 t.id,
254 t.parent_id,
255 t.name,
256 t.spec,
257 t.status,
258 t.complexity,
259 t.priority,
260 t.first_todo_at,
261 t.first_doing_at,
262 t.first_done_at,
263 COALESCE(
264 snippet(tasks_fts, 1, '**', '**', '...', 15),
265 snippet(tasks_fts, 0, '**', '**', '...', 15)
266 ) as match_snippet
267 FROM tasks_fts
268 INNER JOIN tasks t ON tasks_fts.rowid = t.id
269 WHERE tasks_fts MATCH ?
270 ORDER BY rank
271 "#,
272 )
273 .bind(&escaped_query)
274 .fetch_all(self.pool)
275 .await?;
276
277 let mut search_results = Vec::new();
278 for row in results {
279 let task = Task {
280 id: row.get("id"),
281 parent_id: row.get("parent_id"),
282 name: row.get("name"),
283 spec: row.get("spec"),
284 status: row.get("status"),
285 complexity: row.get("complexity"),
286 priority: row.get("priority"),
287 first_todo_at: row.get("first_todo_at"),
288 first_doing_at: row.get("first_doing_at"),
289 first_done_at: row.get("first_done_at"),
290 };
291 let match_snippet: String = row.get("match_snippet");
292
293 search_results.push(TaskSearchResult {
294 task,
295 match_snippet,
296 });
297 }
298
299 Ok(search_results)
300 }
301
302 fn escape_fts_query(&self, query: &str) -> String {
304 query.replace('"', "\"\"")
308 }
309
310 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
312 let mut tx = self.pool.begin().await?;
313
314 let now = Utc::now();
315
316 sqlx::query(
318 r#"
319 UPDATE tasks
320 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
321 WHERE id = ?
322 "#,
323 )
324 .bind(now)
325 .bind(id)
326 .execute(&mut *tx)
327 .await?;
328
329 sqlx::query(
331 r#"
332 INSERT OR REPLACE INTO workspace_state (key, value)
333 VALUES ('current_task_id', ?)
334 "#,
335 )
336 .bind(id.to_string())
337 .execute(&mut *tx)
338 .await?;
339
340 tx.commit().await?;
341
342 if with_events {
343 self.get_task_with_events(id).await
344 } else {
345 let task = self.get_task(id).await?;
346 Ok(TaskWithEvents {
347 task,
348 events_summary: None,
349 })
350 }
351 }
352
353 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
357 let mut tx = self.pool.begin().await?;
358
359 let current_task_id: Option<String> =
361 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
362 .fetch_optional(&mut *tx)
363 .await?;
364
365 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
366 IntentError::InvalidInput(
367 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
368 ),
369 )?;
370
371 let task_info: (String, Option<i64>) =
373 sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
374 .bind(id)
375 .fetch_one(&mut *tx)
376 .await?;
377 let (task_name, parent_id) = task_info;
378
379 let uncompleted_children: i64 = sqlx::query_scalar(
381 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
382 )
383 .bind(id)
384 .fetch_one(&mut *tx)
385 .await?;
386
387 if uncompleted_children > 0 {
388 return Err(IntentError::UncompletedChildren);
389 }
390
391 let now = Utc::now();
392
393 sqlx::query(
395 r#"
396 UPDATE tasks
397 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
398 WHERE id = ?
399 "#,
400 )
401 .bind(now)
402 .bind(id)
403 .execute(&mut *tx)
404 .await?;
405
406 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
408 .execute(&mut *tx)
409 .await?;
410
411 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
413 let remaining_siblings: i64 = sqlx::query_scalar(
415 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
416 )
417 .bind(parent_task_id)
418 .bind(id)
419 .fetch_one(&mut *tx)
420 .await?;
421
422 if remaining_siblings == 0 {
423 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
425 .bind(parent_task_id)
426 .fetch_one(&mut *tx)
427 .await?;
428
429 NextStepSuggestion::ParentIsReady {
430 message: format!(
431 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
432 parent_task_id, parent_name
433 ),
434 parent_task_id,
435 parent_task_name: parent_name,
436 }
437 } else {
438 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
440 .bind(parent_task_id)
441 .fetch_one(&mut *tx)
442 .await?;
443
444 NextStepSuggestion::SiblingTasksRemain {
445 message: format!(
446 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
447 id, parent_task_id, parent_name
448 ),
449 parent_task_id,
450 parent_task_name: parent_name,
451 remaining_siblings_count: remaining_siblings,
452 }
453 }
454 } else {
455 let child_count: i64 =
457 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
458 .bind(id)
459 .fetch_one(&mut *tx)
460 .await?;
461
462 if child_count > 0 {
463 NextStepSuggestion::TopLevelTaskCompleted {
465 message: format!(
466 "Top-level task #{} '{}' has been completed. Well done!",
467 id, task_name
468 ),
469 completed_task_id: id,
470 completed_task_name: task_name.clone(),
471 }
472 } else {
473 let remaining_tasks: i64 = sqlx::query_scalar(
475 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
476 )
477 .bind(id)
478 .fetch_one(&mut *tx)
479 .await?;
480
481 if remaining_tasks == 0 {
482 NextStepSuggestion::WorkspaceIsClear {
483 message: format!(
484 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
485 id
486 ),
487 completed_task_id: id,
488 }
489 } else {
490 NextStepSuggestion::NoParentContext {
491 message: format!("Task #{} '{}' has been completed.", id, task_name),
492 completed_task_id: id,
493 completed_task_name: task_name.clone(),
494 }
495 }
496 }
497 };
498
499 tx.commit().await?;
500
501 let completed_task = self.get_task(id).await?;
502
503 Ok(DoneTaskResponse {
504 completed_task,
505 workspace_status: WorkspaceStatus {
506 current_task_id: None,
507 },
508 next_step_suggestion,
509 })
510 }
511
512 async fn check_task_exists(&self, id: i64) -> Result<()> {
514 let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
515 .bind(id)
516 .fetch_one(self.pool)
517 .await?;
518
519 if !exists {
520 return Err(IntentError::TaskNotFound(id));
521 }
522
523 Ok(())
524 }
525
526 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
528 let mut current_id = new_parent_id;
529
530 loop {
531 if current_id == task_id {
532 return Err(IntentError::CircularDependency);
533 }
534
535 let parent: Option<i64> =
536 sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
537 .bind(current_id)
538 .fetch_optional(self.pool)
539 .await?;
540
541 match parent {
542 Some(pid) => current_id = pid,
543 None => break,
544 }
545 }
546
547 Ok(())
548 }
549
550 pub async fn switch_to_task(&self, id: i64) -> Result<SwitchTaskResponse> {
554 self.check_task_exists(id).await?;
556
557 let mut tx = self.pool.begin().await?;
558 let now = Utc::now();
559
560 let current_task_id: Option<String> =
562 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
563 .fetch_optional(&mut *tx)
564 .await?;
565
566 let previous_task = if let Some(prev_id_str) = current_task_id {
567 if let Ok(prev_id) = prev_id_str.parse::<i64>() {
568 sqlx::query(
570 r#"
571 UPDATE tasks
572 SET status = 'todo'
573 WHERE id = ? AND status = 'doing'
574 "#,
575 )
576 .bind(prev_id)
577 .execute(&mut *tx)
578 .await?;
579
580 Some(PreviousTaskInfo {
581 id: prev_id,
582 status: "todo".to_string(),
583 })
584 } else {
585 None
586 }
587 } else {
588 None
589 };
590
591 sqlx::query(
593 r#"
594 UPDATE tasks
595 SET status = 'doing',
596 first_doing_at = COALESCE(first_doing_at, ?)
597 WHERE id = ? AND status != 'doing'
598 "#,
599 )
600 .bind(now)
601 .bind(id)
602 .execute(&mut *tx)
603 .await?;
604
605 let (task_name, task_status): (String, String) =
607 sqlx::query_as("SELECT name, status FROM tasks WHERE id = ?")
608 .bind(id)
609 .fetch_one(&mut *tx)
610 .await?;
611
612 sqlx::query(
614 r#"
615 INSERT OR REPLACE INTO workspace_state (key, value)
616 VALUES ('current_task_id', ?)
617 "#,
618 )
619 .bind(id.to_string())
620 .execute(&mut *tx)
621 .await?;
622
623 tx.commit().await?;
624
625 Ok(SwitchTaskResponse {
626 previous_task,
627 current_task: CurrentTaskInfo {
628 id,
629 name: task_name,
630 status: task_status,
631 },
632 })
633 }
634
635 pub async fn spawn_subtask(
639 &self,
640 name: &str,
641 spec: Option<&str>,
642 ) -> Result<SpawnSubtaskResponse> {
643 let current_task_id: Option<String> =
645 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
646 .fetch_optional(self.pool)
647 .await?;
648
649 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
650 IntentError::InvalidInput("No current task to create subtask under".to_string()),
651 )?;
652
653 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
655 .bind(parent_id)
656 .fetch_one(self.pool)
657 .await?;
658
659 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
661
662 self.switch_to_task(subtask.id).await?;
664
665 Ok(SpawnSubtaskResponse {
666 subtask: SubtaskInfo {
667 id: subtask.id,
668 name: subtask.name,
669 parent_id,
670 status: "doing".to_string(),
671 },
672 parent_task: ParentTaskInfo {
673 id: parent_id,
674 name: parent_name,
675 },
676 })
677 }
678
679 pub async fn pick_next_tasks(
692 &self,
693 max_count: usize,
694 capacity_limit: usize,
695 ) -> Result<Vec<Task>> {
696 let mut tx = self.pool.begin().await?;
697
698 let doing_count: i64 =
700 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
701 .fetch_one(&mut *tx)
702 .await?;
703
704 let available = capacity_limit.saturating_sub(doing_count as usize);
706 if available == 0 {
707 return Ok(vec![]);
708 }
709
710 let limit = std::cmp::min(max_count, available);
711
712 let todo_tasks = sqlx::query_as::<_, Task>(
714 r#"
715 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
716 FROM tasks
717 WHERE status = 'todo'
718 ORDER BY
719 COALESCE(priority, 0) DESC,
720 COALESCE(complexity, 5) ASC,
721 id ASC
722 LIMIT ?
723 "#,
724 )
725 .bind(limit as i64)
726 .fetch_all(&mut *tx)
727 .await?;
728
729 if todo_tasks.is_empty() {
730 return Ok(vec![]);
731 }
732
733 let now = Utc::now();
734
735 for task in &todo_tasks {
737 sqlx::query(
738 r#"
739 UPDATE tasks
740 SET status = 'doing',
741 first_doing_at = COALESCE(first_doing_at, ?)
742 WHERE id = ?
743 "#,
744 )
745 .bind(now)
746 .bind(task.id)
747 .execute(&mut *tx)
748 .await?;
749 }
750
751 tx.commit().await?;
752
753 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
755 let placeholders = vec!["?"; task_ids.len()].join(",");
756 let query = format!(
757 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
758 FROM tasks WHERE id IN ({})
759 ORDER BY
760 COALESCE(priority, 0) DESC,
761 COALESCE(complexity, 5) ASC,
762 id ASC",
763 placeholders
764 );
765
766 let mut q = sqlx::query_as::<_, Task>(&query);
767 for id in task_ids {
768 q = q.bind(id);
769 }
770
771 let updated_tasks = q.fetch_all(self.pool).await?;
772 Ok(updated_tasks)
773 }
774
775 pub async fn pick_next(&self) -> Result<PickNextResponse> {
784 let current_task_id: Option<String> =
786 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
787 .fetch_optional(self.pool)
788 .await?;
789
790 if let Some(current_id_str) = current_task_id {
791 if let Ok(current_id) = current_id_str.parse::<i64>() {
792 let subtasks = sqlx::query_as::<_, Task>(
794 r#"
795 SELECT id, parent_id, name, spec, status, complexity, priority,
796 first_todo_at, first_doing_at, first_done_at
797 FROM tasks
798 WHERE parent_id = ? AND status = 'todo'
799 ORDER BY COALESCE(priority, 999999) ASC, id ASC
800 LIMIT 1
801 "#,
802 )
803 .bind(current_id)
804 .fetch_optional(self.pool)
805 .await?;
806
807 if let Some(task) = subtasks {
808 return Ok(PickNextResponse::focused_subtask(task));
809 }
810 }
811 }
812
813 let top_level_task = sqlx::query_as::<_, Task>(
815 r#"
816 SELECT id, parent_id, name, spec, status, complexity, priority,
817 first_todo_at, first_doing_at, first_done_at
818 FROM tasks
819 WHERE parent_id IS NULL AND status = 'todo'
820 ORDER BY COALESCE(priority, 999999) ASC, id ASC
821 LIMIT 1
822 "#,
823 )
824 .fetch_optional(self.pool)
825 .await?;
826
827 if let Some(task) = top_level_task {
828 return Ok(PickNextResponse::top_level_task(task));
829 }
830
831 let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
834 .fetch_one(self.pool)
835 .await?;
836
837 if total_tasks == 0 {
838 return Ok(PickNextResponse::no_tasks_in_project());
839 }
840
841 let todo_or_doing_count: i64 =
843 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
844 .fetch_one(self.pool)
845 .await?;
846
847 if todo_or_doing_count == 0 {
848 return Ok(PickNextResponse::all_tasks_completed());
849 }
850
851 Ok(PickNextResponse::no_available_todos())
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use crate::events::EventManager;
860 use crate::test_utils::test_helpers::TestContext;
861
862 #[tokio::test]
863 async fn test_add_task() {
864 let ctx = TestContext::new().await;
865 let manager = TaskManager::new(ctx.pool());
866
867 let task = manager.add_task("Test task", None, None).await.unwrap();
868
869 assert_eq!(task.name, "Test task");
870 assert_eq!(task.status, "todo");
871 assert!(task.first_todo_at.is_some());
872 assert!(task.first_doing_at.is_none());
873 assert!(task.first_done_at.is_none());
874 }
875
876 #[tokio::test]
877 async fn test_add_task_with_spec() {
878 let ctx = TestContext::new().await;
879 let manager = TaskManager::new(ctx.pool());
880
881 let spec = "This is a task specification";
882 let task = manager
883 .add_task("Test task", Some(spec), None)
884 .await
885 .unwrap();
886
887 assert_eq!(task.name, "Test task");
888 assert_eq!(task.spec.as_deref(), Some(spec));
889 }
890
891 #[tokio::test]
892 async fn test_add_task_with_parent() {
893 let ctx = TestContext::new().await;
894 let manager = TaskManager::new(ctx.pool());
895
896 let parent = manager.add_task("Parent task", None, None).await.unwrap();
897 let child = manager
898 .add_task("Child task", None, Some(parent.id))
899 .await
900 .unwrap();
901
902 assert_eq!(child.parent_id, Some(parent.id));
903 }
904
905 #[tokio::test]
906 async fn test_get_task() {
907 let ctx = TestContext::new().await;
908 let manager = TaskManager::new(ctx.pool());
909
910 let created = manager.add_task("Test task", None, None).await.unwrap();
911 let retrieved = manager.get_task(created.id).await.unwrap();
912
913 assert_eq!(created.id, retrieved.id);
914 assert_eq!(created.name, retrieved.name);
915 }
916
917 #[tokio::test]
918 async fn test_get_task_not_found() {
919 let ctx = TestContext::new().await;
920 let manager = TaskManager::new(ctx.pool());
921
922 let result = manager.get_task(999).await;
923 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
924 }
925
926 #[tokio::test]
927 async fn test_update_task_name() {
928 let ctx = TestContext::new().await;
929 let manager = TaskManager::new(ctx.pool());
930
931 let task = manager.add_task("Original name", None, None).await.unwrap();
932 let updated = manager
933 .update_task(task.id, Some("New name"), None, None, None, None, None)
934 .await
935 .unwrap();
936
937 assert_eq!(updated.name, "New name");
938 }
939
940 #[tokio::test]
941 async fn test_update_task_status() {
942 let ctx = TestContext::new().await;
943 let manager = TaskManager::new(ctx.pool());
944
945 let task = manager.add_task("Test task", None, None).await.unwrap();
946 let updated = manager
947 .update_task(task.id, None, None, None, Some("doing"), None, None)
948 .await
949 .unwrap();
950
951 assert_eq!(updated.status, "doing");
952 assert!(updated.first_doing_at.is_some());
953 }
954
955 #[tokio::test]
956 async fn test_delete_task() {
957 let ctx = TestContext::new().await;
958 let manager = TaskManager::new(ctx.pool());
959
960 let task = manager.add_task("Test task", None, None).await.unwrap();
961 manager.delete_task(task.id).await.unwrap();
962
963 let result = manager.get_task(task.id).await;
964 assert!(result.is_err());
965 }
966
967 #[tokio::test]
968 async fn test_find_tasks_by_status() {
969 let ctx = TestContext::new().await;
970 let manager = TaskManager::new(ctx.pool());
971
972 manager.add_task("Todo task", None, None).await.unwrap();
973 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
974 manager
975 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
976 .await
977 .unwrap();
978
979 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
980 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
981
982 assert_eq!(todo_tasks.len(), 1);
983 assert_eq!(doing_tasks.len(), 1);
984 assert_eq!(doing_tasks[0].status, "doing");
985 }
986
987 #[tokio::test]
988 async fn test_find_tasks_by_parent() {
989 let ctx = TestContext::new().await;
990 let manager = TaskManager::new(ctx.pool());
991
992 let parent = manager.add_task("Parent", None, None).await.unwrap();
993 manager
994 .add_task("Child 1", None, Some(parent.id))
995 .await
996 .unwrap();
997 manager
998 .add_task("Child 2", None, Some(parent.id))
999 .await
1000 .unwrap();
1001
1002 let children = manager
1003 .find_tasks(None, Some(Some(parent.id)))
1004 .await
1005 .unwrap();
1006
1007 assert_eq!(children.len(), 2);
1008 }
1009
1010 #[tokio::test]
1011 async fn test_start_task() {
1012 let ctx = TestContext::new().await;
1013 let manager = TaskManager::new(ctx.pool());
1014
1015 let task = manager.add_task("Test task", None, None).await.unwrap();
1016 let started = manager.start_task(task.id, false).await.unwrap();
1017
1018 assert_eq!(started.task.status, "doing");
1019 assert!(started.task.first_doing_at.is_some());
1020
1021 let current: Option<String> =
1023 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1024 .fetch_optional(ctx.pool())
1025 .await
1026 .unwrap();
1027
1028 assert_eq!(current, Some(task.id.to_string()));
1029 }
1030
1031 #[tokio::test]
1032 async fn test_start_task_with_events() {
1033 let ctx = TestContext::new().await;
1034 let manager = TaskManager::new(ctx.pool());
1035
1036 let task = manager.add_task("Test task", None, None).await.unwrap();
1037
1038 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1040 .bind(task.id)
1041 .bind("test")
1042 .bind("test event")
1043 .execute(ctx.pool())
1044 .await
1045 .unwrap();
1046
1047 let started = manager.start_task(task.id, true).await.unwrap();
1048
1049 assert!(started.events_summary.is_some());
1050 let summary = started.events_summary.unwrap();
1051 assert_eq!(summary.total_count, 1);
1052 }
1053
1054 #[tokio::test]
1055 async fn test_done_task() {
1056 let ctx = TestContext::new().await;
1057 let manager = TaskManager::new(ctx.pool());
1058
1059 let task = manager.add_task("Test task", None, None).await.unwrap();
1060 manager.start_task(task.id, false).await.unwrap();
1061 let response = manager.done_task().await.unwrap();
1062
1063 assert_eq!(response.completed_task.status, "done");
1064 assert!(response.completed_task.first_done_at.is_some());
1065 assert_eq!(response.workspace_status.current_task_id, None);
1066
1067 match response.next_step_suggestion {
1069 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1070 _ => panic!("Expected WorkspaceIsClear suggestion"),
1071 }
1072
1073 let current: Option<String> =
1075 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1076 .fetch_optional(ctx.pool())
1077 .await
1078 .unwrap();
1079
1080 assert!(current.is_none());
1081 }
1082
1083 #[tokio::test]
1084 async fn test_done_task_with_uncompleted_children() {
1085 let ctx = TestContext::new().await;
1086 let manager = TaskManager::new(ctx.pool());
1087
1088 let parent = manager.add_task("Parent", None, None).await.unwrap();
1089 manager
1090 .add_task("Child", None, Some(parent.id))
1091 .await
1092 .unwrap();
1093
1094 manager.start_task(parent.id, false).await.unwrap();
1096
1097 let result = manager.done_task().await;
1098 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1099 }
1100
1101 #[tokio::test]
1102 async fn test_done_task_with_completed_children() {
1103 let ctx = TestContext::new().await;
1104 let manager = TaskManager::new(ctx.pool());
1105
1106 let parent = manager.add_task("Parent", None, None).await.unwrap();
1107 let child = manager
1108 .add_task("Child", None, Some(parent.id))
1109 .await
1110 .unwrap();
1111
1112 manager.start_task(child.id, false).await.unwrap();
1114 let child_response = manager.done_task().await.unwrap();
1115
1116 match child_response.next_step_suggestion {
1118 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1119 assert_eq!(parent_task_id, parent.id);
1120 },
1121 _ => panic!("Expected ParentIsReady suggestion"),
1122 }
1123
1124 manager.start_task(parent.id, false).await.unwrap();
1126 let parent_response = manager.done_task().await.unwrap();
1127 assert_eq!(parent_response.completed_task.status, "done");
1128
1129 match parent_response.next_step_suggestion {
1131 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1132 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1133 }
1134 }
1135
1136 #[tokio::test]
1137 async fn test_circular_dependency() {
1138 let ctx = TestContext::new().await;
1139 let manager = TaskManager::new(ctx.pool());
1140
1141 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1142 let task2 = manager
1143 .add_task("Task 2", None, Some(task1.id))
1144 .await
1145 .unwrap();
1146
1147 let result = manager
1149 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1150 .await;
1151
1152 assert!(matches!(result, Err(IntentError::CircularDependency)));
1153 }
1154
1155 #[tokio::test]
1156 async fn test_invalid_parent_id() {
1157 let ctx = TestContext::new().await;
1158 let manager = TaskManager::new(ctx.pool());
1159
1160 let result = manager.add_task("Test", None, Some(999)).await;
1161 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1162 }
1163
1164 #[tokio::test]
1165 async fn test_update_task_complexity_and_priority() {
1166 let ctx = TestContext::new().await;
1167 let manager = TaskManager::new(ctx.pool());
1168
1169 let task = manager.add_task("Test task", None, None).await.unwrap();
1170 let updated = manager
1171 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1172 .await
1173 .unwrap();
1174
1175 assert_eq!(updated.complexity, Some(8));
1176 assert_eq!(updated.priority, Some(10));
1177 }
1178
1179 #[tokio::test]
1180 async fn test_switch_to_task() {
1181 let ctx = TestContext::new().await;
1182 let manager = TaskManager::new(ctx.pool());
1183
1184 let task = manager.add_task("Test task", None, None).await.unwrap();
1186 assert_eq!(task.status, "todo");
1187
1188 let response = manager.switch_to_task(task.id).await.unwrap();
1190 assert_eq!(response.current_task.id, task.id);
1191 assert_eq!(response.current_task.status, "doing");
1192 assert!(response.previous_task.is_none());
1193
1194 let current: Option<String> =
1196 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1197 .fetch_optional(ctx.pool())
1198 .await
1199 .unwrap();
1200
1201 assert_eq!(current, Some(task.id.to_string()));
1202 }
1203
1204 #[tokio::test]
1205 async fn test_switch_to_task_already_doing() {
1206 let ctx = TestContext::new().await;
1207 let manager = TaskManager::new(ctx.pool());
1208
1209 let task = manager.add_task("Test task", None, None).await.unwrap();
1211 manager.start_task(task.id, false).await.unwrap();
1212
1213 let response = manager.switch_to_task(task.id).await.unwrap();
1215 assert_eq!(response.current_task.id, task.id);
1216 assert_eq!(response.current_task.status, "doing");
1217 }
1218
1219 #[tokio::test]
1220 async fn test_spawn_subtask() {
1221 let ctx = TestContext::new().await;
1222 let manager = TaskManager::new(ctx.pool());
1223
1224 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1226 manager.start_task(parent.id, false).await.unwrap();
1227
1228 let response = manager
1230 .spawn_subtask("Child task", Some("Details"))
1231 .await
1232 .unwrap();
1233
1234 assert_eq!(response.subtask.parent_id, parent.id);
1235 assert_eq!(response.subtask.name, "Child task");
1236 assert_eq!(response.subtask.status, "doing");
1237 assert_eq!(response.parent_task.id, parent.id);
1238 assert_eq!(response.parent_task.name, "Parent task");
1239
1240 let current: Option<String> =
1242 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1243 .fetch_optional(ctx.pool())
1244 .await
1245 .unwrap();
1246
1247 assert_eq!(current, Some(response.subtask.id.to_string()));
1248
1249 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1251 assert_eq!(retrieved.status, "doing");
1252 }
1253
1254 #[tokio::test]
1255 async fn test_spawn_subtask_no_current_task() {
1256 let ctx = TestContext::new().await;
1257 let manager = TaskManager::new(ctx.pool());
1258
1259 let result = manager.spawn_subtask("Child", None).await;
1261 assert!(result.is_err());
1262 }
1263
1264 #[tokio::test]
1265 async fn test_pick_next_tasks_basic() {
1266 let ctx = TestContext::new().await;
1267 let manager = TaskManager::new(ctx.pool());
1268
1269 for i in 1..=10 {
1271 manager
1272 .add_task(&format!("Task {}", i), None, None)
1273 .await
1274 .unwrap();
1275 }
1276
1277 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1279
1280 assert_eq!(picked.len(), 5);
1281 for task in &picked {
1282 assert_eq!(task.status, "doing");
1283 assert!(task.first_doing_at.is_some());
1284 }
1285
1286 let doing_count: i64 =
1288 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1289 .fetch_one(ctx.pool())
1290 .await
1291 .unwrap();
1292
1293 assert_eq!(doing_count, 5);
1294 }
1295
1296 #[tokio::test]
1297 async fn test_pick_next_tasks_with_existing_doing() {
1298 let ctx = TestContext::new().await;
1299 let manager = TaskManager::new(ctx.pool());
1300
1301 for i in 1..=10 {
1303 manager
1304 .add_task(&format!("Task {}", i), None, None)
1305 .await
1306 .unwrap();
1307 }
1308
1309 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1311 manager.start_task(tasks[0].id, false).await.unwrap();
1312 manager.start_task(tasks[1].id, false).await.unwrap();
1313
1314 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1316
1317 assert_eq!(picked.len(), 3);
1319
1320 let doing_count: i64 =
1322 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1323 .fetch_one(ctx.pool())
1324 .await
1325 .unwrap();
1326
1327 assert_eq!(doing_count, 5);
1328 }
1329
1330 #[tokio::test]
1331 async fn test_pick_next_tasks_at_capacity() {
1332 let ctx = TestContext::new().await;
1333 let manager = TaskManager::new(ctx.pool());
1334
1335 for i in 1..=10 {
1337 manager
1338 .add_task(&format!("Task {}", i), None, None)
1339 .await
1340 .unwrap();
1341 }
1342
1343 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1345 assert_eq!(first_batch.len(), 5);
1346
1347 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1349 assert_eq!(second_batch.len(), 0);
1350 }
1351
1352 #[tokio::test]
1353 async fn test_pick_next_tasks_priority_ordering() {
1354 let ctx = TestContext::new().await;
1355 let manager = TaskManager::new(ctx.pool());
1356
1357 let low = manager.add_task("Low priority", None, None).await.unwrap();
1359 manager
1360 .update_task(low.id, None, None, None, None, None, Some(1))
1361 .await
1362 .unwrap();
1363
1364 let high = manager.add_task("High priority", None, None).await.unwrap();
1365 manager
1366 .update_task(high.id, None, None, None, None, None, Some(10))
1367 .await
1368 .unwrap();
1369
1370 let medium = manager
1371 .add_task("Medium priority", None, None)
1372 .await
1373 .unwrap();
1374 manager
1375 .update_task(medium.id, None, None, None, None, None, Some(5))
1376 .await
1377 .unwrap();
1378
1379 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1381
1382 assert_eq!(picked.len(), 3);
1384 assert_eq!(picked[0].priority, Some(10)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(1)); }
1388
1389 #[tokio::test]
1390 async fn test_pick_next_tasks_complexity_ordering() {
1391 let ctx = TestContext::new().await;
1392 let manager = TaskManager::new(ctx.pool());
1393
1394 let complex = manager.add_task("Complex", None, None).await.unwrap();
1396 manager
1397 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1398 .await
1399 .unwrap();
1400
1401 let simple = manager.add_task("Simple", None, None).await.unwrap();
1402 manager
1403 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1404 .await
1405 .unwrap();
1406
1407 let medium = manager.add_task("Medium", None, None).await.unwrap();
1408 manager
1409 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1410 .await
1411 .unwrap();
1412
1413 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1415
1416 assert_eq!(picked.len(), 3);
1418 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1422
1423 #[tokio::test]
1424 async fn test_done_task_sibling_tasks_remain() {
1425 let ctx = TestContext::new().await;
1426 let manager = TaskManager::new(ctx.pool());
1427
1428 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1430 let child1 = manager
1431 .add_task("Child 1", None, Some(parent.id))
1432 .await
1433 .unwrap();
1434 let child2 = manager
1435 .add_task("Child 2", None, Some(parent.id))
1436 .await
1437 .unwrap();
1438 let _child3 = manager
1439 .add_task("Child 3", None, Some(parent.id))
1440 .await
1441 .unwrap();
1442
1443 manager.start_task(child1.id, false).await.unwrap();
1445 let response = manager.done_task().await.unwrap();
1446
1447 match response.next_step_suggestion {
1449 NextStepSuggestion::SiblingTasksRemain {
1450 parent_task_id,
1451 remaining_siblings_count,
1452 ..
1453 } => {
1454 assert_eq!(parent_task_id, parent.id);
1455 assert_eq!(remaining_siblings_count, 2); },
1457 _ => panic!("Expected SiblingTasksRemain suggestion"),
1458 }
1459
1460 manager.start_task(child2.id, false).await.unwrap();
1462 let response2 = manager.done_task().await.unwrap();
1463
1464 match response2.next_step_suggestion {
1466 NextStepSuggestion::SiblingTasksRemain {
1467 remaining_siblings_count,
1468 ..
1469 } => {
1470 assert_eq!(remaining_siblings_count, 1); },
1472 _ => panic!("Expected SiblingTasksRemain suggestion"),
1473 }
1474 }
1475
1476 #[tokio::test]
1477 async fn test_done_task_top_level_with_children() {
1478 let ctx = TestContext::new().await;
1479 let manager = TaskManager::new(ctx.pool());
1480
1481 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1483 let child = manager
1484 .add_task("Sub Task", None, Some(parent.id))
1485 .await
1486 .unwrap();
1487
1488 manager.start_task(child.id, false).await.unwrap();
1490 manager.done_task().await.unwrap();
1491
1492 manager.start_task(parent.id, false).await.unwrap();
1494 let response = manager.done_task().await.unwrap();
1495
1496 match response.next_step_suggestion {
1498 NextStepSuggestion::TopLevelTaskCompleted {
1499 completed_task_id,
1500 completed_task_name,
1501 ..
1502 } => {
1503 assert_eq!(completed_task_id, parent.id);
1504 assert_eq!(completed_task_name, "Epic Task");
1505 },
1506 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1507 }
1508 }
1509
1510 #[tokio::test]
1511 async fn test_done_task_no_parent_context() {
1512 let ctx = TestContext::new().await;
1513 let manager = TaskManager::new(ctx.pool());
1514
1515 let task1 = manager
1517 .add_task("Standalone Task 1", None, None)
1518 .await
1519 .unwrap();
1520 let _task2 = manager
1521 .add_task("Standalone Task 2", None, None)
1522 .await
1523 .unwrap();
1524
1525 manager.start_task(task1.id, false).await.unwrap();
1527 let response = manager.done_task().await.unwrap();
1528
1529 match response.next_step_suggestion {
1531 NextStepSuggestion::NoParentContext {
1532 completed_task_id,
1533 completed_task_name,
1534 ..
1535 } => {
1536 assert_eq!(completed_task_id, task1.id);
1537 assert_eq!(completed_task_name, "Standalone Task 1");
1538 },
1539 _ => panic!("Expected NoParentContext suggestion"),
1540 }
1541 }
1542
1543 #[tokio::test]
1544 async fn test_search_tasks_by_name() {
1545 let ctx = TestContext::new().await;
1546 let manager = TaskManager::new(ctx.pool());
1547
1548 manager
1550 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1551 .await
1552 .unwrap();
1553 manager
1554 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1555 .await
1556 .unwrap();
1557 manager
1558 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1559 .await
1560 .unwrap();
1561
1562 let results = manager.search_tasks("authentication").await.unwrap();
1564
1565 assert_eq!(results.len(), 2);
1566 assert!(results[0]
1567 .task
1568 .name
1569 .to_lowercase()
1570 .contains("authentication"));
1571 assert!(results[1]
1572 .task
1573 .name
1574 .to_lowercase()
1575 .contains("authentication"));
1576
1577 assert!(!results[0].match_snippet.is_empty());
1579 }
1580
1581 #[tokio::test]
1582 async fn test_search_tasks_by_spec() {
1583 let ctx = TestContext::new().await;
1584 let manager = TaskManager::new(ctx.pool());
1585
1586 manager
1588 .add_task("Task 1", Some("Implement JWT authentication"), None)
1589 .await
1590 .unwrap();
1591 manager
1592 .add_task("Task 2", Some("Add user registration"), None)
1593 .await
1594 .unwrap();
1595 manager
1596 .add_task("Task 3", Some("JWT token refresh"), None)
1597 .await
1598 .unwrap();
1599
1600 let results = manager.search_tasks("JWT").await.unwrap();
1602
1603 assert_eq!(results.len(), 2);
1604 for result in &results {
1605 assert!(result
1606 .task
1607 .spec
1608 .as_ref()
1609 .unwrap()
1610 .to_uppercase()
1611 .contains("JWT"));
1612 }
1613 }
1614
1615 #[tokio::test]
1616 async fn test_search_tasks_with_advanced_query() {
1617 let ctx = TestContext::new().await;
1618 let manager = TaskManager::new(ctx.pool());
1619
1620 manager
1622 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1623 .await
1624 .unwrap();
1625 manager
1626 .add_task("Feature", Some("Add authentication feature"), None)
1627 .await
1628 .unwrap();
1629 manager
1630 .add_task("Bug report", Some("Report critical database bug"), None)
1631 .await
1632 .unwrap();
1633
1634 let results = manager
1636 .search_tasks("authentication AND bug")
1637 .await
1638 .unwrap();
1639
1640 assert_eq!(results.len(), 1);
1641 assert!(results[0]
1642 .task
1643 .spec
1644 .as_ref()
1645 .unwrap()
1646 .contains("authentication"));
1647 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1648 }
1649
1650 #[tokio::test]
1651 async fn test_search_tasks_no_results() {
1652 let ctx = TestContext::new().await;
1653 let manager = TaskManager::new(ctx.pool());
1654
1655 manager
1657 .add_task("Task 1", Some("Some description"), None)
1658 .await
1659 .unwrap();
1660
1661 let results = manager.search_tasks("nonexistent").await.unwrap();
1663
1664 assert_eq!(results.len(), 0);
1665 }
1666
1667 #[tokio::test]
1668 async fn test_search_tasks_snippet_highlighting() {
1669 let ctx = TestContext::new().await;
1670 let manager = TaskManager::new(ctx.pool());
1671
1672 manager
1674 .add_task(
1675 "Test task",
1676 Some("This is a description with the keyword authentication in the middle"),
1677 None,
1678 )
1679 .await
1680 .unwrap();
1681
1682 let results = manager.search_tasks("authentication").await.unwrap();
1684
1685 assert_eq!(results.len(), 1);
1686 assert!(results[0].match_snippet.contains("**authentication**"));
1688 }
1689
1690 #[tokio::test]
1691 async fn test_pick_next_focused_subtask() {
1692 let ctx = TestContext::new().await;
1693 let manager = TaskManager::new(ctx.pool());
1694
1695 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1697 manager.start_task(parent.id, false).await.unwrap();
1698
1699 let subtask1 = manager
1701 .add_task("Subtask 1", None, Some(parent.id))
1702 .await
1703 .unwrap();
1704 let subtask2 = manager
1705 .add_task("Subtask 2", None, Some(parent.id))
1706 .await
1707 .unwrap();
1708
1709 manager
1711 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1712 .await
1713 .unwrap();
1714 manager
1715 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1716 .await
1717 .unwrap();
1718
1719 let response = manager.pick_next().await.unwrap();
1721
1722 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1723 assert!(response.task.is_some());
1724 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1725 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1726 }
1727
1728 #[tokio::test]
1729 async fn test_pick_next_top_level_task() {
1730 let ctx = TestContext::new().await;
1731 let manager = TaskManager::new(ctx.pool());
1732
1733 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1735 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1736
1737 manager
1739 .update_task(task1.id, None, None, None, None, None, Some(5))
1740 .await
1741 .unwrap();
1742 manager
1743 .update_task(task2.id, None, None, None, None, None, Some(3))
1744 .await
1745 .unwrap();
1746
1747 let response = manager.pick_next().await.unwrap();
1749
1750 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1751 assert!(response.task.is_some());
1752 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1753 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1754 }
1755
1756 #[tokio::test]
1757 async fn test_pick_next_no_tasks() {
1758 let ctx = TestContext::new().await;
1759 let manager = TaskManager::new(ctx.pool());
1760
1761 let response = manager.pick_next().await.unwrap();
1763
1764 assert_eq!(response.suggestion_type, "NONE");
1765 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1766 assert!(response.message.is_some());
1767 }
1768
1769 #[tokio::test]
1770 async fn test_pick_next_all_completed() {
1771 let ctx = TestContext::new().await;
1772 let manager = TaskManager::new(ctx.pool());
1773
1774 let task = manager.add_task("Task 1", None, None).await.unwrap();
1776 manager.start_task(task.id, false).await.unwrap();
1777 manager.done_task().await.unwrap();
1778
1779 let response = manager.pick_next().await.unwrap();
1781
1782 assert_eq!(response.suggestion_type, "NONE");
1783 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1784 assert!(response.message.is_some());
1785 }
1786
1787 #[tokio::test]
1788 async fn test_pick_next_no_available_todos() {
1789 let ctx = TestContext::new().await;
1790 let manager = TaskManager::new(ctx.pool());
1791
1792 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1794 manager.start_task(parent.id, false).await.unwrap();
1795
1796 let subtask = manager
1798 .add_task("Subtask", None, Some(parent.id))
1799 .await
1800 .unwrap();
1801 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1803 .bind(subtask.id)
1804 .execute(ctx.pool())
1805 .await
1806 .unwrap();
1807
1808 sqlx::query(
1810 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
1811 )
1812 .bind(subtask.id.to_string())
1813 .execute(ctx.pool())
1814 .await
1815 .unwrap();
1816
1817 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1819 .bind(parent.id)
1820 .execute(ctx.pool())
1821 .await
1822 .unwrap();
1823
1824 let response = manager.pick_next().await.unwrap();
1826
1827 assert_eq!(response.suggestion_type, "NONE");
1828 assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
1829 assert!(response.message.is_some());
1830 }
1831
1832 #[tokio::test]
1833 async fn test_pick_next_priority_ordering() {
1834 let ctx = TestContext::new().await;
1835 let manager = TaskManager::new(ctx.pool());
1836
1837 let parent = manager.add_task("Parent", None, None).await.unwrap();
1839 manager.start_task(parent.id, false).await.unwrap();
1840
1841 let sub1 = manager
1843 .add_task("Priority 10", None, Some(parent.id))
1844 .await
1845 .unwrap();
1846 manager
1847 .update_task(sub1.id, None, None, None, None, None, Some(10))
1848 .await
1849 .unwrap();
1850
1851 let sub2 = manager
1852 .add_task("Priority 1", None, Some(parent.id))
1853 .await
1854 .unwrap();
1855 manager
1856 .update_task(sub2.id, None, None, None, None, None, Some(1))
1857 .await
1858 .unwrap();
1859
1860 let sub3 = manager
1861 .add_task("Priority 5", None, Some(parent.id))
1862 .await
1863 .unwrap();
1864 manager
1865 .update_task(sub3.id, None, None, None, None, None, Some(5))
1866 .await
1867 .unwrap();
1868
1869 let response = manager.pick_next().await.unwrap();
1871
1872 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1873 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
1874 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
1875 }
1876
1877 #[tokio::test]
1878 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
1879 let ctx = TestContext::new().await;
1880 let manager = TaskManager::new(ctx.pool());
1881
1882 let parent = manager.add_task("Parent", None, None).await.unwrap();
1884 manager.start_task(parent.id, false).await.unwrap();
1885
1886 let top_level = manager
1888 .add_task("Top level task", None, None)
1889 .await
1890 .unwrap();
1891
1892 let response = manager.pick_next().await.unwrap();
1894
1895 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1896 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
1897 }
1898
1899 #[tokio::test]
1902 async fn test_get_task_with_events() {
1903 let ctx = TestContext::new().await;
1904 let task_mgr = TaskManager::new(ctx.pool());
1905 let event_mgr = EventManager::new(ctx.pool());
1906
1907 let task = task_mgr.add_task("Test", None, None).await.unwrap();
1908
1909 event_mgr
1911 .add_event(task.id, "progress", "Event 1")
1912 .await
1913 .unwrap();
1914 event_mgr
1915 .add_event(task.id, "decision", "Event 2")
1916 .await
1917 .unwrap();
1918
1919 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1920
1921 assert_eq!(result.task.id, task.id);
1922 assert!(result.events_summary.is_some());
1923
1924 let summary = result.events_summary.unwrap();
1925 assert_eq!(summary.total_count, 2);
1926 assert_eq!(summary.recent_events.len(), 2);
1927 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
1929 }
1930
1931 #[tokio::test]
1932 async fn test_get_task_with_events_nonexistent() {
1933 let ctx = TestContext::new().await;
1934 let task_mgr = TaskManager::new(ctx.pool());
1935
1936 let result = task_mgr.get_task_with_events(999).await;
1937 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1938 }
1939
1940 #[tokio::test]
1941 async fn test_get_task_with_many_events() {
1942 let ctx = TestContext::new().await;
1943 let task_mgr = TaskManager::new(ctx.pool());
1944 let event_mgr = EventManager::new(ctx.pool());
1945
1946 let task = task_mgr.add_task("Test", None, None).await.unwrap();
1947
1948 for i in 0..20 {
1950 event_mgr
1951 .add_event(task.id, "test", &format!("Event {}", i))
1952 .await
1953 .unwrap();
1954 }
1955
1956 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1957 let summary = result.events_summary.unwrap();
1958
1959 assert_eq!(summary.total_count, 20);
1960 assert_eq!(summary.recent_events.len(), 10); }
1962
1963 #[tokio::test]
1964 async fn test_get_task_with_no_events() {
1965 let ctx = TestContext::new().await;
1966 let task_mgr = TaskManager::new(ctx.pool());
1967
1968 let task = task_mgr.add_task("Test", None, None).await.unwrap();
1969
1970 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1971 let summary = result.events_summary.unwrap();
1972
1973 assert_eq!(summary.total_count, 0);
1974 assert_eq!(summary.recent_events.len(), 0);
1975 }
1976
1977 #[tokio::test]
1978 async fn test_pick_next_tasks_zero_capacity() {
1979 let ctx = TestContext::new().await;
1980 let task_mgr = TaskManager::new(ctx.pool());
1981
1982 task_mgr.add_task("Task 1", None, None).await.unwrap();
1983
1984 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
1986 assert_eq!(results.len(), 0);
1987 }
1988
1989 #[tokio::test]
1990 async fn test_pick_next_tasks_capacity_exceeds_available() {
1991 let ctx = TestContext::new().await;
1992 let task_mgr = TaskManager::new(ctx.pool());
1993
1994 task_mgr.add_task("Task 1", None, None).await.unwrap();
1995 task_mgr.add_task("Task 2", None, None).await.unwrap();
1996
1997 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
1999 assert_eq!(results.len(), 2); }
2001}