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