1use crate::db::models::{
2 CurrentTaskInfo, DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, ParentTaskInfo,
3 PickNextResponse, PreviousTaskInfo, SpawnSubtaskResponse, SubtaskInfo, SwitchTaskResponse,
4 Task, TaskContext, 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 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
86 let task = self.get_task(id).await?;
88
89 let mut ancestors = Vec::new();
91 let mut current_parent_id = task.parent_id;
92
93 while let Some(parent_id) = current_parent_id {
94 let parent = self.get_task(parent_id).await?;
95 current_parent_id = parent.parent_id;
96 ancestors.push(parent);
97 }
98
99 let siblings = if let Some(parent_id) = task.parent_id {
101 sqlx::query_as::<_, Task>(
102 r#"
103 SELECT id, parent_id, name, spec, status, complexity, priority,
104 first_todo_at, first_doing_at, first_done_at
105 FROM tasks
106 WHERE parent_id = ? AND id != ?
107 ORDER BY priority ASC NULLS LAST, id ASC
108 "#,
109 )
110 .bind(parent_id)
111 .bind(id)
112 .fetch_all(self.pool)
113 .await?
114 } else {
115 sqlx::query_as::<_, Task>(
117 r#"
118 SELECT id, parent_id, name, spec, status, complexity, priority,
119 first_todo_at, first_doing_at, first_done_at
120 FROM tasks
121 WHERE parent_id IS NULL AND id != ?
122 ORDER BY priority ASC NULLS LAST, id ASC
123 "#,
124 )
125 .bind(id)
126 .fetch_all(self.pool)
127 .await?
128 };
129
130 let children = sqlx::query_as::<_, Task>(
132 r#"
133 SELECT id, parent_id, name, spec, status, complexity, priority,
134 first_todo_at, first_doing_at, first_done_at
135 FROM tasks
136 WHERE parent_id = ?
137 ORDER BY priority ASC NULLS LAST, id ASC
138 "#,
139 )
140 .bind(id)
141 .fetch_all(self.pool)
142 .await?;
143
144 Ok(TaskContext {
145 task,
146 ancestors,
147 siblings,
148 children,
149 })
150 }
151
152 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
154 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
155 .bind(task_id)
156 .fetch_one(self.pool)
157 .await?;
158
159 let recent_events = sqlx::query_as::<_, Event>(
160 r#"
161 SELECT id, task_id, timestamp, log_type, discussion_data
162 FROM events
163 WHERE task_id = ?
164 ORDER BY timestamp DESC
165 LIMIT 10
166 "#,
167 )
168 .bind(task_id)
169 .fetch_all(self.pool)
170 .await?;
171
172 Ok(EventsSummary {
173 total_count,
174 recent_events,
175 })
176 }
177
178 #[allow(clippy::too_many_arguments)]
180 pub async fn update_task(
181 &self,
182 id: i64,
183 name: Option<&str>,
184 spec: Option<&str>,
185 parent_id: Option<Option<i64>>,
186 status: Option<&str>,
187 complexity: Option<i32>,
188 priority: Option<i32>,
189 ) -> Result<Task> {
190 let task = self.get_task(id).await?;
192
193 if let Some(s) = status {
195 if !["todo", "doing", "done"].contains(&s) {
196 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
197 }
198 }
199
200 if let Some(Some(pid)) = parent_id {
202 if pid == id {
203 return Err(IntentError::CircularDependency);
204 }
205 self.check_task_exists(pid).await?;
206 self.check_circular_dependency(id, pid).await?;
207 }
208
209 let mut query = String::from("UPDATE tasks SET ");
211 let mut updates = Vec::new();
212
213 if let Some(n) = name {
214 updates.push(format!("name = '{}'", n.replace('\'', "''")));
215 }
216
217 if let Some(s) = spec {
218 updates.push(format!("spec = '{}'", s.replace('\'', "''")));
219 }
220
221 if let Some(pid) = parent_id {
222 match pid {
223 Some(p) => updates.push(format!("parent_id = {}", p)),
224 None => updates.push("parent_id = NULL".to_string()),
225 }
226 }
227
228 if let Some(c) = complexity {
229 updates.push(format!("complexity = {}", c));
230 }
231
232 if let Some(p) = priority {
233 updates.push(format!("priority = {}", p));
234 }
235
236 if let Some(s) = status {
237 updates.push(format!("status = '{}'", s));
238
239 let now = Utc::now();
241 match s {
242 "todo" if task.first_todo_at.is_none() => {
243 updates.push(format!("first_todo_at = '{}'", now.to_rfc3339()));
244 },
245 "doing" if task.first_doing_at.is_none() => {
246 updates.push(format!("first_doing_at = '{}'", now.to_rfc3339()));
247 },
248 "done" if task.first_done_at.is_none() => {
249 updates.push(format!("first_done_at = '{}'", now.to_rfc3339()));
250 },
251 _ => {},
252 }
253 }
254
255 if updates.is_empty() {
256 return Ok(task);
257 }
258
259 query.push_str(&updates.join(", "));
260 query.push_str(&format!(" WHERE id = {}", id));
261
262 sqlx::query(&query).execute(self.pool).await?;
263
264 self.get_task(id).await
265 }
266
267 pub async fn delete_task(&self, id: i64) -> Result<()> {
269 self.check_task_exists(id).await?;
270
271 sqlx::query("DELETE FROM tasks WHERE id = ?")
272 .bind(id)
273 .execute(self.pool)
274 .await?;
275
276 Ok(())
277 }
278
279 pub async fn find_tasks(
281 &self,
282 status: Option<&str>,
283 parent_id: Option<Option<i64>>,
284 ) -> Result<Vec<Task>> {
285 let mut query = String::from(
286 "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"
287 );
288 let mut conditions = Vec::new();
289
290 if let Some(s) = status {
291 query.push_str(" AND status = ?");
292 conditions.push(s.to_string());
293 }
294
295 if let Some(pid) = parent_id {
296 if let Some(p) = pid {
297 query.push_str(" AND parent_id = ?");
298 conditions.push(p.to_string());
299 } else {
300 query.push_str(" AND parent_id IS NULL");
301 }
302 }
303
304 query.push_str(" ORDER BY id");
305
306 let mut q = sqlx::query_as::<_, Task>(&query);
307 for cond in conditions {
308 q = q.bind(cond);
309 }
310
311 let tasks = q.fetch_all(self.pool).await?;
312 Ok(tasks)
313 }
314
315 pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
318 let escaped_query = self.escape_fts_query(query);
320
321 let results = sqlx::query(
325 r#"
326 SELECT
327 t.id,
328 t.parent_id,
329 t.name,
330 t.spec,
331 t.status,
332 t.complexity,
333 t.priority,
334 t.first_todo_at,
335 t.first_doing_at,
336 t.first_done_at,
337 COALESCE(
338 snippet(tasks_fts, 1, '**', '**', '...', 15),
339 snippet(tasks_fts, 0, '**', '**', '...', 15)
340 ) as match_snippet
341 FROM tasks_fts
342 INNER JOIN tasks t ON tasks_fts.rowid = t.id
343 WHERE tasks_fts MATCH ?
344 ORDER BY rank
345 "#,
346 )
347 .bind(&escaped_query)
348 .fetch_all(self.pool)
349 .await?;
350
351 let mut search_results = Vec::new();
352 for row in results {
353 let task = Task {
354 id: row.get("id"),
355 parent_id: row.get("parent_id"),
356 name: row.get("name"),
357 spec: row.get("spec"),
358 status: row.get("status"),
359 complexity: row.get("complexity"),
360 priority: row.get("priority"),
361 first_todo_at: row.get("first_todo_at"),
362 first_doing_at: row.get("first_doing_at"),
363 first_done_at: row.get("first_done_at"),
364 };
365 let match_snippet: String = row.get("match_snippet");
366
367 search_results.push(TaskSearchResult {
368 task,
369 match_snippet,
370 });
371 }
372
373 Ok(search_results)
374 }
375
376 fn escape_fts_query(&self, query: &str) -> String {
378 query.replace('"', "\"\"")
382 }
383
384 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
386 let mut tx = self.pool.begin().await?;
387
388 let now = Utc::now();
389
390 sqlx::query(
392 r#"
393 UPDATE tasks
394 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
395 WHERE id = ?
396 "#,
397 )
398 .bind(now)
399 .bind(id)
400 .execute(&mut *tx)
401 .await?;
402
403 sqlx::query(
405 r#"
406 INSERT OR REPLACE INTO workspace_state (key, value)
407 VALUES ('current_task_id', ?)
408 "#,
409 )
410 .bind(id.to_string())
411 .execute(&mut *tx)
412 .await?;
413
414 tx.commit().await?;
415
416 if with_events {
417 self.get_task_with_events(id).await
418 } else {
419 let task = self.get_task(id).await?;
420 Ok(TaskWithEvents {
421 task,
422 events_summary: None,
423 })
424 }
425 }
426
427 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
431 let mut tx = self.pool.begin().await?;
432
433 let current_task_id: Option<String> =
435 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
436 .fetch_optional(&mut *tx)
437 .await?;
438
439 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
440 IntentError::InvalidInput(
441 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
442 ),
443 )?;
444
445 let task_info: (String, Option<i64>) =
447 sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
448 .bind(id)
449 .fetch_one(&mut *tx)
450 .await?;
451 let (task_name, parent_id) = task_info;
452
453 let uncompleted_children: i64 = sqlx::query_scalar(
455 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
456 )
457 .bind(id)
458 .fetch_one(&mut *tx)
459 .await?;
460
461 if uncompleted_children > 0 {
462 return Err(IntentError::UncompletedChildren);
463 }
464
465 let now = Utc::now();
466
467 sqlx::query(
469 r#"
470 UPDATE tasks
471 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
472 WHERE id = ?
473 "#,
474 )
475 .bind(now)
476 .bind(id)
477 .execute(&mut *tx)
478 .await?;
479
480 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
482 .execute(&mut *tx)
483 .await?;
484
485 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
487 let remaining_siblings: i64 = sqlx::query_scalar(
489 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
490 )
491 .bind(parent_task_id)
492 .bind(id)
493 .fetch_one(&mut *tx)
494 .await?;
495
496 if remaining_siblings == 0 {
497 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
499 .bind(parent_task_id)
500 .fetch_one(&mut *tx)
501 .await?;
502
503 NextStepSuggestion::ParentIsReady {
504 message: format!(
505 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
506 parent_task_id, parent_name
507 ),
508 parent_task_id,
509 parent_task_name: parent_name,
510 }
511 } else {
512 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
514 .bind(parent_task_id)
515 .fetch_one(&mut *tx)
516 .await?;
517
518 NextStepSuggestion::SiblingTasksRemain {
519 message: format!(
520 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
521 id, parent_task_id, parent_name
522 ),
523 parent_task_id,
524 parent_task_name: parent_name,
525 remaining_siblings_count: remaining_siblings,
526 }
527 }
528 } else {
529 let child_count: i64 =
531 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
532 .bind(id)
533 .fetch_one(&mut *tx)
534 .await?;
535
536 if child_count > 0 {
537 NextStepSuggestion::TopLevelTaskCompleted {
539 message: format!(
540 "Top-level task #{} '{}' has been completed. Well done!",
541 id, task_name
542 ),
543 completed_task_id: id,
544 completed_task_name: task_name.clone(),
545 }
546 } else {
547 let remaining_tasks: i64 = sqlx::query_scalar(
549 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
550 )
551 .bind(id)
552 .fetch_one(&mut *tx)
553 .await?;
554
555 if remaining_tasks == 0 {
556 NextStepSuggestion::WorkspaceIsClear {
557 message: format!(
558 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
559 id
560 ),
561 completed_task_id: id,
562 }
563 } else {
564 NextStepSuggestion::NoParentContext {
565 message: format!("Task #{} '{}' has been completed.", id, task_name),
566 completed_task_id: id,
567 completed_task_name: task_name.clone(),
568 }
569 }
570 }
571 };
572
573 tx.commit().await?;
574
575 let completed_task = self.get_task(id).await?;
576
577 Ok(DoneTaskResponse {
578 completed_task,
579 workspace_status: WorkspaceStatus {
580 current_task_id: None,
581 },
582 next_step_suggestion,
583 })
584 }
585
586 async fn check_task_exists(&self, id: i64) -> Result<()> {
588 let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
589 .bind(id)
590 .fetch_one(self.pool)
591 .await?;
592
593 if !exists {
594 return Err(IntentError::TaskNotFound(id));
595 }
596
597 Ok(())
598 }
599
600 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
602 let mut current_id = new_parent_id;
603
604 loop {
605 if current_id == task_id {
606 return Err(IntentError::CircularDependency);
607 }
608
609 let parent: Option<i64> =
610 sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
611 .bind(current_id)
612 .fetch_optional(self.pool)
613 .await?;
614
615 match parent {
616 Some(pid) => current_id = pid,
617 None => break,
618 }
619 }
620
621 Ok(())
622 }
623
624 pub async fn switch_to_task(&self, id: i64) -> Result<SwitchTaskResponse> {
628 self.check_task_exists(id).await?;
630
631 let mut tx = self.pool.begin().await?;
632 let now = Utc::now();
633
634 let current_task_id: Option<String> =
636 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
637 .fetch_optional(&mut *tx)
638 .await?;
639
640 let previous_task = if let Some(prev_id_str) = current_task_id {
641 if let Ok(prev_id) = prev_id_str.parse::<i64>() {
642 sqlx::query(
644 r#"
645 UPDATE tasks
646 SET status = 'todo'
647 WHERE id = ? AND status = 'doing'
648 "#,
649 )
650 .bind(prev_id)
651 .execute(&mut *tx)
652 .await?;
653
654 Some(PreviousTaskInfo {
655 id: prev_id,
656 status: "todo".to_string(),
657 })
658 } else {
659 None
660 }
661 } else {
662 None
663 };
664
665 sqlx::query(
667 r#"
668 UPDATE tasks
669 SET status = 'doing',
670 first_doing_at = COALESCE(first_doing_at, ?)
671 WHERE id = ? AND status != 'doing'
672 "#,
673 )
674 .bind(now)
675 .bind(id)
676 .execute(&mut *tx)
677 .await?;
678
679 let (task_name, task_status): (String, String) =
681 sqlx::query_as("SELECT name, status FROM tasks WHERE id = ?")
682 .bind(id)
683 .fetch_one(&mut *tx)
684 .await?;
685
686 sqlx::query(
688 r#"
689 INSERT OR REPLACE INTO workspace_state (key, value)
690 VALUES ('current_task_id', ?)
691 "#,
692 )
693 .bind(id.to_string())
694 .execute(&mut *tx)
695 .await?;
696
697 tx.commit().await?;
698
699 Ok(SwitchTaskResponse {
700 previous_task,
701 current_task: CurrentTaskInfo {
702 id,
703 name: task_name,
704 status: task_status,
705 },
706 })
707 }
708
709 pub async fn spawn_subtask(
713 &self,
714 name: &str,
715 spec: Option<&str>,
716 ) -> Result<SpawnSubtaskResponse> {
717 let current_task_id: Option<String> =
719 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
720 .fetch_optional(self.pool)
721 .await?;
722
723 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
724 IntentError::InvalidInput("No current task to create subtask under".to_string()),
725 )?;
726
727 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
729 .bind(parent_id)
730 .fetch_one(self.pool)
731 .await?;
732
733 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
735
736 self.switch_to_task(subtask.id).await?;
738
739 Ok(SpawnSubtaskResponse {
740 subtask: SubtaskInfo {
741 id: subtask.id,
742 name: subtask.name,
743 parent_id,
744 status: "doing".to_string(),
745 },
746 parent_task: ParentTaskInfo {
747 id: parent_id,
748 name: parent_name,
749 },
750 })
751 }
752
753 pub async fn pick_next_tasks(
766 &self,
767 max_count: usize,
768 capacity_limit: usize,
769 ) -> Result<Vec<Task>> {
770 let mut tx = self.pool.begin().await?;
771
772 let doing_count: i64 =
774 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
775 .fetch_one(&mut *tx)
776 .await?;
777
778 let available = capacity_limit.saturating_sub(doing_count as usize);
780 if available == 0 {
781 return Ok(vec![]);
782 }
783
784 let limit = std::cmp::min(max_count, available);
785
786 let todo_tasks = sqlx::query_as::<_, Task>(
788 r#"
789 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
790 FROM tasks
791 WHERE status = 'todo'
792 ORDER BY
793 COALESCE(priority, 0) DESC,
794 COALESCE(complexity, 5) ASC,
795 id ASC
796 LIMIT ?
797 "#,
798 )
799 .bind(limit as i64)
800 .fetch_all(&mut *tx)
801 .await?;
802
803 if todo_tasks.is_empty() {
804 return Ok(vec![]);
805 }
806
807 let now = Utc::now();
808
809 for task in &todo_tasks {
811 sqlx::query(
812 r#"
813 UPDATE tasks
814 SET status = 'doing',
815 first_doing_at = COALESCE(first_doing_at, ?)
816 WHERE id = ?
817 "#,
818 )
819 .bind(now)
820 .bind(task.id)
821 .execute(&mut *tx)
822 .await?;
823 }
824
825 tx.commit().await?;
826
827 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
829 let placeholders = vec!["?"; task_ids.len()].join(",");
830 let query = format!(
831 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
832 FROM tasks WHERE id IN ({})
833 ORDER BY
834 COALESCE(priority, 0) DESC,
835 COALESCE(complexity, 5) ASC,
836 id ASC",
837 placeholders
838 );
839
840 let mut q = sqlx::query_as::<_, Task>(&query);
841 for id in task_ids {
842 q = q.bind(id);
843 }
844
845 let updated_tasks = q.fetch_all(self.pool).await?;
846 Ok(updated_tasks)
847 }
848
849 pub async fn pick_next(&self) -> Result<PickNextResponse> {
858 let current_task_id: Option<String> =
860 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
861 .fetch_optional(self.pool)
862 .await?;
863
864 if let Some(current_id_str) = current_task_id {
865 if let Ok(current_id) = current_id_str.parse::<i64>() {
866 let subtasks = sqlx::query_as::<_, Task>(
868 r#"
869 SELECT id, parent_id, name, spec, status, complexity, priority,
870 first_todo_at, first_doing_at, first_done_at
871 FROM tasks
872 WHERE parent_id = ? AND status = 'todo'
873 ORDER BY COALESCE(priority, 999999) ASC, id ASC
874 LIMIT 1
875 "#,
876 )
877 .bind(current_id)
878 .fetch_optional(self.pool)
879 .await?;
880
881 if let Some(task) = subtasks {
882 return Ok(PickNextResponse::focused_subtask(task));
883 }
884 }
885 }
886
887 let top_level_task = sqlx::query_as::<_, Task>(
889 r#"
890 SELECT id, parent_id, name, spec, status, complexity, priority,
891 first_todo_at, first_doing_at, first_done_at
892 FROM tasks
893 WHERE parent_id IS NULL AND status = 'todo'
894 ORDER BY COALESCE(priority, 999999) ASC, id ASC
895 LIMIT 1
896 "#,
897 )
898 .fetch_optional(self.pool)
899 .await?;
900
901 if let Some(task) = top_level_task {
902 return Ok(PickNextResponse::top_level_task(task));
903 }
904
905 let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
908 .fetch_one(self.pool)
909 .await?;
910
911 if total_tasks == 0 {
912 return Ok(PickNextResponse::no_tasks_in_project());
913 }
914
915 let todo_or_doing_count: i64 =
917 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
918 .fetch_one(self.pool)
919 .await?;
920
921 if todo_or_doing_count == 0 {
922 return Ok(PickNextResponse::all_tasks_completed());
923 }
924
925 Ok(PickNextResponse::no_available_todos())
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use crate::events::EventManager;
934 use crate::test_utils::test_helpers::TestContext;
935
936 #[tokio::test]
937 async fn test_add_task() {
938 let ctx = TestContext::new().await;
939 let manager = TaskManager::new(ctx.pool());
940
941 let task = manager.add_task("Test task", None, None).await.unwrap();
942
943 assert_eq!(task.name, "Test task");
944 assert_eq!(task.status, "todo");
945 assert!(task.first_todo_at.is_some());
946 assert!(task.first_doing_at.is_none());
947 assert!(task.first_done_at.is_none());
948 }
949
950 #[tokio::test]
951 async fn test_add_task_with_spec() {
952 let ctx = TestContext::new().await;
953 let manager = TaskManager::new(ctx.pool());
954
955 let spec = "This is a task specification";
956 let task = manager
957 .add_task("Test task", Some(spec), None)
958 .await
959 .unwrap();
960
961 assert_eq!(task.name, "Test task");
962 assert_eq!(task.spec.as_deref(), Some(spec));
963 }
964
965 #[tokio::test]
966 async fn test_add_task_with_parent() {
967 let ctx = TestContext::new().await;
968 let manager = TaskManager::new(ctx.pool());
969
970 let parent = manager.add_task("Parent task", None, None).await.unwrap();
971 let child = manager
972 .add_task("Child task", None, Some(parent.id))
973 .await
974 .unwrap();
975
976 assert_eq!(child.parent_id, Some(parent.id));
977 }
978
979 #[tokio::test]
980 async fn test_get_task() {
981 let ctx = TestContext::new().await;
982 let manager = TaskManager::new(ctx.pool());
983
984 let created = manager.add_task("Test task", None, None).await.unwrap();
985 let retrieved = manager.get_task(created.id).await.unwrap();
986
987 assert_eq!(created.id, retrieved.id);
988 assert_eq!(created.name, retrieved.name);
989 }
990
991 #[tokio::test]
992 async fn test_get_task_not_found() {
993 let ctx = TestContext::new().await;
994 let manager = TaskManager::new(ctx.pool());
995
996 let result = manager.get_task(999).await;
997 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
998 }
999
1000 #[tokio::test]
1001 async fn test_update_task_name() {
1002 let ctx = TestContext::new().await;
1003 let manager = TaskManager::new(ctx.pool());
1004
1005 let task = manager.add_task("Original name", None, None).await.unwrap();
1006 let updated = manager
1007 .update_task(task.id, Some("New name"), None, None, None, None, None)
1008 .await
1009 .unwrap();
1010
1011 assert_eq!(updated.name, "New name");
1012 }
1013
1014 #[tokio::test]
1015 async fn test_update_task_status() {
1016 let ctx = TestContext::new().await;
1017 let manager = TaskManager::new(ctx.pool());
1018
1019 let task = manager.add_task("Test task", None, None).await.unwrap();
1020 let updated = manager
1021 .update_task(task.id, None, None, None, Some("doing"), None, None)
1022 .await
1023 .unwrap();
1024
1025 assert_eq!(updated.status, "doing");
1026 assert!(updated.first_doing_at.is_some());
1027 }
1028
1029 #[tokio::test]
1030 async fn test_delete_task() {
1031 let ctx = TestContext::new().await;
1032 let manager = TaskManager::new(ctx.pool());
1033
1034 let task = manager.add_task("Test task", None, None).await.unwrap();
1035 manager.delete_task(task.id).await.unwrap();
1036
1037 let result = manager.get_task(task.id).await;
1038 assert!(result.is_err());
1039 }
1040
1041 #[tokio::test]
1042 async fn test_find_tasks_by_status() {
1043 let ctx = TestContext::new().await;
1044 let manager = TaskManager::new(ctx.pool());
1045
1046 manager.add_task("Todo task", None, None).await.unwrap();
1047 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1048 manager
1049 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1050 .await
1051 .unwrap();
1052
1053 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1054 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1055
1056 assert_eq!(todo_tasks.len(), 1);
1057 assert_eq!(doing_tasks.len(), 1);
1058 assert_eq!(doing_tasks[0].status, "doing");
1059 }
1060
1061 #[tokio::test]
1062 async fn test_find_tasks_by_parent() {
1063 let ctx = TestContext::new().await;
1064 let manager = TaskManager::new(ctx.pool());
1065
1066 let parent = manager.add_task("Parent", None, None).await.unwrap();
1067 manager
1068 .add_task("Child 1", None, Some(parent.id))
1069 .await
1070 .unwrap();
1071 manager
1072 .add_task("Child 2", None, Some(parent.id))
1073 .await
1074 .unwrap();
1075
1076 let children = manager
1077 .find_tasks(None, Some(Some(parent.id)))
1078 .await
1079 .unwrap();
1080
1081 assert_eq!(children.len(), 2);
1082 }
1083
1084 #[tokio::test]
1085 async fn test_start_task() {
1086 let ctx = TestContext::new().await;
1087 let manager = TaskManager::new(ctx.pool());
1088
1089 let task = manager.add_task("Test task", None, None).await.unwrap();
1090 let started = manager.start_task(task.id, false).await.unwrap();
1091
1092 assert_eq!(started.task.status, "doing");
1093 assert!(started.task.first_doing_at.is_some());
1094
1095 let current: Option<String> =
1097 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1098 .fetch_optional(ctx.pool())
1099 .await
1100 .unwrap();
1101
1102 assert_eq!(current, Some(task.id.to_string()));
1103 }
1104
1105 #[tokio::test]
1106 async fn test_start_task_with_events() {
1107 let ctx = TestContext::new().await;
1108 let manager = TaskManager::new(ctx.pool());
1109
1110 let task = manager.add_task("Test task", None, None).await.unwrap();
1111
1112 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1114 .bind(task.id)
1115 .bind("test")
1116 .bind("test event")
1117 .execute(ctx.pool())
1118 .await
1119 .unwrap();
1120
1121 let started = manager.start_task(task.id, true).await.unwrap();
1122
1123 assert!(started.events_summary.is_some());
1124 let summary = started.events_summary.unwrap();
1125 assert_eq!(summary.total_count, 1);
1126 }
1127
1128 #[tokio::test]
1129 async fn test_done_task() {
1130 let ctx = TestContext::new().await;
1131 let manager = TaskManager::new(ctx.pool());
1132
1133 let task = manager.add_task("Test task", None, None).await.unwrap();
1134 manager.start_task(task.id, false).await.unwrap();
1135 let response = manager.done_task().await.unwrap();
1136
1137 assert_eq!(response.completed_task.status, "done");
1138 assert!(response.completed_task.first_done_at.is_some());
1139 assert_eq!(response.workspace_status.current_task_id, None);
1140
1141 match response.next_step_suggestion {
1143 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1144 _ => panic!("Expected WorkspaceIsClear suggestion"),
1145 }
1146
1147 let current: Option<String> =
1149 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1150 .fetch_optional(ctx.pool())
1151 .await
1152 .unwrap();
1153
1154 assert!(current.is_none());
1155 }
1156
1157 #[tokio::test]
1158 async fn test_done_task_with_uncompleted_children() {
1159 let ctx = TestContext::new().await;
1160 let manager = TaskManager::new(ctx.pool());
1161
1162 let parent = manager.add_task("Parent", None, None).await.unwrap();
1163 manager
1164 .add_task("Child", None, Some(parent.id))
1165 .await
1166 .unwrap();
1167
1168 manager.start_task(parent.id, false).await.unwrap();
1170
1171 let result = manager.done_task().await;
1172 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1173 }
1174
1175 #[tokio::test]
1176 async fn test_done_task_with_completed_children() {
1177 let ctx = TestContext::new().await;
1178 let manager = TaskManager::new(ctx.pool());
1179
1180 let parent = manager.add_task("Parent", None, None).await.unwrap();
1181 let child = manager
1182 .add_task("Child", None, Some(parent.id))
1183 .await
1184 .unwrap();
1185
1186 manager.start_task(child.id, false).await.unwrap();
1188 let child_response = manager.done_task().await.unwrap();
1189
1190 match child_response.next_step_suggestion {
1192 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1193 assert_eq!(parent_task_id, parent.id);
1194 },
1195 _ => panic!("Expected ParentIsReady suggestion"),
1196 }
1197
1198 manager.start_task(parent.id, false).await.unwrap();
1200 let parent_response = manager.done_task().await.unwrap();
1201 assert_eq!(parent_response.completed_task.status, "done");
1202
1203 match parent_response.next_step_suggestion {
1205 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1206 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1207 }
1208 }
1209
1210 #[tokio::test]
1211 async fn test_circular_dependency() {
1212 let ctx = TestContext::new().await;
1213 let manager = TaskManager::new(ctx.pool());
1214
1215 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1216 let task2 = manager
1217 .add_task("Task 2", None, Some(task1.id))
1218 .await
1219 .unwrap();
1220
1221 let result = manager
1223 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1224 .await;
1225
1226 assert!(matches!(result, Err(IntentError::CircularDependency)));
1227 }
1228
1229 #[tokio::test]
1230 async fn test_invalid_parent_id() {
1231 let ctx = TestContext::new().await;
1232 let manager = TaskManager::new(ctx.pool());
1233
1234 let result = manager.add_task("Test", None, Some(999)).await;
1235 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1236 }
1237
1238 #[tokio::test]
1239 async fn test_update_task_complexity_and_priority() {
1240 let ctx = TestContext::new().await;
1241 let manager = TaskManager::new(ctx.pool());
1242
1243 let task = manager.add_task("Test task", None, None).await.unwrap();
1244 let updated = manager
1245 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1246 .await
1247 .unwrap();
1248
1249 assert_eq!(updated.complexity, Some(8));
1250 assert_eq!(updated.priority, Some(10));
1251 }
1252
1253 #[tokio::test]
1254 async fn test_switch_to_task() {
1255 let ctx = TestContext::new().await;
1256 let manager = TaskManager::new(ctx.pool());
1257
1258 let task = manager.add_task("Test task", None, None).await.unwrap();
1260 assert_eq!(task.status, "todo");
1261
1262 let response = manager.switch_to_task(task.id).await.unwrap();
1264 assert_eq!(response.current_task.id, task.id);
1265 assert_eq!(response.current_task.status, "doing");
1266 assert!(response.previous_task.is_none());
1267
1268 let current: Option<String> =
1270 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1271 .fetch_optional(ctx.pool())
1272 .await
1273 .unwrap();
1274
1275 assert_eq!(current, Some(task.id.to_string()));
1276 }
1277
1278 #[tokio::test]
1279 async fn test_switch_to_task_already_doing() {
1280 let ctx = TestContext::new().await;
1281 let manager = TaskManager::new(ctx.pool());
1282
1283 let task = manager.add_task("Test task", None, None).await.unwrap();
1285 manager.start_task(task.id, false).await.unwrap();
1286
1287 let response = manager.switch_to_task(task.id).await.unwrap();
1289 assert_eq!(response.current_task.id, task.id);
1290 assert_eq!(response.current_task.status, "doing");
1291 }
1292
1293 #[tokio::test]
1294 async fn test_spawn_subtask() {
1295 let ctx = TestContext::new().await;
1296 let manager = TaskManager::new(ctx.pool());
1297
1298 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1300 manager.start_task(parent.id, false).await.unwrap();
1301
1302 let response = manager
1304 .spawn_subtask("Child task", Some("Details"))
1305 .await
1306 .unwrap();
1307
1308 assert_eq!(response.subtask.parent_id, parent.id);
1309 assert_eq!(response.subtask.name, "Child task");
1310 assert_eq!(response.subtask.status, "doing");
1311 assert_eq!(response.parent_task.id, parent.id);
1312 assert_eq!(response.parent_task.name, "Parent task");
1313
1314 let current: Option<String> =
1316 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1317 .fetch_optional(ctx.pool())
1318 .await
1319 .unwrap();
1320
1321 assert_eq!(current, Some(response.subtask.id.to_string()));
1322
1323 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1325 assert_eq!(retrieved.status, "doing");
1326 }
1327
1328 #[tokio::test]
1329 async fn test_spawn_subtask_no_current_task() {
1330 let ctx = TestContext::new().await;
1331 let manager = TaskManager::new(ctx.pool());
1332
1333 let result = manager.spawn_subtask("Child", None).await;
1335 assert!(result.is_err());
1336 }
1337
1338 #[tokio::test]
1339 async fn test_pick_next_tasks_basic() {
1340 let ctx = TestContext::new().await;
1341 let manager = TaskManager::new(ctx.pool());
1342
1343 for i in 1..=10 {
1345 manager
1346 .add_task(&format!("Task {}", i), None, None)
1347 .await
1348 .unwrap();
1349 }
1350
1351 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1353
1354 assert_eq!(picked.len(), 5);
1355 for task in &picked {
1356 assert_eq!(task.status, "doing");
1357 assert!(task.first_doing_at.is_some());
1358 }
1359
1360 let doing_count: i64 =
1362 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1363 .fetch_one(ctx.pool())
1364 .await
1365 .unwrap();
1366
1367 assert_eq!(doing_count, 5);
1368 }
1369
1370 #[tokio::test]
1371 async fn test_pick_next_tasks_with_existing_doing() {
1372 let ctx = TestContext::new().await;
1373 let manager = TaskManager::new(ctx.pool());
1374
1375 for i in 1..=10 {
1377 manager
1378 .add_task(&format!("Task {}", i), None, None)
1379 .await
1380 .unwrap();
1381 }
1382
1383 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1385 manager.start_task(tasks[0].id, false).await.unwrap();
1386 manager.start_task(tasks[1].id, false).await.unwrap();
1387
1388 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1390
1391 assert_eq!(picked.len(), 3);
1393
1394 let doing_count: i64 =
1396 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1397 .fetch_one(ctx.pool())
1398 .await
1399 .unwrap();
1400
1401 assert_eq!(doing_count, 5);
1402 }
1403
1404 #[tokio::test]
1405 async fn test_pick_next_tasks_at_capacity() {
1406 let ctx = TestContext::new().await;
1407 let manager = TaskManager::new(ctx.pool());
1408
1409 for i in 1..=10 {
1411 manager
1412 .add_task(&format!("Task {}", i), None, None)
1413 .await
1414 .unwrap();
1415 }
1416
1417 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1419 assert_eq!(first_batch.len(), 5);
1420
1421 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1423 assert_eq!(second_batch.len(), 0);
1424 }
1425
1426 #[tokio::test]
1427 async fn test_pick_next_tasks_priority_ordering() {
1428 let ctx = TestContext::new().await;
1429 let manager = TaskManager::new(ctx.pool());
1430
1431 let low = manager.add_task("Low priority", None, None).await.unwrap();
1433 manager
1434 .update_task(low.id, None, None, None, None, None, Some(1))
1435 .await
1436 .unwrap();
1437
1438 let high = manager.add_task("High priority", None, None).await.unwrap();
1439 manager
1440 .update_task(high.id, None, None, None, None, None, Some(10))
1441 .await
1442 .unwrap();
1443
1444 let medium = manager
1445 .add_task("Medium priority", None, None)
1446 .await
1447 .unwrap();
1448 manager
1449 .update_task(medium.id, None, None, None, None, None, Some(5))
1450 .await
1451 .unwrap();
1452
1453 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1455
1456 assert_eq!(picked.len(), 3);
1458 assert_eq!(picked[0].priority, Some(10)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(1)); }
1462
1463 #[tokio::test]
1464 async fn test_pick_next_tasks_complexity_ordering() {
1465 let ctx = TestContext::new().await;
1466 let manager = TaskManager::new(ctx.pool());
1467
1468 let complex = manager.add_task("Complex", None, None).await.unwrap();
1470 manager
1471 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1472 .await
1473 .unwrap();
1474
1475 let simple = manager.add_task("Simple", None, None).await.unwrap();
1476 manager
1477 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1478 .await
1479 .unwrap();
1480
1481 let medium = manager.add_task("Medium", None, None).await.unwrap();
1482 manager
1483 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1484 .await
1485 .unwrap();
1486
1487 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1489
1490 assert_eq!(picked.len(), 3);
1492 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1496
1497 #[tokio::test]
1498 async fn test_done_task_sibling_tasks_remain() {
1499 let ctx = TestContext::new().await;
1500 let manager = TaskManager::new(ctx.pool());
1501
1502 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1504 let child1 = manager
1505 .add_task("Child 1", None, Some(parent.id))
1506 .await
1507 .unwrap();
1508 let child2 = manager
1509 .add_task("Child 2", None, Some(parent.id))
1510 .await
1511 .unwrap();
1512 let _child3 = manager
1513 .add_task("Child 3", None, Some(parent.id))
1514 .await
1515 .unwrap();
1516
1517 manager.start_task(child1.id, false).await.unwrap();
1519 let response = manager.done_task().await.unwrap();
1520
1521 match response.next_step_suggestion {
1523 NextStepSuggestion::SiblingTasksRemain {
1524 parent_task_id,
1525 remaining_siblings_count,
1526 ..
1527 } => {
1528 assert_eq!(parent_task_id, parent.id);
1529 assert_eq!(remaining_siblings_count, 2); },
1531 _ => panic!("Expected SiblingTasksRemain suggestion"),
1532 }
1533
1534 manager.start_task(child2.id, false).await.unwrap();
1536 let response2 = manager.done_task().await.unwrap();
1537
1538 match response2.next_step_suggestion {
1540 NextStepSuggestion::SiblingTasksRemain {
1541 remaining_siblings_count,
1542 ..
1543 } => {
1544 assert_eq!(remaining_siblings_count, 1); },
1546 _ => panic!("Expected SiblingTasksRemain suggestion"),
1547 }
1548 }
1549
1550 #[tokio::test]
1551 async fn test_done_task_top_level_with_children() {
1552 let ctx = TestContext::new().await;
1553 let manager = TaskManager::new(ctx.pool());
1554
1555 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1557 let child = manager
1558 .add_task("Sub Task", None, Some(parent.id))
1559 .await
1560 .unwrap();
1561
1562 manager.start_task(child.id, false).await.unwrap();
1564 manager.done_task().await.unwrap();
1565
1566 manager.start_task(parent.id, false).await.unwrap();
1568 let response = manager.done_task().await.unwrap();
1569
1570 match response.next_step_suggestion {
1572 NextStepSuggestion::TopLevelTaskCompleted {
1573 completed_task_id,
1574 completed_task_name,
1575 ..
1576 } => {
1577 assert_eq!(completed_task_id, parent.id);
1578 assert_eq!(completed_task_name, "Epic Task");
1579 },
1580 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1581 }
1582 }
1583
1584 #[tokio::test]
1585 async fn test_done_task_no_parent_context() {
1586 let ctx = TestContext::new().await;
1587 let manager = TaskManager::new(ctx.pool());
1588
1589 let task1 = manager
1591 .add_task("Standalone Task 1", None, None)
1592 .await
1593 .unwrap();
1594 let _task2 = manager
1595 .add_task("Standalone Task 2", None, None)
1596 .await
1597 .unwrap();
1598
1599 manager.start_task(task1.id, false).await.unwrap();
1601 let response = manager.done_task().await.unwrap();
1602
1603 match response.next_step_suggestion {
1605 NextStepSuggestion::NoParentContext {
1606 completed_task_id,
1607 completed_task_name,
1608 ..
1609 } => {
1610 assert_eq!(completed_task_id, task1.id);
1611 assert_eq!(completed_task_name, "Standalone Task 1");
1612 },
1613 _ => panic!("Expected NoParentContext suggestion"),
1614 }
1615 }
1616
1617 #[tokio::test]
1618 async fn test_search_tasks_by_name() {
1619 let ctx = TestContext::new().await;
1620 let manager = TaskManager::new(ctx.pool());
1621
1622 manager
1624 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1625 .await
1626 .unwrap();
1627 manager
1628 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1629 .await
1630 .unwrap();
1631 manager
1632 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1633 .await
1634 .unwrap();
1635
1636 let results = manager.search_tasks("authentication").await.unwrap();
1638
1639 assert_eq!(results.len(), 2);
1640 assert!(results[0]
1641 .task
1642 .name
1643 .to_lowercase()
1644 .contains("authentication"));
1645 assert!(results[1]
1646 .task
1647 .name
1648 .to_lowercase()
1649 .contains("authentication"));
1650
1651 assert!(!results[0].match_snippet.is_empty());
1653 }
1654
1655 #[tokio::test]
1656 async fn test_search_tasks_by_spec() {
1657 let ctx = TestContext::new().await;
1658 let manager = TaskManager::new(ctx.pool());
1659
1660 manager
1662 .add_task("Task 1", Some("Implement JWT authentication"), None)
1663 .await
1664 .unwrap();
1665 manager
1666 .add_task("Task 2", Some("Add user registration"), None)
1667 .await
1668 .unwrap();
1669 manager
1670 .add_task("Task 3", Some("JWT token refresh"), None)
1671 .await
1672 .unwrap();
1673
1674 let results = manager.search_tasks("JWT").await.unwrap();
1676
1677 assert_eq!(results.len(), 2);
1678 for result in &results {
1679 assert!(result
1680 .task
1681 .spec
1682 .as_ref()
1683 .unwrap()
1684 .to_uppercase()
1685 .contains("JWT"));
1686 }
1687 }
1688
1689 #[tokio::test]
1690 async fn test_search_tasks_with_advanced_query() {
1691 let ctx = TestContext::new().await;
1692 let manager = TaskManager::new(ctx.pool());
1693
1694 manager
1696 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1697 .await
1698 .unwrap();
1699 manager
1700 .add_task("Feature", Some("Add authentication feature"), None)
1701 .await
1702 .unwrap();
1703 manager
1704 .add_task("Bug report", Some("Report critical database bug"), None)
1705 .await
1706 .unwrap();
1707
1708 let results = manager
1710 .search_tasks("authentication AND bug")
1711 .await
1712 .unwrap();
1713
1714 assert_eq!(results.len(), 1);
1715 assert!(results[0]
1716 .task
1717 .spec
1718 .as_ref()
1719 .unwrap()
1720 .contains("authentication"));
1721 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1722 }
1723
1724 #[tokio::test]
1725 async fn test_search_tasks_no_results() {
1726 let ctx = TestContext::new().await;
1727 let manager = TaskManager::new(ctx.pool());
1728
1729 manager
1731 .add_task("Task 1", Some("Some description"), None)
1732 .await
1733 .unwrap();
1734
1735 let results = manager.search_tasks("nonexistent").await.unwrap();
1737
1738 assert_eq!(results.len(), 0);
1739 }
1740
1741 #[tokio::test]
1742 async fn test_search_tasks_snippet_highlighting() {
1743 let ctx = TestContext::new().await;
1744 let manager = TaskManager::new(ctx.pool());
1745
1746 manager
1748 .add_task(
1749 "Test task",
1750 Some("This is a description with the keyword authentication in the middle"),
1751 None,
1752 )
1753 .await
1754 .unwrap();
1755
1756 let results = manager.search_tasks("authentication").await.unwrap();
1758
1759 assert_eq!(results.len(), 1);
1760 assert!(results[0].match_snippet.contains("**authentication**"));
1762 }
1763
1764 #[tokio::test]
1765 async fn test_pick_next_focused_subtask() {
1766 let ctx = TestContext::new().await;
1767 let manager = TaskManager::new(ctx.pool());
1768
1769 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1771 manager.start_task(parent.id, false).await.unwrap();
1772
1773 let subtask1 = manager
1775 .add_task("Subtask 1", None, Some(parent.id))
1776 .await
1777 .unwrap();
1778 let subtask2 = manager
1779 .add_task("Subtask 2", None, Some(parent.id))
1780 .await
1781 .unwrap();
1782
1783 manager
1785 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1786 .await
1787 .unwrap();
1788 manager
1789 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1790 .await
1791 .unwrap();
1792
1793 let response = manager.pick_next().await.unwrap();
1795
1796 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1797 assert!(response.task.is_some());
1798 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1799 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1800 }
1801
1802 #[tokio::test]
1803 async fn test_pick_next_top_level_task() {
1804 let ctx = TestContext::new().await;
1805 let manager = TaskManager::new(ctx.pool());
1806
1807 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1809 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1810
1811 manager
1813 .update_task(task1.id, None, None, None, None, None, Some(5))
1814 .await
1815 .unwrap();
1816 manager
1817 .update_task(task2.id, None, None, None, None, None, Some(3))
1818 .await
1819 .unwrap();
1820
1821 let response = manager.pick_next().await.unwrap();
1823
1824 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1825 assert!(response.task.is_some());
1826 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1827 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1828 }
1829
1830 #[tokio::test]
1831 async fn test_pick_next_no_tasks() {
1832 let ctx = TestContext::new().await;
1833 let manager = TaskManager::new(ctx.pool());
1834
1835 let response = manager.pick_next().await.unwrap();
1837
1838 assert_eq!(response.suggestion_type, "NONE");
1839 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1840 assert!(response.message.is_some());
1841 }
1842
1843 #[tokio::test]
1844 async fn test_pick_next_all_completed() {
1845 let ctx = TestContext::new().await;
1846 let manager = TaskManager::new(ctx.pool());
1847
1848 let task = manager.add_task("Task 1", None, None).await.unwrap();
1850 manager.start_task(task.id, false).await.unwrap();
1851 manager.done_task().await.unwrap();
1852
1853 let response = manager.pick_next().await.unwrap();
1855
1856 assert_eq!(response.suggestion_type, "NONE");
1857 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1858 assert!(response.message.is_some());
1859 }
1860
1861 #[tokio::test]
1862 async fn test_pick_next_no_available_todos() {
1863 let ctx = TestContext::new().await;
1864 let manager = TaskManager::new(ctx.pool());
1865
1866 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1868 manager.start_task(parent.id, false).await.unwrap();
1869
1870 let subtask = manager
1872 .add_task("Subtask", None, Some(parent.id))
1873 .await
1874 .unwrap();
1875 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1877 .bind(subtask.id)
1878 .execute(ctx.pool())
1879 .await
1880 .unwrap();
1881
1882 sqlx::query(
1884 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
1885 )
1886 .bind(subtask.id.to_string())
1887 .execute(ctx.pool())
1888 .await
1889 .unwrap();
1890
1891 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1893 .bind(parent.id)
1894 .execute(ctx.pool())
1895 .await
1896 .unwrap();
1897
1898 let response = manager.pick_next().await.unwrap();
1900
1901 assert_eq!(response.suggestion_type, "NONE");
1902 assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
1903 assert!(response.message.is_some());
1904 }
1905
1906 #[tokio::test]
1907 async fn test_pick_next_priority_ordering() {
1908 let ctx = TestContext::new().await;
1909 let manager = TaskManager::new(ctx.pool());
1910
1911 let parent = manager.add_task("Parent", None, None).await.unwrap();
1913 manager.start_task(parent.id, false).await.unwrap();
1914
1915 let sub1 = manager
1917 .add_task("Priority 10", None, Some(parent.id))
1918 .await
1919 .unwrap();
1920 manager
1921 .update_task(sub1.id, None, None, None, None, None, Some(10))
1922 .await
1923 .unwrap();
1924
1925 let sub2 = manager
1926 .add_task("Priority 1", None, Some(parent.id))
1927 .await
1928 .unwrap();
1929 manager
1930 .update_task(sub2.id, None, None, None, None, None, Some(1))
1931 .await
1932 .unwrap();
1933
1934 let sub3 = manager
1935 .add_task("Priority 5", None, Some(parent.id))
1936 .await
1937 .unwrap();
1938 manager
1939 .update_task(sub3.id, None, None, None, None, None, Some(5))
1940 .await
1941 .unwrap();
1942
1943 let response = manager.pick_next().await.unwrap();
1945
1946 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1947 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
1948 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
1949 }
1950
1951 #[tokio::test]
1952 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
1953 let ctx = TestContext::new().await;
1954 let manager = TaskManager::new(ctx.pool());
1955
1956 let parent = manager.add_task("Parent", None, None).await.unwrap();
1958 manager.start_task(parent.id, false).await.unwrap();
1959
1960 let top_level = manager
1962 .add_task("Top level task", None, None)
1963 .await
1964 .unwrap();
1965
1966 let response = manager.pick_next().await.unwrap();
1968
1969 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1970 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
1971 }
1972
1973 #[tokio::test]
1976 async fn test_get_task_with_events() {
1977 let ctx = TestContext::new().await;
1978 let task_mgr = TaskManager::new(ctx.pool());
1979 let event_mgr = EventManager::new(ctx.pool());
1980
1981 let task = task_mgr.add_task("Test", None, None).await.unwrap();
1982
1983 event_mgr
1985 .add_event(task.id, "progress", "Event 1")
1986 .await
1987 .unwrap();
1988 event_mgr
1989 .add_event(task.id, "decision", "Event 2")
1990 .await
1991 .unwrap();
1992
1993 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1994
1995 assert_eq!(result.task.id, task.id);
1996 assert!(result.events_summary.is_some());
1997
1998 let summary = result.events_summary.unwrap();
1999 assert_eq!(summary.total_count, 2);
2000 assert_eq!(summary.recent_events.len(), 2);
2001 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2003 }
2004
2005 #[tokio::test]
2006 async fn test_get_task_with_events_nonexistent() {
2007 let ctx = TestContext::new().await;
2008 let task_mgr = TaskManager::new(ctx.pool());
2009
2010 let result = task_mgr.get_task_with_events(999).await;
2011 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2012 }
2013
2014 #[tokio::test]
2015 async fn test_get_task_with_many_events() {
2016 let ctx = TestContext::new().await;
2017 let task_mgr = TaskManager::new(ctx.pool());
2018 let event_mgr = EventManager::new(ctx.pool());
2019
2020 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2021
2022 for i in 0..20 {
2024 event_mgr
2025 .add_event(task.id, "test", &format!("Event {}", i))
2026 .await
2027 .unwrap();
2028 }
2029
2030 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2031 let summary = result.events_summary.unwrap();
2032
2033 assert_eq!(summary.total_count, 20);
2034 assert_eq!(summary.recent_events.len(), 10); }
2036
2037 #[tokio::test]
2038 async fn test_get_task_with_no_events() {
2039 let ctx = TestContext::new().await;
2040 let task_mgr = TaskManager::new(ctx.pool());
2041
2042 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2043
2044 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2045 let summary = result.events_summary.unwrap();
2046
2047 assert_eq!(summary.total_count, 0);
2048 assert_eq!(summary.recent_events.len(), 0);
2049 }
2050
2051 #[tokio::test]
2052 async fn test_pick_next_tasks_zero_capacity() {
2053 let ctx = TestContext::new().await;
2054 let task_mgr = TaskManager::new(ctx.pool());
2055
2056 task_mgr.add_task("Task 1", None, None).await.unwrap();
2057
2058 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2060 assert_eq!(results.len(), 0);
2061 }
2062
2063 #[tokio::test]
2064 async fn test_pick_next_tasks_capacity_exceeds_available() {
2065 let ctx = TestContext::new().await;
2066 let task_mgr = TaskManager::new(ctx.pool());
2067
2068 task_mgr.add_task("Task 1", None, None).await.unwrap();
2069 task_mgr.add_task("Task 2", None, None).await.unwrap();
2070
2071 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2073 assert_eq!(results.len(), 2); }
2075
2076 #[tokio::test]
2079 async fn test_get_task_context_root_task_no_relations() {
2080 let ctx = TestContext::new().await;
2081 let task_mgr = TaskManager::new(ctx.pool());
2082
2083 let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2085
2086 let context = task_mgr.get_task_context(task.id).await.unwrap();
2087
2088 assert_eq!(context.task.id, task.id);
2090 assert_eq!(context.task.name, "Root task");
2091
2092 assert_eq!(context.ancestors.len(), 0);
2094
2095 assert_eq!(context.siblings.len(), 0);
2097
2098 assert_eq!(context.children.len(), 0);
2100 }
2101
2102 #[tokio::test]
2103 async fn test_get_task_context_with_siblings() {
2104 let ctx = TestContext::new().await;
2105 let task_mgr = TaskManager::new(ctx.pool());
2106
2107 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2109 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2110 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2111
2112 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2113
2114 assert_eq!(context.task.id, task2.id);
2116
2117 assert_eq!(context.ancestors.len(), 0);
2119
2120 assert_eq!(context.siblings.len(), 2);
2122 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2123 assert!(sibling_ids.contains(&task1.id));
2124 assert!(sibling_ids.contains(&task3.id));
2125 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2129 }
2130
2131 #[tokio::test]
2132 async fn test_get_task_context_with_parent() {
2133 let ctx = TestContext::new().await;
2134 let task_mgr = TaskManager::new(ctx.pool());
2135
2136 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2138 let child = task_mgr
2139 .add_task("Child task", None, Some(parent.id))
2140 .await
2141 .unwrap();
2142
2143 let context = task_mgr.get_task_context(child.id).await.unwrap();
2144
2145 assert_eq!(context.task.id, child.id);
2147 assert_eq!(context.task.parent_id, Some(parent.id));
2148
2149 assert_eq!(context.ancestors.len(), 1);
2151 assert_eq!(context.ancestors[0].id, parent.id);
2152 assert_eq!(context.ancestors[0].name, "Parent task");
2153
2154 assert_eq!(context.siblings.len(), 0);
2156
2157 assert_eq!(context.children.len(), 0);
2159 }
2160
2161 #[tokio::test]
2162 async fn test_get_task_context_with_children() {
2163 let ctx = TestContext::new().await;
2164 let task_mgr = TaskManager::new(ctx.pool());
2165
2166 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2168 let child1 = task_mgr
2169 .add_task("Child 1", None, Some(parent.id))
2170 .await
2171 .unwrap();
2172 let child2 = task_mgr
2173 .add_task("Child 2", None, Some(parent.id))
2174 .await
2175 .unwrap();
2176 let child3 = task_mgr
2177 .add_task("Child 3", None, Some(parent.id))
2178 .await
2179 .unwrap();
2180
2181 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2182
2183 assert_eq!(context.task.id, parent.id);
2185
2186 assert_eq!(context.ancestors.len(), 0);
2188
2189 assert_eq!(context.siblings.len(), 0);
2191
2192 assert_eq!(context.children.len(), 3);
2194 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2195 assert!(child_ids.contains(&child1.id));
2196 assert!(child_ids.contains(&child2.id));
2197 assert!(child_ids.contains(&child3.id));
2198 }
2199
2200 #[tokio::test]
2201 async fn test_get_task_context_multi_level_hierarchy() {
2202 let ctx = TestContext::new().await;
2203 let task_mgr = TaskManager::new(ctx.pool());
2204
2205 let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2207 let parent = task_mgr
2208 .add_task("Parent", None, Some(grandparent.id))
2209 .await
2210 .unwrap();
2211 let child = task_mgr
2212 .add_task("Child", None, Some(parent.id))
2213 .await
2214 .unwrap();
2215
2216 let context = task_mgr.get_task_context(child.id).await.unwrap();
2217
2218 assert_eq!(context.task.id, child.id);
2220
2221 assert_eq!(context.ancestors.len(), 2);
2223 assert_eq!(context.ancestors[0].id, parent.id);
2224 assert_eq!(context.ancestors[0].name, "Parent");
2225 assert_eq!(context.ancestors[1].id, grandparent.id);
2226 assert_eq!(context.ancestors[1].name, "Grandparent");
2227
2228 assert_eq!(context.siblings.len(), 0);
2230
2231 assert_eq!(context.children.len(), 0);
2233 }
2234
2235 #[tokio::test]
2236 async fn test_get_task_context_complex_family_tree() {
2237 let ctx = TestContext::new().await;
2238 let task_mgr = TaskManager::new(ctx.pool());
2239
2240 let root = task_mgr.add_task("Root", None, None).await.unwrap();
2248 let child1 = task_mgr
2249 .add_task("Child1", None, Some(root.id))
2250 .await
2251 .unwrap();
2252 let child2 = task_mgr
2253 .add_task("Child2", None, Some(root.id))
2254 .await
2255 .unwrap();
2256 let grandchild1 = task_mgr
2257 .add_task("Grandchild1", None, Some(child1.id))
2258 .await
2259 .unwrap();
2260 let grandchild2 = task_mgr
2261 .add_task("Grandchild2", None, Some(child1.id))
2262 .await
2263 .unwrap();
2264
2265 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2267
2268 assert_eq!(context.task.id, grandchild2.id);
2270
2271 assert_eq!(context.ancestors.len(), 2);
2273 assert_eq!(context.ancestors[0].id, child1.id);
2274 assert_eq!(context.ancestors[1].id, root.id);
2275
2276 assert_eq!(context.siblings.len(), 1);
2278 assert_eq!(context.siblings[0].id, grandchild1.id);
2279
2280 assert_eq!(context.children.len(), 0);
2282
2283 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2285 assert_eq!(context_child1.ancestors.len(), 1);
2286 assert_eq!(context_child1.ancestors[0].id, root.id);
2287 assert_eq!(context_child1.siblings.len(), 1);
2288 assert_eq!(context_child1.siblings[0].id, child2.id);
2289 assert_eq!(context_child1.children.len(), 2);
2290 }
2291
2292 #[tokio::test]
2293 async fn test_get_task_context_respects_priority_ordering() {
2294 let ctx = TestContext::new().await;
2295 let task_mgr = TaskManager::new(ctx.pool());
2296
2297 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2299
2300 let child_low = task_mgr
2302 .add_task("Low priority", None, Some(parent.id))
2303 .await
2304 .unwrap();
2305 let _ = task_mgr
2306 .update_task(child_low.id, None, None, None, None, None, Some(10))
2307 .await
2308 .unwrap();
2309
2310 let child_high = task_mgr
2311 .add_task("High priority", None, Some(parent.id))
2312 .await
2313 .unwrap();
2314 let _ = task_mgr
2315 .update_task(child_high.id, None, None, None, None, None, Some(1))
2316 .await
2317 .unwrap();
2318
2319 let child_medium = task_mgr
2320 .add_task("Medium priority", None, Some(parent.id))
2321 .await
2322 .unwrap();
2323 let _ = task_mgr
2324 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2325 .await
2326 .unwrap();
2327
2328 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2329
2330 assert_eq!(context.children.len(), 3);
2332 assert_eq!(context.children[0].priority, Some(1));
2333 assert_eq!(context.children[1].priority, Some(5));
2334 assert_eq!(context.children[2].priority, Some(10));
2335 }
2336
2337 #[tokio::test]
2338 async fn test_get_task_context_nonexistent_task() {
2339 let ctx = TestContext::new().await;
2340 let task_mgr = TaskManager::new(ctx.pool());
2341
2342 let result = task_mgr.get_task_context(99999).await;
2343 assert!(result.is_err());
2344 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2345 }
2346
2347 #[tokio::test]
2348 async fn test_get_task_context_handles_null_priority() {
2349 let ctx = TestContext::new().await;
2350 let task_mgr = TaskManager::new(ctx.pool());
2351
2352 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2354 let _ = task_mgr
2355 .update_task(task1.id, None, None, None, None, None, Some(1))
2356 .await
2357 .unwrap();
2358
2359 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2360 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2363 let _ = task_mgr
2364 .update_task(task3.id, None, None, None, None, None, Some(5))
2365 .await
2366 .unwrap();
2367
2368 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2369
2370 assert_eq!(context.siblings.len(), 2);
2372 assert_eq!(context.siblings[0].id, task1.id);
2374 assert_eq!(context.siblings[0].priority, Some(1));
2375 assert_eq!(context.siblings[1].id, task3.id);
2377 assert_eq!(context.siblings[1].priority, Some(5));
2378 }
2379}