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