1use crate::db::models::{
2 DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, PaginatedTasks, ParentTaskInfo,
3 PickNextResponse, SpawnSubtaskResponse, SubtaskInfo, Task, TaskSortBy, TaskWithEvents,
4 WorkspaceStats, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::SqlitePool;
9use std::sync::Arc;
10
11pub use crate::db::models::TaskContext;
12
13#[derive(Debug, Clone)]
15pub struct DeleteTaskResult {
16 pub found: bool,
18 pub descendant_count: i64,
20}
21
22pub struct TaskManager<'a> {
23 pool: &'a SqlitePool,
24 notifier: crate::notifications::NotificationSender,
25 cli_notifier: Option<crate::dashboard::cli_notifier::CliNotifier>,
26 project_path: Option<String>,
27}
28
29impl<'a> TaskManager<'a> {
30 pub fn new(pool: &'a SqlitePool) -> Self {
31 Self {
32 pool,
33 notifier: crate::notifications::NotificationSender::new(None),
34 cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
35 project_path: None,
36 }
37 }
38
39 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
41 Self {
42 pool,
43 notifier: crate::notifications::NotificationSender::new(None),
44 cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
45 project_path: Some(project_path),
46 }
47 }
48
49 pub fn with_websocket(
51 pool: &'a SqlitePool,
52 ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
53 project_path: String,
54 ) -> Self {
55 Self {
56 pool,
57 notifier: crate::notifications::NotificationSender::new(Some(ws_state)),
58 cli_notifier: None, project_path: Some(project_path),
60 }
61 }
62
63 async fn notify_task_created(&self, task: &Task) {
65 use crate::dashboard::websocket::DatabaseOperationPayload;
66
67 if let Some(project_path) = &self.project_path {
69 let task_json = match serde_json::to_value(task) {
70 Ok(json) => json,
71 Err(e) => {
72 tracing::warn!(error = %e, "Failed to serialize task for notification");
73 return;
74 },
75 };
76
77 let payload =
78 DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
79 self.notifier.send(payload).await;
80 }
81
82 if let Some(cli_notifier) = &self.cli_notifier {
84 cli_notifier
85 .notify_task_changed(Some(task.id), "created", self.project_path.clone())
86 .await;
87 }
88 }
89
90 async fn notify_task_updated(&self, task: &Task) {
92 use crate::dashboard::websocket::DatabaseOperationPayload;
93
94 if let Some(project_path) = &self.project_path {
96 let task_json = match serde_json::to_value(task) {
97 Ok(json) => json,
98 Err(e) => {
99 tracing::warn!(error = %e, "Failed to serialize task for notification");
100 return;
101 },
102 };
103
104 let payload =
105 DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
106 self.notifier.send(payload).await;
107 }
108
109 if let Some(cli_notifier) = &self.cli_notifier {
111 cli_notifier
112 .notify_task_changed(Some(task.id), "updated", self.project_path.clone())
113 .await;
114 }
115 }
116
117 async fn notify_task_deleted(&self, task_id: i64) {
119 use crate::dashboard::websocket::DatabaseOperationPayload;
120
121 if let Some(project_path) = &self.project_path {
123 let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
124 self.notifier.send(payload).await;
125 }
126
127 if let Some(cli_notifier) = &self.cli_notifier {
129 cli_notifier
130 .notify_task_changed(Some(task_id), "deleted", self.project_path.clone())
131 .await;
132 }
133 }
134
135 #[tracing::instrument(skip(self), fields(task_name = %name))]
138 pub async fn add_task(
139 &self,
140 name: &str,
141 spec: Option<&str>,
142 parent_id: Option<i64>,
143 owner: Option<&str>,
144 ) -> Result<Task> {
145 if let Some(pid) = parent_id {
147 self.check_task_exists(pid).await?;
148 }
149
150 let now = Utc::now();
151 let owner = owner.unwrap_or("human");
152
153 let result = sqlx::query(
154 r#"
155 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner)
156 VALUES (?, ?, ?, 'todo', ?, ?)
157 "#,
158 )
159 .bind(name)
160 .bind(spec)
161 .bind(parent_id)
162 .bind(now)
163 .bind(owner)
164 .execute(self.pool)
165 .await?;
166
167 let id = result.last_insert_rowid();
168 let task = self.get_task(id).await?;
169
170 self.notify_task_created(&task).await;
172
173 Ok(task)
174 }
175
176 #[allow(clippy::too_many_arguments)]
199 pub async fn create_task_in_tx(
200 &self,
201 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
202 name: &str,
203 spec: Option<&str>,
204 priority: Option<i32>,
205 status: Option<&str>,
206 active_form: Option<&str>,
207 owner: &str,
208 ) -> Result<i64> {
209 let now = Utc::now();
210 let status = status.unwrap_or("todo");
211 let priority = priority.unwrap_or(3); let result = sqlx::query(
214 r#"
215 INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at, owner)
216 VALUES (?, ?, ?, ?, ?, ?, ?)
217 "#,
218 )
219 .bind(name)
220 .bind(spec)
221 .bind(priority)
222 .bind(status)
223 .bind(active_form)
224 .bind(now)
225 .bind(owner)
226 .execute(&mut **tx)
227 .await?;
228
229 Ok(result.last_insert_rowid())
230 }
231
232 pub async fn update_task_in_tx(
245 &self,
246 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
247 task_id: i64,
248 spec: Option<&str>,
249 priority: Option<i32>,
250 status: Option<&str>,
251 active_form: Option<&str>,
252 ) -> Result<()> {
253 if let Some(spec) = spec {
255 sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
256 .bind(spec)
257 .bind(task_id)
258 .execute(&mut **tx)
259 .await?;
260 }
261
262 if let Some(priority) = priority {
264 sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
265 .bind(priority)
266 .bind(task_id)
267 .execute(&mut **tx)
268 .await?;
269 }
270
271 if let Some(status) = status {
273 sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
274 .bind(status)
275 .bind(task_id)
276 .execute(&mut **tx)
277 .await?;
278 }
279
280 if let Some(active_form) = active_form {
282 sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
283 .bind(active_form)
284 .bind(task_id)
285 .execute(&mut **tx)
286 .await?;
287 }
288
289 Ok(())
290 }
291
292 pub async fn set_parent_in_tx(
296 &self,
297 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
298 task_id: i64,
299 parent_id: i64,
300 ) -> Result<()> {
301 sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
302 .bind(parent_id)
303 .bind(task_id)
304 .execute(&mut **tx)
305 .await?;
306
307 Ok(())
308 }
309
310 pub async fn clear_parent_in_tx(
314 &self,
315 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
316 task_id: i64,
317 ) -> Result<()> {
318 sqlx::query("UPDATE tasks SET parent_id = NULL WHERE id = ?")
319 .bind(task_id)
320 .execute(&mut **tx)
321 .await?;
322
323 Ok(())
324 }
325
326 pub async fn delete_task_in_tx(
342 &self,
343 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
344 task_id: i64,
345 ) -> Result<DeleteTaskResult> {
346 let task_info: Option<(i64,)> = sqlx::query_as("SELECT id FROM tasks WHERE id = ?")
348 .bind(task_id)
349 .fetch_optional(&mut **tx)
350 .await?;
351
352 if task_info.is_none() {
353 return Ok(DeleteTaskResult {
354 found: false,
355 descendant_count: 0,
356 });
357 }
358
359 let descendant_count = self.count_descendants_in_tx(tx, task_id).await?;
361
362 sqlx::query("DELETE FROM tasks WHERE id = ?")
364 .bind(task_id)
365 .execute(&mut **tx)
366 .await?;
367
368 Ok(DeleteTaskResult {
369 found: true,
370 descendant_count,
371 })
372 }
373
374 async fn count_descendants_in_tx(
376 &self,
377 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
378 task_id: i64,
379 ) -> Result<i64> {
380 let count: (i64,) = sqlx::query_as(
382 r#"
383 WITH RECURSIVE descendants AS (
384 SELECT id FROM tasks WHERE parent_id = ?
385 UNION ALL
386 SELECT t.id FROM tasks t
387 INNER JOIN descendants d ON t.parent_id = d.id
388 )
389 SELECT COUNT(*) FROM descendants
390 "#,
391 )
392 .bind(task_id)
393 .fetch_one(&mut **tx)
394 .await?;
395
396 Ok(count.0)
397 }
398
399 pub async fn find_focused_in_subtree_in_tx(
410 &self,
411 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
412 task_id: i64,
413 ) -> Result<Option<(i64, String)>> {
414 let row: Option<(i64, String)> = sqlx::query_as(
417 r#"
418 WITH RECURSIVE subtree AS (
419 SELECT id FROM tasks WHERE id = ?
420 UNION ALL
421 SELECT t.id FROM tasks t
422 INNER JOIN subtree s ON t.parent_id = s.id
423 )
424 SELECT s.current_task_id, s.session_id FROM sessions s
425 WHERE s.current_task_id IN (SELECT id FROM subtree)
426 LIMIT 1
427 "#,
428 )
429 .bind(task_id)
430 .fetch_optional(&mut **tx)
431 .await?;
432
433 Ok(row)
434 }
435
436 pub async fn count_incomplete_children_in_tx(
441 &self,
442 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
443 task_id: i64,
444 ) -> Result<i64> {
445 let count: (i64,) = sqlx::query_as(crate::sql_constants::COUNT_INCOMPLETE_CHILDREN)
446 .bind(task_id)
447 .fetch_one(&mut **tx)
448 .await?;
449
450 Ok(count.0)
451 }
452
453 pub async fn complete_task_in_tx(
462 &self,
463 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
464 task_id: i64,
465 ) -> Result<()> {
466 let incomplete_count = self.count_incomplete_children_in_tx(tx, task_id).await?;
468 if incomplete_count > 0 {
469 return Err(IntentError::UncompletedChildren);
470 }
471
472 let now = chrono::Utc::now();
474 sqlx::query(
475 r#"
476 UPDATE tasks
477 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
478 WHERE id = ?
479 "#,
480 )
481 .bind(now)
482 .bind(task_id)
483 .execute(&mut **tx)
484 .await?;
485
486 Ok(())
487 }
488
489 pub async fn notify_batch_changed(&self) {
494 if let Some(cli_notifier) = &self.cli_notifier {
495 cli_notifier
496 .notify_task_changed(None, "batch_update", self.project_path.clone())
497 .await;
498 }
499 }
500
501 #[tracing::instrument(skip(self))]
507 pub async fn get_task(&self, id: i64) -> Result<Task> {
508 let task = sqlx::query_as::<_, Task>(
509 r#"
510 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
511 FROM tasks
512 WHERE id = ?
513 "#,
514 )
515 .bind(id)
516 .fetch_optional(self.pool)
517 .await?
518 .ok_or(IntentError::TaskNotFound(id))?;
519
520 Ok(task)
521 }
522
523 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
525 let task = self.get_task(id).await?;
526 let events_summary = self.get_events_summary(id).await?;
527
528 Ok(TaskWithEvents {
529 task,
530 events_summary: Some(events_summary),
531 })
532 }
533
534 pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
543 let mut chain = Vec::new();
544 let mut current_id = Some(task_id);
545
546 while let Some(id) = current_id {
547 let task = self.get_task(id).await?;
548 current_id = task.parent_id;
549 chain.push(task);
550 }
551
552 Ok(chain)
553 }
554
555 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
563 let task = self.get_task(id).await?;
565
566 let mut ancestors = Vec::new();
568 let mut current_parent_id = task.parent_id;
569
570 while let Some(parent_id) = current_parent_id {
571 let parent = self.get_task(parent_id).await?;
572 current_parent_id = parent.parent_id;
573 ancestors.push(parent);
574 }
575
576 let siblings = if let Some(parent_id) = task.parent_id {
578 sqlx::query_as::<_, Task>(
579 r#"
580 SELECT id, parent_id, name, spec, status, complexity, priority,
581 first_todo_at, first_doing_at, first_done_at, active_form, owner
582 FROM tasks
583 WHERE parent_id = ? AND id != ?
584 ORDER BY priority ASC NULLS LAST, id ASC
585 "#,
586 )
587 .bind(parent_id)
588 .bind(id)
589 .fetch_all(self.pool)
590 .await?
591 } else {
592 sqlx::query_as::<_, Task>(
594 r#"
595 SELECT id, parent_id, name, spec, status, complexity, priority,
596 first_todo_at, first_doing_at, first_done_at, active_form, owner
597 FROM tasks
598 WHERE parent_id IS NULL AND id != ?
599 ORDER BY priority ASC NULLS LAST, id ASC
600 "#,
601 )
602 .bind(id)
603 .fetch_all(self.pool)
604 .await?
605 };
606
607 let children = sqlx::query_as::<_, Task>(
609 r#"
610 SELECT id, parent_id, name, spec, status, complexity, priority,
611 first_todo_at, first_doing_at, first_done_at, active_form, owner
612 FROM tasks
613 WHERE parent_id = ?
614 ORDER BY priority ASC NULLS LAST, id ASC
615 "#,
616 )
617 .bind(id)
618 .fetch_all(self.pool)
619 .await?;
620
621 let blocking_tasks = sqlx::query_as::<_, Task>(
623 r#"
624 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
625 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
626 FROM tasks t
627 JOIN dependencies d ON t.id = d.blocking_task_id
628 WHERE d.blocked_task_id = ?
629 ORDER BY t.priority ASC NULLS LAST, t.id ASC
630 "#,
631 )
632 .bind(id)
633 .fetch_all(self.pool)
634 .await?;
635
636 let blocked_by_tasks = sqlx::query_as::<_, Task>(
638 r#"
639 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
640 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
641 FROM tasks t
642 JOIN dependencies d ON t.id = d.blocked_task_id
643 WHERE d.blocking_task_id = ?
644 ORDER BY t.priority ASC NULLS LAST, t.id ASC
645 "#,
646 )
647 .bind(id)
648 .fetch_all(self.pool)
649 .await?;
650
651 Ok(TaskContext {
652 task,
653 ancestors,
654 siblings,
655 children,
656 dependencies: crate::db::models::TaskDependencies {
657 blocking_tasks,
658 blocked_by_tasks,
659 },
660 })
661 }
662
663 pub async fn get_descendants(&self, task_id: i64) -> Result<Vec<Task>> {
666 let descendants = sqlx::query_as::<_, Task>(
667 r#"
668 WITH RECURSIVE descendants AS (
669 SELECT id, parent_id, name, spec, status, complexity, priority,
670 first_todo_at, first_doing_at, first_done_at, active_form, owner
671 FROM tasks
672 WHERE parent_id = ?
673
674 UNION ALL
675
676 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
677 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
678 FROM tasks t
679 INNER JOIN descendants d ON t.parent_id = d.id
680 )
681 SELECT * FROM descendants
682 ORDER BY parent_id NULLS FIRST, priority ASC NULLS LAST, id ASC
683 "#,
684 )
685 .bind(task_id)
686 .fetch_all(self.pool)
687 .await?;
688
689 Ok(descendants)
690 }
691
692 pub async fn get_status(
695 &self,
696 task_id: i64,
697 with_events: bool,
698 ) -> Result<crate::db::models::StatusResponse> {
699 use crate::db::models::{StatusResponse, TaskBrief};
700
701 let context = self.get_task_context(task_id).await?;
703
704 let descendants_full = self.get_descendants(task_id).await?;
706
707 let siblings: Vec<TaskBrief> = context.siblings.iter().map(TaskBrief::from).collect();
709 let descendants: Vec<TaskBrief> = descendants_full.iter().map(TaskBrief::from).collect();
710
711 let events = if with_events {
713 let event_mgr = crate::events::EventManager::new(self.pool);
714 Some(
715 event_mgr
716 .list_events(Some(task_id), Some(50), None, None)
717 .await?,
718 )
719 } else {
720 None
721 };
722
723 Ok(StatusResponse {
724 focused_task: context.task,
725 ancestors: context.ancestors,
726 siblings,
727 descendants,
728 events,
729 })
730 }
731
732 pub async fn get_root_tasks(&self) -> Result<Vec<Task>> {
734 let tasks = sqlx::query_as::<_, Task>(
735 r#"
736 SELECT id, parent_id, name, spec, status, complexity, priority,
737 first_todo_at, first_doing_at, first_done_at, active_form, owner
738 FROM tasks
739 WHERE parent_id IS NULL
740 ORDER BY
741 CASE status
742 WHEN 'doing' THEN 0
743 WHEN 'todo' THEN 1
744 WHEN 'done' THEN 2
745 END,
746 priority ASC NULLS LAST,
747 id ASC
748 "#,
749 )
750 .fetch_all(self.pool)
751 .await?;
752
753 Ok(tasks)
754 }
755
756 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
758 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
759 .bind(task_id)
760 .fetch_one(self.pool)
761 .await?;
762
763 let recent_events = sqlx::query_as::<_, Event>(
764 r#"
765 SELECT id, task_id, timestamp, log_type, discussion_data
766 FROM events
767 WHERE task_id = ?
768 ORDER BY timestamp DESC
769 LIMIT 10
770 "#,
771 )
772 .bind(task_id)
773 .fetch_all(self.pool)
774 .await?;
775
776 Ok(EventsSummary {
777 total_count,
778 recent_events,
779 })
780 }
781
782 #[allow(clippy::too_many_arguments)]
784 pub async fn update_task(
785 &self,
786 id: i64,
787 name: Option<&str>,
788 spec: Option<&str>,
789 parent_id: Option<Option<i64>>,
790 status: Option<&str>,
791 complexity: Option<i32>,
792 priority: Option<i32>,
793 ) -> Result<Task> {
794 let task = self.get_task(id).await?;
796
797 if let Some(s) = status {
799 if !["todo", "doing", "done"].contains(&s) {
800 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
801 }
802 }
803
804 if let Some(Some(pid)) = parent_id {
806 if pid == id {
807 return Err(IntentError::CircularDependency {
808 blocking_task_id: pid,
809 blocked_task_id: id,
810 });
811 }
812 self.check_task_exists(pid).await?;
813 self.check_circular_dependency(id, pid).await?;
814 }
815
816 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
818 sqlx::QueryBuilder::new("UPDATE tasks SET ");
819 let mut has_updates = false;
820
821 if let Some(n) = name {
822 if has_updates {
823 builder.push(", ");
824 }
825 builder.push("name = ").push_bind(n);
826 has_updates = true;
827 }
828
829 if let Some(s) = spec {
830 if has_updates {
831 builder.push(", ");
832 }
833 builder.push("spec = ").push_bind(s);
834 has_updates = true;
835 }
836
837 if let Some(pid) = parent_id {
838 if has_updates {
839 builder.push(", ");
840 }
841 match pid {
842 Some(p) => {
843 builder.push("parent_id = ").push_bind(p);
844 },
845 None => {
846 builder.push("parent_id = NULL");
847 },
848 }
849 has_updates = true;
850 }
851
852 if let Some(c) = complexity {
853 if has_updates {
854 builder.push(", ");
855 }
856 builder.push("complexity = ").push_bind(c);
857 has_updates = true;
858 }
859
860 if let Some(p) = priority {
861 if has_updates {
862 builder.push(", ");
863 }
864 builder.push("priority = ").push_bind(p);
865 has_updates = true;
866 }
867
868 if let Some(s) = status {
869 if has_updates {
870 builder.push(", ");
871 }
872 builder.push("status = ").push_bind(s);
873 has_updates = true;
874
875 let now = Utc::now();
877 let timestamp = now.to_rfc3339();
878 match s {
879 "todo" if task.first_todo_at.is_none() => {
880 builder.push(", first_todo_at = ").push_bind(timestamp);
881 },
882 "doing" if task.first_doing_at.is_none() => {
883 builder.push(", first_doing_at = ").push_bind(timestamp);
884 },
885 "done" if task.first_done_at.is_none() => {
886 builder.push(", first_done_at = ").push_bind(timestamp);
887 },
888 _ => {},
889 }
890 }
891
892 if !has_updates {
893 return Ok(task);
894 }
895
896 builder.push(" WHERE id = ").push_bind(id);
897
898 builder.build().execute(self.pool).await?;
899
900 let task = self.get_task(id).await?;
901
902 self.notify_task_updated(&task).await;
904
905 Ok(task)
906 }
907
908 pub async fn delete_task(&self, id: i64) -> Result<()> {
910 self.check_task_exists(id).await?;
911
912 sqlx::query("DELETE FROM tasks WHERE id = ?")
913 .bind(id)
914 .execute(self.pool)
915 .await?;
916
917 self.notify_task_deleted(id).await;
919
920 Ok(())
921 }
922
923 pub async fn find_tasks(
925 &self,
926 status: Option<&str>,
927 parent_id: Option<Option<i64>>,
928 sort_by: Option<TaskSortBy>,
929 limit: Option<i64>,
930 offset: Option<i64>,
931 ) -> Result<PaginatedTasks> {
932 let sort_by = sort_by.unwrap_or_default(); let limit = limit.unwrap_or(100);
935 let offset = offset.unwrap_or(0);
936
937 let session_id = crate::workspace::resolve_session_id(None);
939
940 let mut where_clause = String::from("WHERE 1=1");
942 let mut conditions = Vec::new();
943
944 if let Some(s) = status {
945 where_clause.push_str(" AND status = ?");
946 conditions.push(s.to_string());
947 }
948
949 if let Some(pid) = parent_id {
950 if let Some(p) = pid {
951 where_clause.push_str(" AND parent_id = ?");
952 conditions.push(p.to_string());
953 } else {
954 where_clause.push_str(" AND parent_id IS NULL");
955 }
956 }
957
958 let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
960
961 let order_clause = match sort_by {
963 TaskSortBy::Id => {
964 "ORDER BY id ASC".to_string()
966 },
967 TaskSortBy::Priority => {
968 "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
970 .to_string()
971 },
972 TaskSortBy::Time => {
973 r#"ORDER BY
975 CASE status
976 WHEN 'doing' THEN first_doing_at
977 WHEN 'todo' THEN first_todo_at
978 WHEN 'done' THEN first_done_at
979 END ASC NULLS LAST,
980 id ASC"#
981 .to_string()
982 },
983 TaskSortBy::FocusAware => {
984 r#"ORDER BY
986 CASE
987 WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
988 WHEN t.status = 'doing' THEN 1
989 WHEN t.status = 'todo' THEN 2
990 ELSE 3
991 END ASC,
992 COALESCE(t.priority, 999) ASC,
993 t.id ASC"#
994 .to_string()
995 },
996 };
997
998 let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
1000 let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
1001 for cond in &conditions {
1002 count_q = count_q.bind(cond);
1003 }
1004 let total_count = count_q.fetch_one(self.pool).await?;
1005
1006 let main_query = format!(
1008 "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner FROM tasks t {} {} LIMIT ? OFFSET ?",
1009 where_clause, order_clause
1010 );
1011
1012 let mut q = sqlx::query_as::<_, Task>(&main_query);
1013 for cond in conditions {
1014 q = q.bind(cond);
1015 }
1016 if uses_session_bind {
1018 q = q.bind(&session_id);
1019 }
1020 q = q.bind(limit);
1021 q = q.bind(offset);
1022
1023 let tasks = q.fetch_all(self.pool).await?;
1024
1025 let has_more = offset + (tasks.len() as i64) < total_count;
1027
1028 Ok(PaginatedTasks {
1029 tasks,
1030 total_count,
1031 has_more,
1032 limit,
1033 offset,
1034 })
1035 }
1036
1037 pub async fn get_stats(&self) -> Result<WorkspaceStats> {
1042 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
1043 r#"SELECT
1044 COUNT(*) as total,
1045 COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
1046 COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
1047 COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
1048 FROM tasks"#,
1049 )
1050 .fetch_one(self.pool)
1051 .await?;
1052
1053 Ok(WorkspaceStats {
1054 total_tasks: row.0,
1055 todo: row.1,
1056 doing: row.2,
1057 done: row.3,
1058 })
1059 }
1060
1061 #[tracing::instrument(skip(self))]
1063 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
1064 let task_exists: bool =
1066 sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1067 .bind(id)
1068 .fetch_one(self.pool)
1069 .await?;
1070
1071 if !task_exists {
1072 return Err(IntentError::TaskNotFound(id));
1073 }
1074
1075 use crate::dependencies::get_incomplete_blocking_tasks;
1077 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
1078 return Err(IntentError::TaskBlocked {
1079 task_id: id,
1080 blocking_task_ids: blocking_tasks,
1081 });
1082 }
1083
1084 let mut tx = self.pool.begin().await?;
1085
1086 let now = Utc::now();
1087
1088 sqlx::query(
1090 r#"
1091 UPDATE tasks
1092 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
1093 WHERE id = ?
1094 "#,
1095 )
1096 .bind(now)
1097 .bind(id)
1098 .execute(&mut *tx)
1099 .await?;
1100
1101 let session_id = crate::workspace::resolve_session_id(None);
1104 sqlx::query(
1105 r#"
1106 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
1107 VALUES (?, ?, datetime('now'), datetime('now'))
1108 ON CONFLICT(session_id) DO UPDATE SET
1109 current_task_id = excluded.current_task_id,
1110 last_active_at = datetime('now')
1111 "#,
1112 )
1113 .bind(&session_id)
1114 .bind(id)
1115 .execute(&mut *tx)
1116 .await?;
1117
1118 tx.commit().await?;
1119
1120 if with_events {
1121 let result = self.get_task_with_events(id).await?;
1122 self.notify_task_updated(&result.task).await;
1123 Ok(result)
1124 } else {
1125 let task = self.get_task(id).await?;
1126 self.notify_task_updated(&task).await;
1127 Ok(TaskWithEvents {
1128 task,
1129 events_summary: None,
1130 })
1131 }
1132 }
1133
1134 #[tracing::instrument(skip(self))]
1143 pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1144 let session_id = crate::workspace::resolve_session_id(None);
1145 let mut tx = self.pool.begin().await?;
1146
1147 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1149 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1150 )
1151 .bind(&session_id)
1152 .fetch_optional(&mut *tx)
1153 .await?
1154 .flatten();
1155
1156 let id = current_task_id.ok_or(IntentError::InvalidInput(
1157 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
1158 ))?;
1159
1160 let task_info: (String, Option<i64>, String) =
1162 sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
1163 .bind(id)
1164 .fetch_one(&mut *tx)
1165 .await?;
1166 let (task_name, parent_id, owner) = task_info;
1167
1168 if owner == "human" && is_ai_caller {
1171 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1172 task_id: id,
1173 task_name: task_name.clone(),
1174 });
1175 }
1176
1177 self.complete_task_in_tx(&mut tx, id).await?;
1179
1180 sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1182 .bind(&session_id)
1183 .execute(&mut *tx)
1184 .await?;
1185
1186 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
1188 let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1190 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
1191 )
1192 .bind(parent_task_id)
1193 .bind(id)
1194 .fetch_one(&mut *tx)
1195 .await?;
1196
1197 if remaining_siblings == 0 {
1198 let parent_name: String =
1200 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1201 .bind(parent_task_id)
1202 .fetch_one(&mut *tx)
1203 .await?;
1204
1205 NextStepSuggestion::ParentIsReady {
1206 message: format!(
1207 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1208 parent_task_id, parent_name
1209 ),
1210 parent_task_id,
1211 parent_task_name: parent_name,
1212 }
1213 } else {
1214 let parent_name: String =
1216 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1217 .bind(parent_task_id)
1218 .fetch_one(&mut *tx)
1219 .await?;
1220
1221 NextStepSuggestion::SiblingTasksRemain {
1222 message: format!(
1223 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1224 id, parent_task_id, parent_name
1225 ),
1226 parent_task_id,
1227 parent_task_name: parent_name,
1228 remaining_siblings_count: remaining_siblings,
1229 }
1230 }
1231 } else {
1232 let child_count: i64 =
1234 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1235 .bind(id)
1236 .fetch_one(&mut *tx)
1237 .await?;
1238
1239 if child_count > 0 {
1240 NextStepSuggestion::TopLevelTaskCompleted {
1242 message: format!(
1243 "Top-level task #{} '{}' has been completed. Well done!",
1244 id, task_name
1245 ),
1246 completed_task_id: id,
1247 completed_task_name: task_name.clone(),
1248 }
1249 } else {
1250 let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1252 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
1253 )
1254 .bind(id)
1255 .fetch_one(&mut *tx)
1256 .await?;
1257
1258 if remaining_tasks == 0 {
1259 NextStepSuggestion::WorkspaceIsClear {
1260 message: format!(
1261 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1262 id
1263 ),
1264 completed_task_id: id,
1265 }
1266 } else {
1267 NextStepSuggestion::NoParentContext {
1268 message: format!("Task #{} '{}' has been completed.", id, task_name),
1269 completed_task_id: id,
1270 completed_task_name: task_name.clone(),
1271 }
1272 }
1273 }
1274 };
1275
1276 tx.commit().await?;
1277
1278 let completed_task = self.get_task(id).await?;
1280 self.notify_task_updated(&completed_task).await;
1281
1282 Ok(DoneTaskResponse {
1283 completed_task,
1284 workspace_status: WorkspaceStatus {
1285 current_task_id: None,
1286 },
1287 next_step_suggestion,
1288 })
1289 }
1290
1291 async fn check_task_exists(&self, id: i64) -> Result<()> {
1293 let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1294 .bind(id)
1295 .fetch_one(self.pool)
1296 .await?;
1297
1298 if !exists {
1299 return Err(IntentError::TaskNotFound(id));
1300 }
1301
1302 Ok(())
1303 }
1304
1305 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1307 let mut current_id = new_parent_id;
1308
1309 loop {
1310 if current_id == task_id {
1311 return Err(IntentError::CircularDependency {
1312 blocking_task_id: new_parent_id,
1313 blocked_task_id: task_id,
1314 });
1315 }
1316
1317 let parent: Option<i64> =
1318 sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1319 .bind(current_id)
1320 .fetch_optional(self.pool)
1321 .await?
1322 .flatten();
1323
1324 match parent {
1325 Some(pid) => current_id = pid,
1326 None => break,
1327 }
1328 }
1329
1330 Ok(())
1331 }
1332 pub async fn spawn_subtask(
1336 &self,
1337 name: &str,
1338 spec: Option<&str>,
1339 ) -> Result<SpawnSubtaskResponse> {
1340 let session_id = crate::workspace::resolve_session_id(None);
1342 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1343 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1344 )
1345 .bind(&session_id)
1346 .fetch_optional(self.pool)
1347 .await?
1348 .flatten();
1349
1350 let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1351 "No current task to create subtask under".to_string(),
1352 ))?;
1353
1354 let parent_name: String =
1356 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1357 .bind(parent_id)
1358 .fetch_one(self.pool)
1359 .await?;
1360
1361 let subtask = self
1363 .add_task(name, spec, Some(parent_id), Some("ai"))
1364 .await?;
1365
1366 self.start_task(subtask.id, false).await?;
1369
1370 Ok(SpawnSubtaskResponse {
1371 subtask: SubtaskInfo {
1372 id: subtask.id,
1373 name: subtask.name,
1374 parent_id,
1375 status: "doing".to_string(),
1376 },
1377 parent_task: ParentTaskInfo {
1378 id: parent_id,
1379 name: parent_name,
1380 },
1381 })
1382 }
1383
1384 pub async fn pick_next_tasks(
1397 &self,
1398 max_count: usize,
1399 capacity_limit: usize,
1400 ) -> Result<Vec<Task>> {
1401 let mut tx = self.pool.begin().await?;
1402
1403 let doing_count: i64 =
1405 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1406 .fetch_one(&mut *tx)
1407 .await?;
1408
1409 let available = capacity_limit.saturating_sub(doing_count as usize);
1411 if available == 0 {
1412 return Ok(vec![]);
1413 }
1414
1415 let limit = std::cmp::min(max_count, available);
1416
1417 let todo_tasks = sqlx::query_as::<_, Task>(
1419 r#"
1420 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1421 FROM tasks
1422 WHERE status = 'todo'
1423 ORDER BY
1424 COALESCE(priority, 0) ASC,
1425 COALESCE(complexity, 5) ASC,
1426 id ASC
1427 LIMIT ?
1428 "#,
1429 )
1430 .bind(limit as i64)
1431 .fetch_all(&mut *tx)
1432 .await?;
1433
1434 if todo_tasks.is_empty() {
1435 return Ok(vec![]);
1436 }
1437
1438 let now = Utc::now();
1439
1440 for task in &todo_tasks {
1442 sqlx::query(
1443 r#"
1444 UPDATE tasks
1445 SET status = 'doing',
1446 first_doing_at = COALESCE(first_doing_at, ?)
1447 WHERE id = ?
1448 "#,
1449 )
1450 .bind(now)
1451 .bind(task.id)
1452 .execute(&mut *tx)
1453 .await?;
1454 }
1455
1456 tx.commit().await?;
1457
1458 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1460 let placeholders = vec!["?"; task_ids.len()].join(",");
1461 let query = format!(
1462 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1463 FROM tasks WHERE id IN ({})
1464 ORDER BY
1465 COALESCE(priority, 0) ASC,
1466 COALESCE(complexity, 5) ASC,
1467 id ASC",
1468 placeholders
1469 );
1470
1471 let mut q = sqlx::query_as::<_, Task>(&query);
1472 for id in task_ids {
1473 q = q.bind(id);
1474 }
1475
1476 let updated_tasks = q.fetch_all(self.pool).await?;
1477 Ok(updated_tasks)
1478 }
1479
1480 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1489 let session_id = crate::workspace::resolve_session_id(None);
1491 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1492 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1493 )
1494 .bind(&session_id)
1495 .fetch_optional(self.pool)
1496 .await?
1497 .flatten();
1498
1499 if let Some(current_id) = current_task_id {
1500 let doing_subtasks = sqlx::query_as::<_, Task>(
1503 r#"
1504 SELECT id, parent_id, name, spec, status, complexity, priority,
1505 first_todo_at, first_doing_at, first_done_at, active_form, owner
1506 FROM tasks
1507 WHERE parent_id = ? AND status = 'doing'
1508 AND NOT EXISTS (
1509 SELECT 1 FROM dependencies d
1510 JOIN tasks bt ON d.blocking_task_id = bt.id
1511 WHERE d.blocked_task_id = tasks.id
1512 AND bt.status != 'done'
1513 )
1514 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1515 LIMIT 1
1516 "#,
1517 )
1518 .bind(current_id)
1519 .fetch_optional(self.pool)
1520 .await?;
1521
1522 if let Some(task) = doing_subtasks {
1523 return Ok(PickNextResponse::focused_subtask(task));
1524 }
1525
1526 let todo_subtasks = sqlx::query_as::<_, Task>(
1528 r#"
1529 SELECT id, parent_id, name, spec, status, complexity, priority,
1530 first_todo_at, first_doing_at, first_done_at, active_form, owner
1531 FROM tasks
1532 WHERE parent_id = ? AND status = 'todo'
1533 AND NOT EXISTS (
1534 SELECT 1 FROM dependencies d
1535 JOIN tasks bt ON d.blocking_task_id = bt.id
1536 WHERE d.blocked_task_id = tasks.id
1537 AND bt.status != 'done'
1538 )
1539 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1540 LIMIT 1
1541 "#,
1542 )
1543 .bind(current_id)
1544 .fetch_optional(self.pool)
1545 .await?;
1546
1547 if let Some(task) = todo_subtasks {
1548 return Ok(PickNextResponse::focused_subtask(task));
1549 }
1550 }
1551
1552 let doing_top_level = if let Some(current_id) = current_task_id {
1555 sqlx::query_as::<_, Task>(
1556 r#"
1557 SELECT id, parent_id, name, spec, status, complexity, priority,
1558 first_todo_at, first_doing_at, first_done_at, active_form, owner
1559 FROM tasks
1560 WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1561 AND NOT EXISTS (
1562 SELECT 1 FROM dependencies d
1563 JOIN tasks bt ON d.blocking_task_id = bt.id
1564 WHERE d.blocked_task_id = tasks.id
1565 AND bt.status != 'done'
1566 )
1567 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1568 LIMIT 1
1569 "#,
1570 )
1571 .bind(current_id)
1572 .fetch_optional(self.pool)
1573 .await?
1574 } else {
1575 sqlx::query_as::<_, Task>(
1576 r#"
1577 SELECT id, parent_id, name, spec, status, complexity, priority,
1578 first_todo_at, first_doing_at, first_done_at, active_form, owner
1579 FROM tasks
1580 WHERE parent_id IS NULL AND status = 'doing'
1581 AND NOT EXISTS (
1582 SELECT 1 FROM dependencies d
1583 JOIN tasks bt ON d.blocking_task_id = bt.id
1584 WHERE d.blocked_task_id = tasks.id
1585 AND bt.status != 'done'
1586 )
1587 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1588 LIMIT 1
1589 "#,
1590 )
1591 .fetch_optional(self.pool)
1592 .await?
1593 };
1594
1595 if let Some(task) = doing_top_level {
1596 return Ok(PickNextResponse::top_level_task(task));
1597 }
1598
1599 let todo_top_level = sqlx::query_as::<_, Task>(
1602 r#"
1603 SELECT id, parent_id, name, spec, status, complexity, priority,
1604 first_todo_at, first_doing_at, first_done_at, active_form, owner
1605 FROM tasks
1606 WHERE parent_id IS NULL AND status = 'todo'
1607 AND NOT EXISTS (
1608 SELECT 1 FROM dependencies d
1609 JOIN tasks bt ON d.blocking_task_id = bt.id
1610 WHERE d.blocked_task_id = tasks.id
1611 AND bt.status != 'done'
1612 )
1613 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1614 LIMIT 1
1615 "#,
1616 )
1617 .fetch_optional(self.pool)
1618 .await?;
1619
1620 if let Some(task) = todo_top_level {
1621 return Ok(PickNextResponse::top_level_task(task));
1622 }
1623
1624 let total_tasks: i64 =
1627 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1628 .fetch_one(self.pool)
1629 .await?;
1630
1631 if total_tasks == 0 {
1632 return Ok(PickNextResponse::no_tasks_in_project());
1633 }
1634
1635 let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1637 "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')",
1638 )
1639 .fetch_one(self.pool)
1640 .await?;
1641
1642 if todo_or_doing_count == 0 {
1643 return Ok(PickNextResponse::all_tasks_completed());
1644 }
1645
1646 Ok(PickNextResponse::no_available_todos())
1648 }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653 use super::*;
1654 use crate::events::EventManager;
1655 use crate::test_utils::test_helpers::TestContext;
1656 use crate::workspace::WorkspaceManager;
1657
1658 #[tokio::test]
1659 async fn test_get_stats_empty() {
1660 let ctx = TestContext::new().await;
1661 let manager = TaskManager::new(ctx.pool());
1662
1663 let stats = manager.get_stats().await.unwrap();
1664
1665 assert_eq!(stats.total_tasks, 0);
1666 assert_eq!(stats.todo, 0);
1667 assert_eq!(stats.doing, 0);
1668 assert_eq!(stats.done, 0);
1669 }
1670
1671 #[tokio::test]
1672 async fn test_get_stats_with_tasks() {
1673 let ctx = TestContext::new().await;
1674 let manager = TaskManager::new(ctx.pool());
1675
1676 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1678 let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
1679 let _task3 = manager.add_task("Task 3", None, None, None).await.unwrap();
1680
1681 manager
1683 .update_task(task1.id, None, None, None, Some("doing"), None, None)
1684 .await
1685 .unwrap();
1686 manager
1687 .update_task(task2.id, None, None, None, Some("done"), None, None)
1688 .await
1689 .unwrap();
1690 let stats = manager.get_stats().await.unwrap();
1693
1694 assert_eq!(stats.total_tasks, 3);
1695 assert_eq!(stats.todo, 1);
1696 assert_eq!(stats.doing, 1);
1697 assert_eq!(stats.done, 1);
1698 }
1699
1700 #[tokio::test]
1701 async fn test_add_task() {
1702 let ctx = TestContext::new().await;
1703 let manager = TaskManager::new(ctx.pool());
1704
1705 let task = manager
1706 .add_task("Test task", None, None, None)
1707 .await
1708 .unwrap();
1709
1710 assert_eq!(task.name, "Test task");
1711 assert_eq!(task.status, "todo");
1712 assert!(task.first_todo_at.is_some());
1713 assert!(task.first_doing_at.is_none());
1714 assert!(task.first_done_at.is_none());
1715 }
1716
1717 #[tokio::test]
1718 async fn test_add_task_with_spec() {
1719 let ctx = TestContext::new().await;
1720 let manager = TaskManager::new(ctx.pool());
1721
1722 let spec = "This is a task specification";
1723 let task = manager
1724 .add_task("Test task", Some(spec), None, None)
1725 .await
1726 .unwrap();
1727
1728 assert_eq!(task.name, "Test task");
1729 assert_eq!(task.spec.as_deref(), Some(spec));
1730 }
1731
1732 #[tokio::test]
1733 async fn test_add_task_with_parent() {
1734 let ctx = TestContext::new().await;
1735 let manager = TaskManager::new(ctx.pool());
1736
1737 let parent = manager
1738 .add_task("Parent task", None, None, None)
1739 .await
1740 .unwrap();
1741 let child = manager
1742 .add_task("Child task", None, Some(parent.id), None)
1743 .await
1744 .unwrap();
1745
1746 assert_eq!(child.parent_id, Some(parent.id));
1747 }
1748
1749 #[tokio::test]
1750 async fn test_get_task() {
1751 let ctx = TestContext::new().await;
1752 let manager = TaskManager::new(ctx.pool());
1753
1754 let created = manager
1755 .add_task("Test task", None, None, None)
1756 .await
1757 .unwrap();
1758 let retrieved = manager.get_task(created.id).await.unwrap();
1759
1760 assert_eq!(created.id, retrieved.id);
1761 assert_eq!(created.name, retrieved.name);
1762 }
1763
1764 #[tokio::test]
1765 async fn test_get_task_not_found() {
1766 let ctx = TestContext::new().await;
1767 let manager = TaskManager::new(ctx.pool());
1768
1769 let result = manager.get_task(999).await;
1770 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1771 }
1772
1773 #[tokio::test]
1774 async fn test_update_task_name() {
1775 let ctx = TestContext::new().await;
1776 let manager = TaskManager::new(ctx.pool());
1777
1778 let task = manager
1779 .add_task("Original name", None, None, None)
1780 .await
1781 .unwrap();
1782 let updated = manager
1783 .update_task(task.id, Some("New name"), None, None, None, None, None)
1784 .await
1785 .unwrap();
1786
1787 assert_eq!(updated.name, "New name");
1788 }
1789
1790 #[tokio::test]
1791 async fn test_update_task_status() {
1792 let ctx = TestContext::new().await;
1793 let manager = TaskManager::new(ctx.pool());
1794
1795 let task = manager
1796 .add_task("Test task", None, None, None)
1797 .await
1798 .unwrap();
1799 let updated = manager
1800 .update_task(task.id, None, None, None, Some("doing"), None, None)
1801 .await
1802 .unwrap();
1803
1804 assert_eq!(updated.status, "doing");
1805 assert!(updated.first_doing_at.is_some());
1806 }
1807
1808 #[tokio::test]
1809 async fn test_delete_task() {
1810 let ctx = TestContext::new().await;
1811 let manager = TaskManager::new(ctx.pool());
1812
1813 let task = manager
1814 .add_task("Test task", None, None, None)
1815 .await
1816 .unwrap();
1817 manager.delete_task(task.id).await.unwrap();
1818
1819 let result = manager.get_task(task.id).await;
1820 assert!(result.is_err());
1821 }
1822
1823 #[tokio::test]
1824 async fn test_find_tasks_by_status() {
1825 let ctx = TestContext::new().await;
1826 let manager = TaskManager::new(ctx.pool());
1827
1828 manager
1829 .add_task("Todo task", None, None, None)
1830 .await
1831 .unwrap();
1832 let doing_task = manager
1833 .add_task("Doing task", None, None, None)
1834 .await
1835 .unwrap();
1836 manager
1837 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1838 .await
1839 .unwrap();
1840
1841 let todo_result = manager
1842 .find_tasks(Some("todo"), None, None, None, None)
1843 .await
1844 .unwrap();
1845 let doing_result = manager
1846 .find_tasks(Some("doing"), None, None, None, None)
1847 .await
1848 .unwrap();
1849
1850 assert_eq!(todo_result.tasks.len(), 1);
1851 assert_eq!(doing_result.tasks.len(), 1);
1852 assert_eq!(doing_result.tasks[0].status, "doing");
1853 }
1854
1855 #[tokio::test]
1856 async fn test_find_tasks_by_parent() {
1857 let ctx = TestContext::new().await;
1858 let manager = TaskManager::new(ctx.pool());
1859
1860 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1861 manager
1862 .add_task("Child 1", None, Some(parent.id), None)
1863 .await
1864 .unwrap();
1865 manager
1866 .add_task("Child 2", None, Some(parent.id), None)
1867 .await
1868 .unwrap();
1869
1870 let result = manager
1871 .find_tasks(None, Some(Some(parent.id)), None, None, None)
1872 .await
1873 .unwrap();
1874
1875 assert_eq!(result.tasks.len(), 2);
1876 }
1877
1878 #[tokio::test]
1879 async fn test_start_task() {
1880 let ctx = TestContext::new().await;
1881 let manager = TaskManager::new(ctx.pool());
1882
1883 let task = manager
1884 .add_task("Test task", None, None, None)
1885 .await
1886 .unwrap();
1887 let started = manager.start_task(task.id, false).await.unwrap();
1888
1889 assert_eq!(started.task.status, "doing");
1890 assert!(started.task.first_doing_at.is_some());
1891
1892 let session_id = crate::workspace::resolve_session_id(None);
1894 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1895 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1896 )
1897 .bind(&session_id)
1898 .fetch_optional(ctx.pool())
1899 .await
1900 .unwrap()
1901 .flatten();
1902
1903 assert_eq!(current, Some(task.id));
1904 }
1905
1906 #[tokio::test]
1907 async fn test_start_task_with_events() {
1908 let ctx = TestContext::new().await;
1909 let manager = TaskManager::new(ctx.pool());
1910
1911 let task = manager
1912 .add_task("Test task", None, None, None)
1913 .await
1914 .unwrap();
1915
1916 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1918 .bind(task.id)
1919 .bind("test")
1920 .bind("test event")
1921 .execute(ctx.pool())
1922 .await
1923 .unwrap();
1924
1925 let started = manager.start_task(task.id, true).await.unwrap();
1926
1927 assert!(started.events_summary.is_some());
1928 let summary = started.events_summary.unwrap();
1929 assert_eq!(summary.total_count, 1);
1930 }
1931
1932 #[tokio::test]
1933 async fn test_done_task() {
1934 let ctx = TestContext::new().await;
1935 let manager = TaskManager::new(ctx.pool());
1936
1937 let task = manager
1938 .add_task("Test task", None, None, None)
1939 .await
1940 .unwrap();
1941 manager.start_task(task.id, false).await.unwrap();
1942 let response = manager.done_task(false).await.unwrap();
1943
1944 assert_eq!(response.completed_task.status, "done");
1945 assert!(response.completed_task.first_done_at.is_some());
1946 assert_eq!(response.workspace_status.current_task_id, None);
1947
1948 match response.next_step_suggestion {
1950 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1951 _ => panic!("Expected WorkspaceIsClear suggestion"),
1952 }
1953
1954 let session_id = crate::workspace::resolve_session_id(None);
1956 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1957 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1958 )
1959 .bind(&session_id)
1960 .fetch_optional(ctx.pool())
1961 .await
1962 .unwrap()
1963 .flatten();
1964
1965 assert!(current.is_none());
1966 }
1967
1968 #[tokio::test]
1969 async fn test_done_task_with_uncompleted_children() {
1970 let ctx = TestContext::new().await;
1971 let manager = TaskManager::new(ctx.pool());
1972
1973 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1974 manager
1975 .add_task("Child", None, Some(parent.id), None)
1976 .await
1977 .unwrap();
1978
1979 manager.start_task(parent.id, false).await.unwrap();
1981
1982 let result = manager.done_task(false).await;
1983 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1984 }
1985
1986 #[tokio::test]
1987 async fn test_done_task_with_completed_children() {
1988 let ctx = TestContext::new().await;
1989 let manager = TaskManager::new(ctx.pool());
1990
1991 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1992 let child = manager
1993 .add_task("Child", None, Some(parent.id), None)
1994 .await
1995 .unwrap();
1996
1997 manager.start_task(child.id, false).await.unwrap();
1999 let child_response = manager.done_task(false).await.unwrap();
2000
2001 match child_response.next_step_suggestion {
2003 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
2004 assert_eq!(parent_task_id, parent.id);
2005 },
2006 _ => panic!("Expected ParentIsReady suggestion"),
2007 }
2008
2009 manager.start_task(parent.id, false).await.unwrap();
2011 let parent_response = manager.done_task(false).await.unwrap();
2012 assert_eq!(parent_response.completed_task.status, "done");
2013
2014 match parent_response.next_step_suggestion {
2016 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
2017 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2018 }
2019 }
2020
2021 #[tokio::test]
2022 async fn test_circular_dependency() {
2023 let ctx = TestContext::new().await;
2024 let manager = TaskManager::new(ctx.pool());
2025
2026 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
2027 let task2 = manager
2028 .add_task("Task 2", None, Some(task1.id), None)
2029 .await
2030 .unwrap();
2031
2032 let result = manager
2034 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
2035 .await;
2036
2037 assert!(matches!(
2038 result,
2039 Err(IntentError::CircularDependency { .. })
2040 ));
2041 }
2042
2043 #[tokio::test]
2044 async fn test_invalid_parent_id() {
2045 let ctx = TestContext::new().await;
2046 let manager = TaskManager::new(ctx.pool());
2047
2048 let result = manager.add_task("Test", None, Some(999), None).await;
2049 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2050 }
2051
2052 #[tokio::test]
2053 async fn test_update_task_complexity_and_priority() {
2054 let ctx = TestContext::new().await;
2055 let manager = TaskManager::new(ctx.pool());
2056
2057 let task = manager
2058 .add_task("Test task", None, None, None)
2059 .await
2060 .unwrap();
2061 let updated = manager
2062 .update_task(task.id, None, None, None, None, Some(8), Some(10))
2063 .await
2064 .unwrap();
2065
2066 assert_eq!(updated.complexity, Some(8));
2067 assert_eq!(updated.priority, Some(10));
2068 }
2069
2070 #[tokio::test]
2071 async fn test_spawn_subtask() {
2072 let ctx = TestContext::new().await;
2073 let manager = TaskManager::new(ctx.pool());
2074
2075 let parent = manager
2077 .add_task("Parent task", None, None, None)
2078 .await
2079 .unwrap();
2080 manager.start_task(parent.id, false).await.unwrap();
2081
2082 let response = manager
2084 .spawn_subtask("Child task", Some("Details"))
2085 .await
2086 .unwrap();
2087
2088 assert_eq!(response.subtask.parent_id, parent.id);
2089 assert_eq!(response.subtask.name, "Child task");
2090 assert_eq!(response.subtask.status, "doing");
2091 assert_eq!(response.parent_task.id, parent.id);
2092 assert_eq!(response.parent_task.name, "Parent task");
2093
2094 let session_id = crate::workspace::resolve_session_id(None);
2096 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2097 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2098 )
2099 .bind(&session_id)
2100 .fetch_optional(ctx.pool())
2101 .await
2102 .unwrap()
2103 .flatten();
2104
2105 assert_eq!(current, Some(response.subtask.id));
2106
2107 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
2109 assert_eq!(retrieved.status, "doing");
2110 }
2111
2112 #[tokio::test]
2113 async fn test_spawn_subtask_no_current_task() {
2114 let ctx = TestContext::new().await;
2115 let manager = TaskManager::new(ctx.pool());
2116
2117 let result = manager.spawn_subtask("Child", None).await;
2119 assert!(result.is_err());
2120 }
2121
2122 #[tokio::test]
2123 async fn test_pick_next_tasks_basic() {
2124 let ctx = TestContext::new().await;
2125 let manager = TaskManager::new(ctx.pool());
2126
2127 for i in 1..=10 {
2129 manager
2130 .add_task(&format!("Task {}", i), None, None, None)
2131 .await
2132 .unwrap();
2133 }
2134
2135 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
2137
2138 assert_eq!(picked.len(), 5);
2139 for task in &picked {
2140 assert_eq!(task.status, "doing");
2141 assert!(task.first_doing_at.is_some());
2142 }
2143
2144 let doing_count: i64 =
2146 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2147 .fetch_one(ctx.pool())
2148 .await
2149 .unwrap();
2150
2151 assert_eq!(doing_count, 5);
2152 }
2153
2154 #[tokio::test]
2155 async fn test_pick_next_tasks_with_existing_doing() {
2156 let ctx = TestContext::new().await;
2157 let manager = TaskManager::new(ctx.pool());
2158
2159 for i in 1..=10 {
2161 manager
2162 .add_task(&format!("Task {}", i), None, None, None)
2163 .await
2164 .unwrap();
2165 }
2166
2167 let result = manager
2169 .find_tasks(Some("todo"), None, None, None, None)
2170 .await
2171 .unwrap();
2172 manager.start_task(result.tasks[0].id, false).await.unwrap();
2173 manager.start_task(result.tasks[1].id, false).await.unwrap();
2174
2175 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2177
2178 assert_eq!(picked.len(), 3);
2180
2181 let doing_count: i64 =
2183 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2184 .fetch_one(ctx.pool())
2185 .await
2186 .unwrap();
2187
2188 assert_eq!(doing_count, 5);
2189 }
2190
2191 #[tokio::test]
2192 async fn test_pick_next_tasks_at_capacity() {
2193 let ctx = TestContext::new().await;
2194 let manager = TaskManager::new(ctx.pool());
2195
2196 for i in 1..=10 {
2198 manager
2199 .add_task(&format!("Task {}", i), None, None, None)
2200 .await
2201 .unwrap();
2202 }
2203
2204 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2206 assert_eq!(first_batch.len(), 5);
2207
2208 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2210 assert_eq!(second_batch.len(), 0);
2211 }
2212
2213 #[tokio::test]
2214 async fn test_pick_next_tasks_priority_ordering() {
2215 let ctx = TestContext::new().await;
2216 let manager = TaskManager::new(ctx.pool());
2217
2218 let low = manager
2220 .add_task("Low priority", None, None, None)
2221 .await
2222 .unwrap();
2223 manager
2224 .update_task(low.id, None, None, None, None, None, Some(1))
2225 .await
2226 .unwrap();
2227
2228 let high = manager
2229 .add_task("High priority", None, None, None)
2230 .await
2231 .unwrap();
2232 manager
2233 .update_task(high.id, None, None, None, None, None, Some(10))
2234 .await
2235 .unwrap();
2236
2237 let medium = manager
2238 .add_task("Medium priority", None, None, None)
2239 .await
2240 .unwrap();
2241 manager
2242 .update_task(medium.id, None, None, None, None, None, Some(5))
2243 .await
2244 .unwrap();
2245
2246 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2248
2249 assert_eq!(picked.len(), 3);
2251 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
2255
2256 #[tokio::test]
2257 async fn test_pick_next_tasks_complexity_ordering() {
2258 let ctx = TestContext::new().await;
2259 let manager = TaskManager::new(ctx.pool());
2260
2261 let complex = manager.add_task("Complex", None, None, None).await.unwrap();
2263 manager
2264 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
2265 .await
2266 .unwrap();
2267
2268 let simple = manager.add_task("Simple", None, None, None).await.unwrap();
2269 manager
2270 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
2271 .await
2272 .unwrap();
2273
2274 let medium = manager.add_task("Medium", None, None, None).await.unwrap();
2275 manager
2276 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
2277 .await
2278 .unwrap();
2279
2280 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2282
2283 assert_eq!(picked.len(), 3);
2285 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
2289
2290 #[tokio::test]
2291 async fn test_done_task_sibling_tasks_remain() {
2292 let ctx = TestContext::new().await;
2293 let manager = TaskManager::new(ctx.pool());
2294
2295 let parent = manager
2297 .add_task("Parent Task", None, None, None)
2298 .await
2299 .unwrap();
2300 let child1 = manager
2301 .add_task("Child 1", None, Some(parent.id), None)
2302 .await
2303 .unwrap();
2304 let child2 = manager
2305 .add_task("Child 2", None, Some(parent.id), None)
2306 .await
2307 .unwrap();
2308 let _child3 = manager
2309 .add_task("Child 3", None, Some(parent.id), None)
2310 .await
2311 .unwrap();
2312
2313 manager.start_task(child1.id, false).await.unwrap();
2315 let response = manager.done_task(false).await.unwrap();
2316
2317 match response.next_step_suggestion {
2319 NextStepSuggestion::SiblingTasksRemain {
2320 parent_task_id,
2321 remaining_siblings_count,
2322 ..
2323 } => {
2324 assert_eq!(parent_task_id, parent.id);
2325 assert_eq!(remaining_siblings_count, 2); },
2327 _ => panic!("Expected SiblingTasksRemain suggestion"),
2328 }
2329
2330 manager.start_task(child2.id, false).await.unwrap();
2332 let response2 = manager.done_task(false).await.unwrap();
2333
2334 match response2.next_step_suggestion {
2336 NextStepSuggestion::SiblingTasksRemain {
2337 remaining_siblings_count,
2338 ..
2339 } => {
2340 assert_eq!(remaining_siblings_count, 1); },
2342 _ => panic!("Expected SiblingTasksRemain suggestion"),
2343 }
2344 }
2345
2346 #[tokio::test]
2347 async fn test_done_task_top_level_with_children() {
2348 let ctx = TestContext::new().await;
2349 let manager = TaskManager::new(ctx.pool());
2350
2351 let parent = manager
2353 .add_task("Epic Task", None, None, None)
2354 .await
2355 .unwrap();
2356 let child = manager
2357 .add_task("Sub Task", None, Some(parent.id), None)
2358 .await
2359 .unwrap();
2360
2361 manager.start_task(child.id, false).await.unwrap();
2363 manager.done_task(false).await.unwrap();
2364
2365 manager.start_task(parent.id, false).await.unwrap();
2367 let response = manager.done_task(false).await.unwrap();
2368
2369 match response.next_step_suggestion {
2371 NextStepSuggestion::TopLevelTaskCompleted {
2372 completed_task_id,
2373 completed_task_name,
2374 ..
2375 } => {
2376 assert_eq!(completed_task_id, parent.id);
2377 assert_eq!(completed_task_name, "Epic Task");
2378 },
2379 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2380 }
2381 }
2382
2383 #[tokio::test]
2384 async fn test_done_task_no_parent_context() {
2385 let ctx = TestContext::new().await;
2386 let manager = TaskManager::new(ctx.pool());
2387
2388 let task1 = manager
2390 .add_task("Standalone Task 1", None, None, None)
2391 .await
2392 .unwrap();
2393 let _task2 = manager
2394 .add_task("Standalone Task 2", None, None, None)
2395 .await
2396 .unwrap();
2397
2398 manager.start_task(task1.id, false).await.unwrap();
2400 let response = manager.done_task(false).await.unwrap();
2401
2402 match response.next_step_suggestion {
2404 NextStepSuggestion::NoParentContext {
2405 completed_task_id,
2406 completed_task_name,
2407 ..
2408 } => {
2409 assert_eq!(completed_task_id, task1.id);
2410 assert_eq!(completed_task_name, "Standalone Task 1");
2411 },
2412 _ => panic!("Expected NoParentContext suggestion"),
2413 }
2414 }
2415
2416 #[tokio::test]
2417 async fn test_pick_next_focused_subtask() {
2418 let ctx = TestContext::new().await;
2419 let manager = TaskManager::new(ctx.pool());
2420
2421 let parent = manager
2423 .add_task("Parent task", None, None, None)
2424 .await
2425 .unwrap();
2426 manager.start_task(parent.id, false).await.unwrap();
2427
2428 let subtask1 = manager
2430 .add_task("Subtask 1", None, Some(parent.id), None)
2431 .await
2432 .unwrap();
2433 let subtask2 = manager
2434 .add_task("Subtask 2", None, Some(parent.id), None)
2435 .await
2436 .unwrap();
2437
2438 manager
2440 .update_task(subtask1.id, None, None, None, None, None, Some(2))
2441 .await
2442 .unwrap();
2443 manager
2444 .update_task(subtask2.id, None, None, None, None, None, Some(1))
2445 .await
2446 .unwrap();
2447
2448 let response = manager.pick_next().await.unwrap();
2450
2451 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2452 assert!(response.task.is_some());
2453 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2454 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2455 }
2456
2457 #[tokio::test]
2458 async fn test_pick_next_top_level_task() {
2459 let ctx = TestContext::new().await;
2460 let manager = TaskManager::new(ctx.pool());
2461
2462 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
2464 let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
2465
2466 manager
2468 .update_task(task1.id, None, None, None, None, None, Some(5))
2469 .await
2470 .unwrap();
2471 manager
2472 .update_task(task2.id, None, None, None, None, None, Some(3))
2473 .await
2474 .unwrap();
2475
2476 let response = manager.pick_next().await.unwrap();
2478
2479 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2480 assert!(response.task.is_some());
2481 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2482 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2483 }
2484
2485 #[tokio::test]
2486 async fn test_pick_next_no_tasks() {
2487 let ctx = TestContext::new().await;
2488 let manager = TaskManager::new(ctx.pool());
2489
2490 let response = manager.pick_next().await.unwrap();
2492
2493 assert_eq!(response.suggestion_type, "NONE");
2494 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2495 assert!(response.message.is_some());
2496 }
2497
2498 #[tokio::test]
2499 async fn test_pick_next_all_completed() {
2500 let ctx = TestContext::new().await;
2501 let manager = TaskManager::new(ctx.pool());
2502
2503 let task = manager.add_task("Task 1", None, None, None).await.unwrap();
2505 manager.start_task(task.id, false).await.unwrap();
2506 manager.done_task(false).await.unwrap();
2507
2508 let response = manager.pick_next().await.unwrap();
2510
2511 assert_eq!(response.suggestion_type, "NONE");
2512 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2513 assert!(response.message.is_some());
2514 }
2515
2516 #[tokio::test]
2517 async fn test_pick_next_no_available_todos() {
2518 let ctx = TestContext::new().await;
2519 let manager = TaskManager::new(ctx.pool());
2520
2521 let parent = manager
2523 .add_task("Parent task", None, None, None)
2524 .await
2525 .unwrap();
2526 manager.start_task(parent.id, false).await.unwrap();
2527
2528 let subtask = manager
2530 .add_task("Subtask", None, Some(parent.id), None)
2531 .await
2532 .unwrap();
2533 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2535 .bind(subtask.id)
2536 .execute(ctx.pool())
2537 .await
2538 .unwrap();
2539
2540 let session_id = crate::workspace::resolve_session_id(None);
2542 sqlx::query(
2543 r#"
2544 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
2545 VALUES (?, ?, datetime('now'), datetime('now'))
2546 ON CONFLICT(session_id) DO UPDATE SET
2547 current_task_id = excluded.current_task_id,
2548 last_active_at = datetime('now')
2549 "#,
2550 )
2551 .bind(&session_id)
2552 .bind(subtask.id)
2553 .execute(ctx.pool())
2554 .await
2555 .unwrap();
2556
2557 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2559 .bind(parent.id)
2560 .execute(ctx.pool())
2561 .await
2562 .unwrap();
2563
2564 let response = manager.pick_next().await.unwrap();
2567
2568 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2569 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2570 assert_eq!(response.task.as_ref().unwrap().status, "doing");
2571 }
2572
2573 #[tokio::test]
2574 async fn test_pick_next_priority_ordering() {
2575 let ctx = TestContext::new().await;
2576 let manager = TaskManager::new(ctx.pool());
2577
2578 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2580 manager.start_task(parent.id, false).await.unwrap();
2581
2582 let sub1 = manager
2584 .add_task("Priority 10", None, Some(parent.id), None)
2585 .await
2586 .unwrap();
2587 manager
2588 .update_task(sub1.id, None, None, None, None, None, Some(10))
2589 .await
2590 .unwrap();
2591
2592 let sub2 = manager
2593 .add_task("Priority 1", None, Some(parent.id), None)
2594 .await
2595 .unwrap();
2596 manager
2597 .update_task(sub2.id, None, None, None, None, None, Some(1))
2598 .await
2599 .unwrap();
2600
2601 let sub3 = manager
2602 .add_task("Priority 5", None, Some(parent.id), None)
2603 .await
2604 .unwrap();
2605 manager
2606 .update_task(sub3.id, None, None, None, None, None, Some(5))
2607 .await
2608 .unwrap();
2609
2610 let response = manager.pick_next().await.unwrap();
2612
2613 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2614 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2615 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2616 }
2617
2618 #[tokio::test]
2619 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2620 let ctx = TestContext::new().await;
2621 let manager = TaskManager::new(ctx.pool());
2622
2623 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2625 manager.start_task(parent.id, false).await.unwrap();
2626
2627 let top_level = manager
2629 .add_task("Top level task", None, None, None)
2630 .await
2631 .unwrap();
2632
2633 let response = manager.pick_next().await.unwrap();
2635
2636 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2637 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2638 }
2639
2640 #[tokio::test]
2643 async fn test_get_task_with_events() {
2644 let ctx = TestContext::new().await;
2645 let task_mgr = TaskManager::new(ctx.pool());
2646 let event_mgr = EventManager::new(ctx.pool());
2647
2648 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2649
2650 event_mgr
2652 .add_event(task.id, "progress", "Event 1")
2653 .await
2654 .unwrap();
2655 event_mgr
2656 .add_event(task.id, "decision", "Event 2")
2657 .await
2658 .unwrap();
2659
2660 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2661
2662 assert_eq!(result.task.id, task.id);
2663 assert!(result.events_summary.is_some());
2664
2665 let summary = result.events_summary.unwrap();
2666 assert_eq!(summary.total_count, 2);
2667 assert_eq!(summary.recent_events.len(), 2);
2668 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2670 }
2671
2672 #[tokio::test]
2673 async fn test_get_task_with_events_nonexistent() {
2674 let ctx = TestContext::new().await;
2675 let task_mgr = TaskManager::new(ctx.pool());
2676
2677 let result = task_mgr.get_task_with_events(999).await;
2678 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2679 }
2680
2681 #[tokio::test]
2682 async fn test_get_task_with_many_events() {
2683 let ctx = TestContext::new().await;
2684 let task_mgr = TaskManager::new(ctx.pool());
2685 let event_mgr = EventManager::new(ctx.pool());
2686
2687 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2688
2689 for i in 0..20 {
2691 event_mgr
2692 .add_event(task.id, "test", &format!("Event {}", i))
2693 .await
2694 .unwrap();
2695 }
2696
2697 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2698 let summary = result.events_summary.unwrap();
2699
2700 assert_eq!(summary.total_count, 20);
2701 assert_eq!(summary.recent_events.len(), 10); }
2703
2704 #[tokio::test]
2705 async fn test_get_task_with_no_events() {
2706 let ctx = TestContext::new().await;
2707 let task_mgr = TaskManager::new(ctx.pool());
2708
2709 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2710
2711 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2712 let summary = result.events_summary.unwrap();
2713
2714 assert_eq!(summary.total_count, 0);
2715 assert_eq!(summary.recent_events.len(), 0);
2716 }
2717
2718 #[tokio::test]
2719 async fn test_pick_next_tasks_zero_capacity() {
2720 let ctx = TestContext::new().await;
2721 let task_mgr = TaskManager::new(ctx.pool());
2722
2723 task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2724
2725 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2727 assert_eq!(results.len(), 0);
2728 }
2729
2730 #[tokio::test]
2731 async fn test_pick_next_tasks_capacity_exceeds_available() {
2732 let ctx = TestContext::new().await;
2733 let task_mgr = TaskManager::new(ctx.pool());
2734
2735 task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2736 task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2737
2738 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2740 assert_eq!(results.len(), 2); }
2742
2743 #[tokio::test]
2746 async fn test_get_task_context_root_task_no_relations() {
2747 let ctx = TestContext::new().await;
2748 let task_mgr = TaskManager::new(ctx.pool());
2749
2750 let task = task_mgr
2752 .add_task("Root task", None, None, None)
2753 .await
2754 .unwrap();
2755
2756 let context = task_mgr.get_task_context(task.id).await.unwrap();
2757
2758 assert_eq!(context.task.id, task.id);
2760 assert_eq!(context.task.name, "Root task");
2761
2762 assert_eq!(context.ancestors.len(), 0);
2764
2765 assert_eq!(context.siblings.len(), 0);
2767
2768 assert_eq!(context.children.len(), 0);
2770 }
2771
2772 #[tokio::test]
2773 async fn test_get_task_context_with_siblings() {
2774 let ctx = TestContext::new().await;
2775 let task_mgr = TaskManager::new(ctx.pool());
2776
2777 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2779 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2780 let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2781
2782 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2783
2784 assert_eq!(context.task.id, task2.id);
2786
2787 assert_eq!(context.ancestors.len(), 0);
2789
2790 assert_eq!(context.siblings.len(), 2);
2792 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2793 assert!(sibling_ids.contains(&task1.id));
2794 assert!(sibling_ids.contains(&task3.id));
2795 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2799 }
2800
2801 #[tokio::test]
2802 async fn test_get_task_context_with_parent() {
2803 let ctx = TestContext::new().await;
2804 let task_mgr = TaskManager::new(ctx.pool());
2805
2806 let parent = task_mgr
2808 .add_task("Parent task", None, None, None)
2809 .await
2810 .unwrap();
2811 let child = task_mgr
2812 .add_task("Child task", None, Some(parent.id), None)
2813 .await
2814 .unwrap();
2815
2816 let context = task_mgr.get_task_context(child.id).await.unwrap();
2817
2818 assert_eq!(context.task.id, child.id);
2820 assert_eq!(context.task.parent_id, Some(parent.id));
2821
2822 assert_eq!(context.ancestors.len(), 1);
2824 assert_eq!(context.ancestors[0].id, parent.id);
2825 assert_eq!(context.ancestors[0].name, "Parent task");
2826
2827 assert_eq!(context.siblings.len(), 0);
2829
2830 assert_eq!(context.children.len(), 0);
2832 }
2833
2834 #[tokio::test]
2835 async fn test_get_task_context_with_children() {
2836 let ctx = TestContext::new().await;
2837 let task_mgr = TaskManager::new(ctx.pool());
2838
2839 let parent = task_mgr
2841 .add_task("Parent task", None, None, None)
2842 .await
2843 .unwrap();
2844 let child1 = task_mgr
2845 .add_task("Child 1", None, Some(parent.id), None)
2846 .await
2847 .unwrap();
2848 let child2 = task_mgr
2849 .add_task("Child 2", None, Some(parent.id), None)
2850 .await
2851 .unwrap();
2852 let child3 = task_mgr
2853 .add_task("Child 3", None, Some(parent.id), None)
2854 .await
2855 .unwrap();
2856
2857 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2858
2859 assert_eq!(context.task.id, parent.id);
2861
2862 assert_eq!(context.ancestors.len(), 0);
2864
2865 assert_eq!(context.siblings.len(), 0);
2867
2868 assert_eq!(context.children.len(), 3);
2870 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2871 assert!(child_ids.contains(&child1.id));
2872 assert!(child_ids.contains(&child2.id));
2873 assert!(child_ids.contains(&child3.id));
2874 }
2875
2876 #[tokio::test]
2877 async fn test_get_task_context_multi_level_hierarchy() {
2878 let ctx = TestContext::new().await;
2879 let task_mgr = TaskManager::new(ctx.pool());
2880
2881 let grandparent = task_mgr
2883 .add_task("Grandparent", None, None, None)
2884 .await
2885 .unwrap();
2886 let parent = task_mgr
2887 .add_task("Parent", None, Some(grandparent.id), None)
2888 .await
2889 .unwrap();
2890 let child = task_mgr
2891 .add_task("Child", None, Some(parent.id), None)
2892 .await
2893 .unwrap();
2894
2895 let context = task_mgr.get_task_context(child.id).await.unwrap();
2896
2897 assert_eq!(context.task.id, child.id);
2899
2900 assert_eq!(context.ancestors.len(), 2);
2902 assert_eq!(context.ancestors[0].id, parent.id);
2903 assert_eq!(context.ancestors[0].name, "Parent");
2904 assert_eq!(context.ancestors[1].id, grandparent.id);
2905 assert_eq!(context.ancestors[1].name, "Grandparent");
2906
2907 assert_eq!(context.siblings.len(), 0);
2909
2910 assert_eq!(context.children.len(), 0);
2912 }
2913
2914 #[tokio::test]
2915 async fn test_get_task_context_complex_family_tree() {
2916 let ctx = TestContext::new().await;
2917 let task_mgr = TaskManager::new(ctx.pool());
2918
2919 let root = task_mgr.add_task("Root", None, None, None).await.unwrap();
2927 let child1 = task_mgr
2928 .add_task("Child1", None, Some(root.id), None)
2929 .await
2930 .unwrap();
2931 let child2 = task_mgr
2932 .add_task("Child2", None, Some(root.id), None)
2933 .await
2934 .unwrap();
2935 let grandchild1 = task_mgr
2936 .add_task("Grandchild1", None, Some(child1.id), None)
2937 .await
2938 .unwrap();
2939 let grandchild2 = task_mgr
2940 .add_task("Grandchild2", None, Some(child1.id), None)
2941 .await
2942 .unwrap();
2943
2944 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2946
2947 assert_eq!(context.task.id, grandchild2.id);
2949
2950 assert_eq!(context.ancestors.len(), 2);
2952 assert_eq!(context.ancestors[0].id, child1.id);
2953 assert_eq!(context.ancestors[1].id, root.id);
2954
2955 assert_eq!(context.siblings.len(), 1);
2957 assert_eq!(context.siblings[0].id, grandchild1.id);
2958
2959 assert_eq!(context.children.len(), 0);
2961
2962 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2964 assert_eq!(context_child1.ancestors.len(), 1);
2965 assert_eq!(context_child1.ancestors[0].id, root.id);
2966 assert_eq!(context_child1.siblings.len(), 1);
2967 assert_eq!(context_child1.siblings[0].id, child2.id);
2968 assert_eq!(context_child1.children.len(), 2);
2969 }
2970
2971 #[tokio::test]
2972 async fn test_get_task_context_respects_priority_ordering() {
2973 let ctx = TestContext::new().await;
2974 let task_mgr = TaskManager::new(ctx.pool());
2975
2976 let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2978
2979 let child_low = task_mgr
2981 .add_task("Low priority", None, Some(parent.id), None)
2982 .await
2983 .unwrap();
2984 let _ = task_mgr
2985 .update_task(child_low.id, None, None, None, None, None, Some(10))
2986 .await
2987 .unwrap();
2988
2989 let child_high = task_mgr
2990 .add_task("High priority", None, Some(parent.id), None)
2991 .await
2992 .unwrap();
2993 let _ = task_mgr
2994 .update_task(child_high.id, None, None, None, None, None, Some(1))
2995 .await
2996 .unwrap();
2997
2998 let child_medium = task_mgr
2999 .add_task("Medium priority", None, Some(parent.id), None)
3000 .await
3001 .unwrap();
3002 let _ = task_mgr
3003 .update_task(child_medium.id, None, None, None, None, None, Some(5))
3004 .await
3005 .unwrap();
3006
3007 let context = task_mgr.get_task_context(parent.id).await.unwrap();
3008
3009 assert_eq!(context.children.len(), 3);
3011 assert_eq!(context.children[0].priority, Some(1));
3012 assert_eq!(context.children[1].priority, Some(5));
3013 assert_eq!(context.children[2].priority, Some(10));
3014 }
3015
3016 #[tokio::test]
3017 async fn test_get_task_context_nonexistent_task() {
3018 let ctx = TestContext::new().await;
3019 let task_mgr = TaskManager::new(ctx.pool());
3020
3021 let result = task_mgr.get_task_context(99999).await;
3022 assert!(result.is_err());
3023 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
3024 }
3025
3026 #[tokio::test]
3027 async fn test_get_task_context_handles_null_priority() {
3028 let ctx = TestContext::new().await;
3029 let task_mgr = TaskManager::new(ctx.pool());
3030
3031 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
3033 let _ = task_mgr
3034 .update_task(task1.id, None, None, None, None, None, Some(1))
3035 .await
3036 .unwrap();
3037
3038 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
3039 let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
3042 let _ = task_mgr
3043 .update_task(task3.id, None, None, None, None, None, Some(5))
3044 .await
3045 .unwrap();
3046
3047 let context = task_mgr.get_task_context(task2.id).await.unwrap();
3048
3049 assert_eq!(context.siblings.len(), 2);
3051 assert_eq!(context.siblings[0].id, task1.id);
3053 assert_eq!(context.siblings[0].priority, Some(1));
3054 assert_eq!(context.siblings[1].id, task3.id);
3056 assert_eq!(context.siblings[1].priority, Some(5));
3057 }
3058
3059 #[tokio::test]
3060 async fn test_pick_next_tasks_priority_order() {
3061 let ctx = TestContext::new().await;
3062 let task_mgr = TaskManager::new(ctx.pool());
3063
3064 let critical = task_mgr
3066 .add_task("Critical Task", None, None, None)
3067 .await
3068 .unwrap();
3069 task_mgr
3070 .update_task(critical.id, None, None, None, None, None, Some(1))
3071 .await
3072 .unwrap();
3073
3074 let low = task_mgr
3075 .add_task("Low Task", None, None, None)
3076 .await
3077 .unwrap();
3078 task_mgr
3079 .update_task(low.id, None, None, None, None, None, Some(4))
3080 .await
3081 .unwrap();
3082
3083 let high = task_mgr
3084 .add_task("High Task", None, None, None)
3085 .await
3086 .unwrap();
3087 task_mgr
3088 .update_task(high.id, None, None, None, None, None, Some(2))
3089 .await
3090 .unwrap();
3091
3092 let medium = task_mgr
3093 .add_task("Medium Task", None, None, None)
3094 .await
3095 .unwrap();
3096 task_mgr
3097 .update_task(medium.id, None, None, None, None, None, Some(3))
3098 .await
3099 .unwrap();
3100
3101 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
3103
3104 assert_eq!(tasks.len(), 4);
3105 assert_eq!(tasks[0].id, critical.id); assert_eq!(tasks[1].id, high.id); assert_eq!(tasks[2].id, medium.id); assert_eq!(tasks[3].id, low.id); }
3110
3111 #[tokio::test]
3112 async fn test_pick_next_prefers_doing_over_todo() {
3113 let ctx = TestContext::new().await;
3114 let task_mgr = TaskManager::new(ctx.pool());
3115 let workspace_mgr = WorkspaceManager::new(ctx.pool());
3116
3117 let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
3119 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
3120 workspace_mgr
3121 .set_current_task(parent_started.task.id, None)
3122 .await
3123 .unwrap();
3124
3125 let doing_subtask = task_mgr
3127 .add_task("Doing Subtask", None, Some(parent.id), None)
3128 .await
3129 .unwrap();
3130 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
3131 workspace_mgr
3133 .set_current_task(parent.id, None)
3134 .await
3135 .unwrap();
3136
3137 let _todo_subtask = task_mgr
3138 .add_task("Todo Subtask", None, Some(parent.id), None)
3139 .await
3140 .unwrap();
3141
3142 let result = task_mgr.pick_next().await.unwrap();
3144
3145 if let Some(task) = result.task {
3146 assert_eq!(
3147 task.id, doing_subtask.id,
3148 "Should recommend doing subtask over todo subtask"
3149 );
3150 assert_eq!(task.status, "doing");
3151 } else {
3152 panic!("Expected a task recommendation");
3153 }
3154 }
3155
3156 #[tokio::test]
3157 async fn test_multiple_doing_tasks_allowed() {
3158 let ctx = TestContext::new().await;
3159 let task_mgr = TaskManager::new(ctx.pool());
3160 let workspace_mgr = WorkspaceManager::new(ctx.pool());
3161
3162 let task_a = task_mgr.add_task("Task A", None, None, None).await.unwrap();
3164 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
3165 assert_eq!(task_a_started.task.status, "doing");
3166
3167 let current = workspace_mgr.get_current_task(None).await.unwrap();
3169 assert_eq!(current.current_task_id, Some(task_a.id));
3170
3171 let task_b = task_mgr.add_task("Task B", None, None, None).await.unwrap();
3173 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
3174 assert_eq!(task_b_started.task.status, "doing");
3175
3176 let current = workspace_mgr.get_current_task(None).await.unwrap();
3178 assert_eq!(current.current_task_id, Some(task_b.id));
3179
3180 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
3182 assert_eq!(
3183 task_a_after.status, "doing",
3184 "Task A should remain doing even though it is not current"
3185 );
3186
3187 let doing_tasks: Vec<Task> = sqlx::query_as(
3189 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
3190 FROM tasks WHERE status = 'doing' ORDER BY id"#
3191 )
3192 .fetch_all(ctx.pool())
3193 .await
3194 .unwrap();
3195
3196 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
3197 assert_eq!(doing_tasks[0].id, task_a.id);
3198 assert_eq!(doing_tasks[1].id, task_b.id);
3199 }
3200 #[tokio::test]
3201 async fn test_find_tasks_pagination() {
3202 let ctx = TestContext::new().await;
3203 let task_mgr = TaskManager::new(ctx.pool());
3204
3205 for i in 0..15 {
3207 task_mgr
3208 .add_task(&format!("Task {}", i), None, None, None)
3209 .await
3210 .unwrap();
3211 }
3212
3213 let page1 = task_mgr
3215 .find_tasks(None, None, None, Some(10), Some(0))
3216 .await
3217 .unwrap();
3218 assert_eq!(page1.tasks.len(), 10);
3219 assert_eq!(page1.total_count, 15);
3220 assert!(page1.has_more);
3221 assert_eq!(page1.offset, 0);
3222
3223 let page2 = task_mgr
3225 .find_tasks(None, None, None, Some(10), Some(10))
3226 .await
3227 .unwrap();
3228 assert_eq!(page2.tasks.len(), 5);
3229 assert_eq!(page2.total_count, 15);
3230 assert!(!page2.has_more);
3231 assert_eq!(page2.offset, 10);
3232 }
3233}
3234
3235