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;
12pub struct TaskManager<'a> {
13 pool: &'a SqlitePool,
14 notifier: crate::notifications::NotificationSender,
15 cli_notifier: Option<crate::dashboard::cli_notifier::CliNotifier>,
16 project_path: Option<String>,
17}
18
19impl<'a> TaskManager<'a> {
20 pub fn new(pool: &'a SqlitePool) -> Self {
21 Self {
22 pool,
23 notifier: crate::notifications::NotificationSender::new(None),
24 cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
25 project_path: None,
26 }
27 }
28
29 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> 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: Some(project_path),
36 }
37 }
38
39 pub fn with_websocket(
41 pool: &'a SqlitePool,
42 ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
43 project_path: String,
44 ) -> Self {
45 Self {
46 pool,
47 notifier: crate::notifications::NotificationSender::new(Some(ws_state)),
48 cli_notifier: None, project_path: Some(project_path),
50 }
51 }
52
53 async fn notify_task_created(&self, task: &Task) {
55 use crate::dashboard::websocket::DatabaseOperationPayload;
56
57 if let Some(project_path) = &self.project_path {
59 let task_json = match serde_json::to_value(task) {
60 Ok(json) => json,
61 Err(e) => {
62 tracing::warn!(error = %e, "Failed to serialize task for notification");
63 return;
64 },
65 };
66
67 let payload =
68 DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
69 self.notifier.send(payload).await;
70 }
71
72 if let Some(cli_notifier) = &self.cli_notifier {
74 cli_notifier
75 .notify_task_changed(Some(task.id), "created", self.project_path.clone())
76 .await;
77 }
78 }
79
80 async fn notify_task_updated(&self, task: &Task) {
82 use crate::dashboard::websocket::DatabaseOperationPayload;
83
84 if let Some(project_path) = &self.project_path {
86 let task_json = match serde_json::to_value(task) {
87 Ok(json) => json,
88 Err(e) => {
89 tracing::warn!(error = %e, "Failed to serialize task for notification");
90 return;
91 },
92 };
93
94 let payload =
95 DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
96 self.notifier.send(payload).await;
97 }
98
99 if let Some(cli_notifier) = &self.cli_notifier {
101 cli_notifier
102 .notify_task_changed(Some(task.id), "updated", self.project_path.clone())
103 .await;
104 }
105 }
106
107 async fn notify_task_deleted(&self, task_id: i64) {
109 use crate::dashboard::websocket::DatabaseOperationPayload;
110
111 if let Some(project_path) = &self.project_path {
113 let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
114 self.notifier.send(payload).await;
115 }
116
117 if let Some(cli_notifier) = &self.cli_notifier {
119 cli_notifier
120 .notify_task_changed(Some(task_id), "deleted", self.project_path.clone())
121 .await;
122 }
123 }
124
125 #[tracing::instrument(skip(self), fields(task_name = %name))]
128 pub async fn add_task(
129 &self,
130 name: &str,
131 spec: Option<&str>,
132 parent_id: Option<i64>,
133 owner: Option<&str>,
134 ) -> Result<Task> {
135 if let Some(pid) = parent_id {
137 self.check_task_exists(pid).await?;
138 }
139
140 let now = Utc::now();
141 let owner = owner.unwrap_or("human");
142
143 let result = sqlx::query(
144 r#"
145 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner)
146 VALUES (?, ?, ?, 'todo', ?, ?)
147 "#,
148 )
149 .bind(name)
150 .bind(spec)
151 .bind(parent_id)
152 .bind(now)
153 .bind(owner)
154 .execute(self.pool)
155 .await?;
156
157 let id = result.last_insert_rowid();
158 let task = self.get_task(id).await?;
159
160 self.notify_task_created(&task).await;
162
163 Ok(task)
164 }
165
166 #[allow(clippy::too_many_arguments)]
189 pub async fn create_task_in_tx(
190 &self,
191 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
192 name: &str,
193 spec: Option<&str>,
194 priority: Option<i32>,
195 status: Option<&str>,
196 active_form: Option<&str>,
197 owner: &str,
198 ) -> Result<i64> {
199 let now = Utc::now();
200 let status = status.unwrap_or("todo");
201 let priority = priority.unwrap_or(3); let result = sqlx::query(
204 r#"
205 INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at, owner)
206 VALUES (?, ?, ?, ?, ?, ?, ?)
207 "#,
208 )
209 .bind(name)
210 .bind(spec)
211 .bind(priority)
212 .bind(status)
213 .bind(active_form)
214 .bind(now)
215 .bind(owner)
216 .execute(&mut **tx)
217 .await?;
218
219 Ok(result.last_insert_rowid())
220 }
221
222 pub async fn update_task_in_tx(
235 &self,
236 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
237 task_id: i64,
238 spec: Option<&str>,
239 priority: Option<i32>,
240 status: Option<&str>,
241 active_form: Option<&str>,
242 ) -> Result<()> {
243 if let Some(spec) = spec {
245 sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
246 .bind(spec)
247 .bind(task_id)
248 .execute(&mut **tx)
249 .await?;
250 }
251
252 if let Some(priority) = priority {
254 sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
255 .bind(priority)
256 .bind(task_id)
257 .execute(&mut **tx)
258 .await?;
259 }
260
261 if let Some(status) = status {
263 sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
264 .bind(status)
265 .bind(task_id)
266 .execute(&mut **tx)
267 .await?;
268 }
269
270 if let Some(active_form) = active_form {
272 sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
273 .bind(active_form)
274 .bind(task_id)
275 .execute(&mut **tx)
276 .await?;
277 }
278
279 Ok(())
280 }
281
282 pub async fn set_parent_in_tx(
286 &self,
287 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
288 task_id: i64,
289 parent_id: i64,
290 ) -> Result<()> {
291 sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
292 .bind(parent_id)
293 .bind(task_id)
294 .execute(&mut **tx)
295 .await?;
296
297 Ok(())
298 }
299
300 pub async fn clear_parent_in_tx(
304 &self,
305 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
306 task_id: i64,
307 ) -> Result<()> {
308 sqlx::query("UPDATE tasks SET parent_id = NULL WHERE id = ?")
309 .bind(task_id)
310 .execute(&mut **tx)
311 .await?;
312
313 Ok(())
314 }
315
316 pub async fn count_incomplete_children_in_tx(
321 &self,
322 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
323 task_id: i64,
324 ) -> Result<i64> {
325 let count: (i64,) = sqlx::query_as(crate::sql_constants::COUNT_INCOMPLETE_CHILDREN)
326 .bind(task_id)
327 .fetch_one(&mut **tx)
328 .await?;
329
330 Ok(count.0)
331 }
332
333 pub async fn complete_task_in_tx(
342 &self,
343 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
344 task_id: i64,
345 ) -> Result<()> {
346 let incomplete_count = self.count_incomplete_children_in_tx(tx, task_id).await?;
348 if incomplete_count > 0 {
349 return Err(IntentError::UncompletedChildren);
350 }
351
352 let now = chrono::Utc::now();
354 sqlx::query(
355 r#"
356 UPDATE tasks
357 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
358 WHERE id = ?
359 "#,
360 )
361 .bind(now)
362 .bind(task_id)
363 .execute(&mut **tx)
364 .await?;
365
366 Ok(())
367 }
368
369 pub async fn notify_batch_changed(&self) {
374 if let Some(cli_notifier) = &self.cli_notifier {
375 cli_notifier
376 .notify_task_changed(None, "batch_update", self.project_path.clone())
377 .await;
378 }
379 }
380
381 #[tracing::instrument(skip(self))]
387 pub async fn get_task(&self, id: i64) -> Result<Task> {
388 let task = sqlx::query_as::<_, Task>(
389 r#"
390 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
391 FROM tasks
392 WHERE id = ?
393 "#,
394 )
395 .bind(id)
396 .fetch_optional(self.pool)
397 .await?
398 .ok_or(IntentError::TaskNotFound(id))?;
399
400 Ok(task)
401 }
402
403 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
405 let task = self.get_task(id).await?;
406 let events_summary = self.get_events_summary(id).await?;
407
408 Ok(TaskWithEvents {
409 task,
410 events_summary: Some(events_summary),
411 })
412 }
413
414 pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
423 let mut chain = Vec::new();
424 let mut current_id = Some(task_id);
425
426 while let Some(id) = current_id {
427 let task = self.get_task(id).await?;
428 current_id = task.parent_id;
429 chain.push(task);
430 }
431
432 Ok(chain)
433 }
434
435 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
443 let task = self.get_task(id).await?;
445
446 let mut ancestors = Vec::new();
448 let mut current_parent_id = task.parent_id;
449
450 while let Some(parent_id) = current_parent_id {
451 let parent = self.get_task(parent_id).await?;
452 current_parent_id = parent.parent_id;
453 ancestors.push(parent);
454 }
455
456 let siblings = if let Some(parent_id) = task.parent_id {
458 sqlx::query_as::<_, Task>(
459 r#"
460 SELECT id, parent_id, name, spec, status, complexity, priority,
461 first_todo_at, first_doing_at, first_done_at, active_form, owner
462 FROM tasks
463 WHERE parent_id = ? AND id != ?
464 ORDER BY priority ASC NULLS LAST, id ASC
465 "#,
466 )
467 .bind(parent_id)
468 .bind(id)
469 .fetch_all(self.pool)
470 .await?
471 } else {
472 sqlx::query_as::<_, Task>(
474 r#"
475 SELECT id, parent_id, name, spec, status, complexity, priority,
476 first_todo_at, first_doing_at, first_done_at, active_form, owner
477 FROM tasks
478 WHERE parent_id IS NULL AND id != ?
479 ORDER BY priority ASC NULLS LAST, id ASC
480 "#,
481 )
482 .bind(id)
483 .fetch_all(self.pool)
484 .await?
485 };
486
487 let children = sqlx::query_as::<_, Task>(
489 r#"
490 SELECT id, parent_id, name, spec, status, complexity, priority,
491 first_todo_at, first_doing_at, first_done_at, active_form, owner
492 FROM tasks
493 WHERE parent_id = ?
494 ORDER BY priority ASC NULLS LAST, id ASC
495 "#,
496 )
497 .bind(id)
498 .fetch_all(self.pool)
499 .await?;
500
501 let blocking_tasks = sqlx::query_as::<_, Task>(
503 r#"
504 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
505 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
506 FROM tasks t
507 JOIN dependencies d ON t.id = d.blocking_task_id
508 WHERE d.blocked_task_id = ?
509 ORDER BY t.priority ASC NULLS LAST, t.id ASC
510 "#,
511 )
512 .bind(id)
513 .fetch_all(self.pool)
514 .await?;
515
516 let blocked_by_tasks = sqlx::query_as::<_, Task>(
518 r#"
519 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
520 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
521 FROM tasks t
522 JOIN dependencies d ON t.id = d.blocked_task_id
523 WHERE d.blocking_task_id = ?
524 ORDER BY t.priority ASC NULLS LAST, t.id ASC
525 "#,
526 )
527 .bind(id)
528 .fetch_all(self.pool)
529 .await?;
530
531 Ok(TaskContext {
532 task,
533 ancestors,
534 siblings,
535 children,
536 dependencies: crate::db::models::TaskDependencies {
537 blocking_tasks,
538 blocked_by_tasks,
539 },
540 })
541 }
542
543 pub async fn get_descendants(&self, task_id: i64) -> Result<Vec<Task>> {
546 let descendants = sqlx::query_as::<_, Task>(
547 r#"
548 WITH RECURSIVE descendants AS (
549 SELECT id, parent_id, name, spec, status, complexity, priority,
550 first_todo_at, first_doing_at, first_done_at, active_form, owner
551 FROM tasks
552 WHERE parent_id = ?
553
554 UNION ALL
555
556 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
557 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
558 FROM tasks t
559 INNER JOIN descendants d ON t.parent_id = d.id
560 )
561 SELECT * FROM descendants
562 ORDER BY parent_id NULLS FIRST, priority ASC NULLS LAST, id ASC
563 "#,
564 )
565 .bind(task_id)
566 .fetch_all(self.pool)
567 .await?;
568
569 Ok(descendants)
570 }
571
572 pub async fn get_status(
575 &self,
576 task_id: i64,
577 with_events: bool,
578 ) -> Result<crate::db::models::StatusResponse> {
579 use crate::db::models::{StatusResponse, TaskBrief};
580
581 let context = self.get_task_context(task_id).await?;
583
584 let descendants_full = self.get_descendants(task_id).await?;
586
587 let siblings: Vec<TaskBrief> = context.siblings.iter().map(TaskBrief::from).collect();
589 let descendants: Vec<TaskBrief> = descendants_full.iter().map(TaskBrief::from).collect();
590
591 let events = if with_events {
593 let event_mgr = crate::events::EventManager::new(self.pool);
594 Some(
595 event_mgr
596 .list_events(Some(task_id), Some(50), None, None)
597 .await?,
598 )
599 } else {
600 None
601 };
602
603 Ok(StatusResponse {
604 focused_task: context.task,
605 ancestors: context.ancestors,
606 siblings,
607 descendants,
608 events,
609 })
610 }
611
612 pub async fn get_root_tasks(&self) -> Result<Vec<Task>> {
614 let tasks = sqlx::query_as::<_, Task>(
615 r#"
616 SELECT id, parent_id, name, spec, status, complexity, priority,
617 first_todo_at, first_doing_at, first_done_at, active_form, owner
618 FROM tasks
619 WHERE parent_id IS NULL
620 ORDER BY
621 CASE status
622 WHEN 'doing' THEN 0
623 WHEN 'todo' THEN 1
624 WHEN 'done' THEN 2
625 END,
626 priority ASC NULLS LAST,
627 id ASC
628 "#,
629 )
630 .fetch_all(self.pool)
631 .await?;
632
633 Ok(tasks)
634 }
635
636 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
638 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
639 .bind(task_id)
640 .fetch_one(self.pool)
641 .await?;
642
643 let recent_events = sqlx::query_as::<_, Event>(
644 r#"
645 SELECT id, task_id, timestamp, log_type, discussion_data
646 FROM events
647 WHERE task_id = ?
648 ORDER BY timestamp DESC
649 LIMIT 10
650 "#,
651 )
652 .bind(task_id)
653 .fetch_all(self.pool)
654 .await?;
655
656 Ok(EventsSummary {
657 total_count,
658 recent_events,
659 })
660 }
661
662 #[allow(clippy::too_many_arguments)]
664 pub async fn update_task(
665 &self,
666 id: i64,
667 name: Option<&str>,
668 spec: Option<&str>,
669 parent_id: Option<Option<i64>>,
670 status: Option<&str>,
671 complexity: Option<i32>,
672 priority: Option<i32>,
673 ) -> Result<Task> {
674 let task = self.get_task(id).await?;
676
677 if let Some(s) = status {
679 if !["todo", "doing", "done"].contains(&s) {
680 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
681 }
682 }
683
684 if let Some(Some(pid)) = parent_id {
686 if pid == id {
687 return Err(IntentError::CircularDependency {
688 blocking_task_id: pid,
689 blocked_task_id: id,
690 });
691 }
692 self.check_task_exists(pid).await?;
693 self.check_circular_dependency(id, pid).await?;
694 }
695
696 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
698 sqlx::QueryBuilder::new("UPDATE tasks SET ");
699 let mut has_updates = false;
700
701 if let Some(n) = name {
702 if has_updates {
703 builder.push(", ");
704 }
705 builder.push("name = ").push_bind(n);
706 has_updates = true;
707 }
708
709 if let Some(s) = spec {
710 if has_updates {
711 builder.push(", ");
712 }
713 builder.push("spec = ").push_bind(s);
714 has_updates = true;
715 }
716
717 if let Some(pid) = parent_id {
718 if has_updates {
719 builder.push(", ");
720 }
721 match pid {
722 Some(p) => {
723 builder.push("parent_id = ").push_bind(p);
724 },
725 None => {
726 builder.push("parent_id = NULL");
727 },
728 }
729 has_updates = true;
730 }
731
732 if let Some(c) = complexity {
733 if has_updates {
734 builder.push(", ");
735 }
736 builder.push("complexity = ").push_bind(c);
737 has_updates = true;
738 }
739
740 if let Some(p) = priority {
741 if has_updates {
742 builder.push(", ");
743 }
744 builder.push("priority = ").push_bind(p);
745 has_updates = true;
746 }
747
748 if let Some(s) = status {
749 if has_updates {
750 builder.push(", ");
751 }
752 builder.push("status = ").push_bind(s);
753 has_updates = true;
754
755 let now = Utc::now();
757 let timestamp = now.to_rfc3339();
758 match s {
759 "todo" if task.first_todo_at.is_none() => {
760 builder.push(", first_todo_at = ").push_bind(timestamp);
761 },
762 "doing" if task.first_doing_at.is_none() => {
763 builder.push(", first_doing_at = ").push_bind(timestamp);
764 },
765 "done" if task.first_done_at.is_none() => {
766 builder.push(", first_done_at = ").push_bind(timestamp);
767 },
768 _ => {},
769 }
770 }
771
772 if !has_updates {
773 return Ok(task);
774 }
775
776 builder.push(" WHERE id = ").push_bind(id);
777
778 builder.build().execute(self.pool).await?;
779
780 let task = self.get_task(id).await?;
781
782 self.notify_task_updated(&task).await;
784
785 Ok(task)
786 }
787
788 pub async fn delete_task(&self, id: i64) -> Result<()> {
790 self.check_task_exists(id).await?;
791
792 sqlx::query("DELETE FROM tasks WHERE id = ?")
793 .bind(id)
794 .execute(self.pool)
795 .await?;
796
797 self.notify_task_deleted(id).await;
799
800 Ok(())
801 }
802
803 pub async fn find_tasks(
805 &self,
806 status: Option<&str>,
807 parent_id: Option<Option<i64>>,
808 sort_by: Option<TaskSortBy>,
809 limit: Option<i64>,
810 offset: Option<i64>,
811 ) -> Result<PaginatedTasks> {
812 let sort_by = sort_by.unwrap_or_default(); let limit = limit.unwrap_or(100);
815 let offset = offset.unwrap_or(0);
816
817 let session_id = crate::workspace::resolve_session_id(None);
819
820 let mut where_clause = String::from("WHERE 1=1");
822 let mut conditions = Vec::new();
823
824 if let Some(s) = status {
825 where_clause.push_str(" AND status = ?");
826 conditions.push(s.to_string());
827 }
828
829 if let Some(pid) = parent_id {
830 if let Some(p) = pid {
831 where_clause.push_str(" AND parent_id = ?");
832 conditions.push(p.to_string());
833 } else {
834 where_clause.push_str(" AND parent_id IS NULL");
835 }
836 }
837
838 let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
840
841 let order_clause = match sort_by {
843 TaskSortBy::Id => {
844 "ORDER BY id ASC".to_string()
846 },
847 TaskSortBy::Priority => {
848 "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
850 .to_string()
851 },
852 TaskSortBy::Time => {
853 r#"ORDER BY
855 CASE status
856 WHEN 'doing' THEN first_doing_at
857 WHEN 'todo' THEN first_todo_at
858 WHEN 'done' THEN first_done_at
859 END ASC NULLS LAST,
860 id ASC"#
861 .to_string()
862 },
863 TaskSortBy::FocusAware => {
864 r#"ORDER BY
866 CASE
867 WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
868 WHEN t.status = 'doing' THEN 1
869 WHEN t.status = 'todo' THEN 2
870 ELSE 3
871 END ASC,
872 COALESCE(t.priority, 999) ASC,
873 t.id ASC"#
874 .to_string()
875 },
876 };
877
878 let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
880 let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
881 for cond in &conditions {
882 count_q = count_q.bind(cond);
883 }
884 let total_count = count_q.fetch_one(self.pool).await?;
885
886 let main_query = format!(
888 "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 ?",
889 where_clause, order_clause
890 );
891
892 let mut q = sqlx::query_as::<_, Task>(&main_query);
893 for cond in conditions {
894 q = q.bind(cond);
895 }
896 if uses_session_bind {
898 q = q.bind(&session_id);
899 }
900 q = q.bind(limit);
901 q = q.bind(offset);
902
903 let tasks = q.fetch_all(self.pool).await?;
904
905 let has_more = offset + (tasks.len() as i64) < total_count;
907
908 Ok(PaginatedTasks {
909 tasks,
910 total_count,
911 has_more,
912 limit,
913 offset,
914 })
915 }
916
917 pub async fn get_stats(&self) -> Result<WorkspaceStats> {
922 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
923 r#"SELECT
924 COUNT(*) as total,
925 COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
926 COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
927 COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
928 FROM tasks"#,
929 )
930 .fetch_one(self.pool)
931 .await?;
932
933 Ok(WorkspaceStats {
934 total_tasks: row.0,
935 todo: row.1,
936 doing: row.2,
937 done: row.3,
938 })
939 }
940
941 #[tracing::instrument(skip(self))]
943 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
944 let task_exists: bool =
946 sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
947 .bind(id)
948 .fetch_one(self.pool)
949 .await?;
950
951 if !task_exists {
952 return Err(IntentError::TaskNotFound(id));
953 }
954
955 use crate::dependencies::get_incomplete_blocking_tasks;
957 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
958 return Err(IntentError::TaskBlocked {
959 task_id: id,
960 blocking_task_ids: blocking_tasks,
961 });
962 }
963
964 let mut tx = self.pool.begin().await?;
965
966 let now = Utc::now();
967
968 sqlx::query(
970 r#"
971 UPDATE tasks
972 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
973 WHERE id = ?
974 "#,
975 )
976 .bind(now)
977 .bind(id)
978 .execute(&mut *tx)
979 .await?;
980
981 let session_id = crate::workspace::resolve_session_id(None);
984 sqlx::query(
985 r#"
986 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
987 VALUES (?, ?, datetime('now'), datetime('now'))
988 ON CONFLICT(session_id) DO UPDATE SET
989 current_task_id = excluded.current_task_id,
990 last_active_at = datetime('now')
991 "#,
992 )
993 .bind(&session_id)
994 .bind(id)
995 .execute(&mut *tx)
996 .await?;
997
998 tx.commit().await?;
999
1000 if with_events {
1001 let result = self.get_task_with_events(id).await?;
1002 self.notify_task_updated(&result.task).await;
1003 Ok(result)
1004 } else {
1005 let task = self.get_task(id).await?;
1006 self.notify_task_updated(&task).await;
1007 Ok(TaskWithEvents {
1008 task,
1009 events_summary: None,
1010 })
1011 }
1012 }
1013
1014 #[tracing::instrument(skip(self))]
1023 pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1024 let session_id = crate::workspace::resolve_session_id(None);
1025 let mut tx = self.pool.begin().await?;
1026
1027 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1029 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1030 )
1031 .bind(&session_id)
1032 .fetch_optional(&mut *tx)
1033 .await?
1034 .flatten();
1035
1036 let id = current_task_id.ok_or(IntentError::InvalidInput(
1037 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
1038 ))?;
1039
1040 let task_info: (String, Option<i64>, String) =
1042 sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
1043 .bind(id)
1044 .fetch_one(&mut *tx)
1045 .await?;
1046 let (task_name, parent_id, owner) = task_info;
1047
1048 if owner == "human" && is_ai_caller {
1051 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1052 task_id: id,
1053 task_name: task_name.clone(),
1054 });
1055 }
1056
1057 self.complete_task_in_tx(&mut tx, id).await?;
1059
1060 sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1062 .bind(&session_id)
1063 .execute(&mut *tx)
1064 .await?;
1065
1066 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
1068 let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1070 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
1071 )
1072 .bind(parent_task_id)
1073 .bind(id)
1074 .fetch_one(&mut *tx)
1075 .await?;
1076
1077 if remaining_siblings == 0 {
1078 let parent_name: String =
1080 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1081 .bind(parent_task_id)
1082 .fetch_one(&mut *tx)
1083 .await?;
1084
1085 NextStepSuggestion::ParentIsReady {
1086 message: format!(
1087 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1088 parent_task_id, parent_name
1089 ),
1090 parent_task_id,
1091 parent_task_name: parent_name,
1092 }
1093 } else {
1094 let parent_name: String =
1096 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1097 .bind(parent_task_id)
1098 .fetch_one(&mut *tx)
1099 .await?;
1100
1101 NextStepSuggestion::SiblingTasksRemain {
1102 message: format!(
1103 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1104 id, parent_task_id, parent_name
1105 ),
1106 parent_task_id,
1107 parent_task_name: parent_name,
1108 remaining_siblings_count: remaining_siblings,
1109 }
1110 }
1111 } else {
1112 let child_count: i64 =
1114 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1115 .bind(id)
1116 .fetch_one(&mut *tx)
1117 .await?;
1118
1119 if child_count > 0 {
1120 NextStepSuggestion::TopLevelTaskCompleted {
1122 message: format!(
1123 "Top-level task #{} '{}' has been completed. Well done!",
1124 id, task_name
1125 ),
1126 completed_task_id: id,
1127 completed_task_name: task_name.clone(),
1128 }
1129 } else {
1130 let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1132 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
1133 )
1134 .bind(id)
1135 .fetch_one(&mut *tx)
1136 .await?;
1137
1138 if remaining_tasks == 0 {
1139 NextStepSuggestion::WorkspaceIsClear {
1140 message: format!(
1141 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1142 id
1143 ),
1144 completed_task_id: id,
1145 }
1146 } else {
1147 NextStepSuggestion::NoParentContext {
1148 message: format!("Task #{} '{}' has been completed.", id, task_name),
1149 completed_task_id: id,
1150 completed_task_name: task_name.clone(),
1151 }
1152 }
1153 }
1154 };
1155
1156 tx.commit().await?;
1157
1158 let completed_task = self.get_task(id).await?;
1160 self.notify_task_updated(&completed_task).await;
1161
1162 Ok(DoneTaskResponse {
1163 completed_task,
1164 workspace_status: WorkspaceStatus {
1165 current_task_id: None,
1166 },
1167 next_step_suggestion,
1168 })
1169 }
1170
1171 async fn check_task_exists(&self, id: i64) -> Result<()> {
1173 let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1174 .bind(id)
1175 .fetch_one(self.pool)
1176 .await?;
1177
1178 if !exists {
1179 return Err(IntentError::TaskNotFound(id));
1180 }
1181
1182 Ok(())
1183 }
1184
1185 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1187 let mut current_id = new_parent_id;
1188
1189 loop {
1190 if current_id == task_id {
1191 return Err(IntentError::CircularDependency {
1192 blocking_task_id: new_parent_id,
1193 blocked_task_id: task_id,
1194 });
1195 }
1196
1197 let parent: Option<i64> =
1198 sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1199 .bind(current_id)
1200 .fetch_optional(self.pool)
1201 .await?
1202 .flatten();
1203
1204 match parent {
1205 Some(pid) => current_id = pid,
1206 None => break,
1207 }
1208 }
1209
1210 Ok(())
1211 }
1212 pub async fn spawn_subtask(
1216 &self,
1217 name: &str,
1218 spec: Option<&str>,
1219 ) -> Result<SpawnSubtaskResponse> {
1220 let session_id = crate::workspace::resolve_session_id(None);
1222 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1223 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1224 )
1225 .bind(&session_id)
1226 .fetch_optional(self.pool)
1227 .await?
1228 .flatten();
1229
1230 let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1231 "No current task to create subtask under".to_string(),
1232 ))?;
1233
1234 let parent_name: String =
1236 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1237 .bind(parent_id)
1238 .fetch_one(self.pool)
1239 .await?;
1240
1241 let subtask = self
1243 .add_task(name, spec, Some(parent_id), Some("ai"))
1244 .await?;
1245
1246 self.start_task(subtask.id, false).await?;
1249
1250 Ok(SpawnSubtaskResponse {
1251 subtask: SubtaskInfo {
1252 id: subtask.id,
1253 name: subtask.name,
1254 parent_id,
1255 status: "doing".to_string(),
1256 },
1257 parent_task: ParentTaskInfo {
1258 id: parent_id,
1259 name: parent_name,
1260 },
1261 })
1262 }
1263
1264 pub async fn pick_next_tasks(
1277 &self,
1278 max_count: usize,
1279 capacity_limit: usize,
1280 ) -> Result<Vec<Task>> {
1281 let mut tx = self.pool.begin().await?;
1282
1283 let doing_count: i64 =
1285 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1286 .fetch_one(&mut *tx)
1287 .await?;
1288
1289 let available = capacity_limit.saturating_sub(doing_count as usize);
1291 if available == 0 {
1292 return Ok(vec![]);
1293 }
1294
1295 let limit = std::cmp::min(max_count, available);
1296
1297 let todo_tasks = sqlx::query_as::<_, Task>(
1299 r#"
1300 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1301 FROM tasks
1302 WHERE status = 'todo'
1303 ORDER BY
1304 COALESCE(priority, 0) ASC,
1305 COALESCE(complexity, 5) ASC,
1306 id ASC
1307 LIMIT ?
1308 "#,
1309 )
1310 .bind(limit as i64)
1311 .fetch_all(&mut *tx)
1312 .await?;
1313
1314 if todo_tasks.is_empty() {
1315 return Ok(vec![]);
1316 }
1317
1318 let now = Utc::now();
1319
1320 for task in &todo_tasks {
1322 sqlx::query(
1323 r#"
1324 UPDATE tasks
1325 SET status = 'doing',
1326 first_doing_at = COALESCE(first_doing_at, ?)
1327 WHERE id = ?
1328 "#,
1329 )
1330 .bind(now)
1331 .bind(task.id)
1332 .execute(&mut *tx)
1333 .await?;
1334 }
1335
1336 tx.commit().await?;
1337
1338 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1340 let placeholders = vec!["?"; task_ids.len()].join(",");
1341 let query = format!(
1342 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1343 FROM tasks WHERE id IN ({})
1344 ORDER BY
1345 COALESCE(priority, 0) ASC,
1346 COALESCE(complexity, 5) ASC,
1347 id ASC",
1348 placeholders
1349 );
1350
1351 let mut q = sqlx::query_as::<_, Task>(&query);
1352 for id in task_ids {
1353 q = q.bind(id);
1354 }
1355
1356 let updated_tasks = q.fetch_all(self.pool).await?;
1357 Ok(updated_tasks)
1358 }
1359
1360 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1369 let session_id = crate::workspace::resolve_session_id(None);
1371 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1372 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1373 )
1374 .bind(&session_id)
1375 .fetch_optional(self.pool)
1376 .await?
1377 .flatten();
1378
1379 if let Some(current_id) = current_task_id {
1380 let doing_subtasks = sqlx::query_as::<_, Task>(
1383 r#"
1384 SELECT id, parent_id, name, spec, status, complexity, priority,
1385 first_todo_at, first_doing_at, first_done_at, active_form, owner
1386 FROM tasks
1387 WHERE parent_id = ? AND status = 'doing'
1388 AND NOT EXISTS (
1389 SELECT 1 FROM dependencies d
1390 JOIN tasks bt ON d.blocking_task_id = bt.id
1391 WHERE d.blocked_task_id = tasks.id
1392 AND bt.status != 'done'
1393 )
1394 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1395 LIMIT 1
1396 "#,
1397 )
1398 .bind(current_id)
1399 .fetch_optional(self.pool)
1400 .await?;
1401
1402 if let Some(task) = doing_subtasks {
1403 return Ok(PickNextResponse::focused_subtask(task));
1404 }
1405
1406 let todo_subtasks = sqlx::query_as::<_, Task>(
1408 r#"
1409 SELECT id, parent_id, name, spec, status, complexity, priority,
1410 first_todo_at, first_doing_at, first_done_at, active_form, owner
1411 FROM tasks
1412 WHERE parent_id = ? AND status = 'todo'
1413 AND NOT EXISTS (
1414 SELECT 1 FROM dependencies d
1415 JOIN tasks bt ON d.blocking_task_id = bt.id
1416 WHERE d.blocked_task_id = tasks.id
1417 AND bt.status != 'done'
1418 )
1419 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1420 LIMIT 1
1421 "#,
1422 )
1423 .bind(current_id)
1424 .fetch_optional(self.pool)
1425 .await?;
1426
1427 if let Some(task) = todo_subtasks {
1428 return Ok(PickNextResponse::focused_subtask(task));
1429 }
1430 }
1431
1432 let doing_top_level = if let Some(current_id) = current_task_id {
1435 sqlx::query_as::<_, Task>(
1436 r#"
1437 SELECT id, parent_id, name, spec, status, complexity, priority,
1438 first_todo_at, first_doing_at, first_done_at, active_form, owner
1439 FROM tasks
1440 WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1441 AND NOT EXISTS (
1442 SELECT 1 FROM dependencies d
1443 JOIN tasks bt ON d.blocking_task_id = bt.id
1444 WHERE d.blocked_task_id = tasks.id
1445 AND bt.status != 'done'
1446 )
1447 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1448 LIMIT 1
1449 "#,
1450 )
1451 .bind(current_id)
1452 .fetch_optional(self.pool)
1453 .await?
1454 } else {
1455 sqlx::query_as::<_, Task>(
1456 r#"
1457 SELECT id, parent_id, name, spec, status, complexity, priority,
1458 first_todo_at, first_doing_at, first_done_at, active_form, owner
1459 FROM tasks
1460 WHERE parent_id IS NULL AND status = 'doing'
1461 AND NOT EXISTS (
1462 SELECT 1 FROM dependencies d
1463 JOIN tasks bt ON d.blocking_task_id = bt.id
1464 WHERE d.blocked_task_id = tasks.id
1465 AND bt.status != 'done'
1466 )
1467 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1468 LIMIT 1
1469 "#,
1470 )
1471 .fetch_optional(self.pool)
1472 .await?
1473 };
1474
1475 if let Some(task) = doing_top_level {
1476 return Ok(PickNextResponse::top_level_task(task));
1477 }
1478
1479 let todo_top_level = sqlx::query_as::<_, Task>(
1482 r#"
1483 SELECT id, parent_id, name, spec, status, complexity, priority,
1484 first_todo_at, first_doing_at, first_done_at, active_form, owner
1485 FROM tasks
1486 WHERE parent_id IS NULL AND status = 'todo'
1487 AND NOT EXISTS (
1488 SELECT 1 FROM dependencies d
1489 JOIN tasks bt ON d.blocking_task_id = bt.id
1490 WHERE d.blocked_task_id = tasks.id
1491 AND bt.status != 'done'
1492 )
1493 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1494 LIMIT 1
1495 "#,
1496 )
1497 .fetch_optional(self.pool)
1498 .await?;
1499
1500 if let Some(task) = todo_top_level {
1501 return Ok(PickNextResponse::top_level_task(task));
1502 }
1503
1504 let total_tasks: i64 =
1507 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1508 .fetch_one(self.pool)
1509 .await?;
1510
1511 if total_tasks == 0 {
1512 return Ok(PickNextResponse::no_tasks_in_project());
1513 }
1514
1515 let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1517 "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')",
1518 )
1519 .fetch_one(self.pool)
1520 .await?;
1521
1522 if todo_or_doing_count == 0 {
1523 return Ok(PickNextResponse::all_tasks_completed());
1524 }
1525
1526 Ok(PickNextResponse::no_available_todos())
1528 }
1529}
1530
1531#[cfg(test)]
1532mod tests {
1533 use super::*;
1534 use crate::events::EventManager;
1535 use crate::test_utils::test_helpers::TestContext;
1536 use crate::workspace::WorkspaceManager;
1537
1538 #[tokio::test]
1539 async fn test_get_stats_empty() {
1540 let ctx = TestContext::new().await;
1541 let manager = TaskManager::new(ctx.pool());
1542
1543 let stats = manager.get_stats().await.unwrap();
1544
1545 assert_eq!(stats.total_tasks, 0);
1546 assert_eq!(stats.todo, 0);
1547 assert_eq!(stats.doing, 0);
1548 assert_eq!(stats.done, 0);
1549 }
1550
1551 #[tokio::test]
1552 async fn test_get_stats_with_tasks() {
1553 let ctx = TestContext::new().await;
1554 let manager = TaskManager::new(ctx.pool());
1555
1556 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1558 let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
1559 let _task3 = manager.add_task("Task 3", None, None, None).await.unwrap();
1560
1561 manager
1563 .update_task(task1.id, None, None, None, Some("doing"), None, None)
1564 .await
1565 .unwrap();
1566 manager
1567 .update_task(task2.id, None, None, None, Some("done"), None, None)
1568 .await
1569 .unwrap();
1570 let stats = manager.get_stats().await.unwrap();
1573
1574 assert_eq!(stats.total_tasks, 3);
1575 assert_eq!(stats.todo, 1);
1576 assert_eq!(stats.doing, 1);
1577 assert_eq!(stats.done, 1);
1578 }
1579
1580 #[tokio::test]
1581 async fn test_add_task() {
1582 let ctx = TestContext::new().await;
1583 let manager = TaskManager::new(ctx.pool());
1584
1585 let task = manager
1586 .add_task("Test task", None, None, None)
1587 .await
1588 .unwrap();
1589
1590 assert_eq!(task.name, "Test task");
1591 assert_eq!(task.status, "todo");
1592 assert!(task.first_todo_at.is_some());
1593 assert!(task.first_doing_at.is_none());
1594 assert!(task.first_done_at.is_none());
1595 }
1596
1597 #[tokio::test]
1598 async fn test_add_task_with_spec() {
1599 let ctx = TestContext::new().await;
1600 let manager = TaskManager::new(ctx.pool());
1601
1602 let spec = "This is a task specification";
1603 let task = manager
1604 .add_task("Test task", Some(spec), None, None)
1605 .await
1606 .unwrap();
1607
1608 assert_eq!(task.name, "Test task");
1609 assert_eq!(task.spec.as_deref(), Some(spec));
1610 }
1611
1612 #[tokio::test]
1613 async fn test_add_task_with_parent() {
1614 let ctx = TestContext::new().await;
1615 let manager = TaskManager::new(ctx.pool());
1616
1617 let parent = manager
1618 .add_task("Parent task", None, None, None)
1619 .await
1620 .unwrap();
1621 let child = manager
1622 .add_task("Child task", None, Some(parent.id), None)
1623 .await
1624 .unwrap();
1625
1626 assert_eq!(child.parent_id, Some(parent.id));
1627 }
1628
1629 #[tokio::test]
1630 async fn test_get_task() {
1631 let ctx = TestContext::new().await;
1632 let manager = TaskManager::new(ctx.pool());
1633
1634 let created = manager
1635 .add_task("Test task", None, None, None)
1636 .await
1637 .unwrap();
1638 let retrieved = manager.get_task(created.id).await.unwrap();
1639
1640 assert_eq!(created.id, retrieved.id);
1641 assert_eq!(created.name, retrieved.name);
1642 }
1643
1644 #[tokio::test]
1645 async fn test_get_task_not_found() {
1646 let ctx = TestContext::new().await;
1647 let manager = TaskManager::new(ctx.pool());
1648
1649 let result = manager.get_task(999).await;
1650 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1651 }
1652
1653 #[tokio::test]
1654 async fn test_update_task_name() {
1655 let ctx = TestContext::new().await;
1656 let manager = TaskManager::new(ctx.pool());
1657
1658 let task = manager
1659 .add_task("Original name", None, None, None)
1660 .await
1661 .unwrap();
1662 let updated = manager
1663 .update_task(task.id, Some("New name"), None, None, None, None, None)
1664 .await
1665 .unwrap();
1666
1667 assert_eq!(updated.name, "New name");
1668 }
1669
1670 #[tokio::test]
1671 async fn test_update_task_status() {
1672 let ctx = TestContext::new().await;
1673 let manager = TaskManager::new(ctx.pool());
1674
1675 let task = manager
1676 .add_task("Test task", None, None, None)
1677 .await
1678 .unwrap();
1679 let updated = manager
1680 .update_task(task.id, None, None, None, Some("doing"), None, None)
1681 .await
1682 .unwrap();
1683
1684 assert_eq!(updated.status, "doing");
1685 assert!(updated.first_doing_at.is_some());
1686 }
1687
1688 #[tokio::test]
1689 async fn test_delete_task() {
1690 let ctx = TestContext::new().await;
1691 let manager = TaskManager::new(ctx.pool());
1692
1693 let task = manager
1694 .add_task("Test task", None, None, None)
1695 .await
1696 .unwrap();
1697 manager.delete_task(task.id).await.unwrap();
1698
1699 let result = manager.get_task(task.id).await;
1700 assert!(result.is_err());
1701 }
1702
1703 #[tokio::test]
1704 async fn test_find_tasks_by_status() {
1705 let ctx = TestContext::new().await;
1706 let manager = TaskManager::new(ctx.pool());
1707
1708 manager
1709 .add_task("Todo task", None, None, None)
1710 .await
1711 .unwrap();
1712 let doing_task = manager
1713 .add_task("Doing task", None, None, None)
1714 .await
1715 .unwrap();
1716 manager
1717 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1718 .await
1719 .unwrap();
1720
1721 let todo_result = manager
1722 .find_tasks(Some("todo"), None, None, None, None)
1723 .await
1724 .unwrap();
1725 let doing_result = manager
1726 .find_tasks(Some("doing"), None, None, None, None)
1727 .await
1728 .unwrap();
1729
1730 assert_eq!(todo_result.tasks.len(), 1);
1731 assert_eq!(doing_result.tasks.len(), 1);
1732 assert_eq!(doing_result.tasks[0].status, "doing");
1733 }
1734
1735 #[tokio::test]
1736 async fn test_find_tasks_by_parent() {
1737 let ctx = TestContext::new().await;
1738 let manager = TaskManager::new(ctx.pool());
1739
1740 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1741 manager
1742 .add_task("Child 1", None, Some(parent.id), None)
1743 .await
1744 .unwrap();
1745 manager
1746 .add_task("Child 2", None, Some(parent.id), None)
1747 .await
1748 .unwrap();
1749
1750 let result = manager
1751 .find_tasks(None, Some(Some(parent.id)), None, None, None)
1752 .await
1753 .unwrap();
1754
1755 assert_eq!(result.tasks.len(), 2);
1756 }
1757
1758 #[tokio::test]
1759 async fn test_start_task() {
1760 let ctx = TestContext::new().await;
1761 let manager = TaskManager::new(ctx.pool());
1762
1763 let task = manager
1764 .add_task("Test task", None, None, None)
1765 .await
1766 .unwrap();
1767 let started = manager.start_task(task.id, false).await.unwrap();
1768
1769 assert_eq!(started.task.status, "doing");
1770 assert!(started.task.first_doing_at.is_some());
1771
1772 let session_id = crate::workspace::resolve_session_id(None);
1774 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1775 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1776 )
1777 .bind(&session_id)
1778 .fetch_optional(ctx.pool())
1779 .await
1780 .unwrap()
1781 .flatten();
1782
1783 assert_eq!(current, Some(task.id));
1784 }
1785
1786 #[tokio::test]
1787 async fn test_start_task_with_events() {
1788 let ctx = TestContext::new().await;
1789 let manager = TaskManager::new(ctx.pool());
1790
1791 let task = manager
1792 .add_task("Test task", None, None, None)
1793 .await
1794 .unwrap();
1795
1796 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1798 .bind(task.id)
1799 .bind("test")
1800 .bind("test event")
1801 .execute(ctx.pool())
1802 .await
1803 .unwrap();
1804
1805 let started = manager.start_task(task.id, true).await.unwrap();
1806
1807 assert!(started.events_summary.is_some());
1808 let summary = started.events_summary.unwrap();
1809 assert_eq!(summary.total_count, 1);
1810 }
1811
1812 #[tokio::test]
1813 async fn test_done_task() {
1814 let ctx = TestContext::new().await;
1815 let manager = TaskManager::new(ctx.pool());
1816
1817 let task = manager
1818 .add_task("Test task", None, None, None)
1819 .await
1820 .unwrap();
1821 manager.start_task(task.id, false).await.unwrap();
1822 let response = manager.done_task(false).await.unwrap();
1823
1824 assert_eq!(response.completed_task.status, "done");
1825 assert!(response.completed_task.first_done_at.is_some());
1826 assert_eq!(response.workspace_status.current_task_id, None);
1827
1828 match response.next_step_suggestion {
1830 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1831 _ => panic!("Expected WorkspaceIsClear suggestion"),
1832 }
1833
1834 let session_id = crate::workspace::resolve_session_id(None);
1836 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1837 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1838 )
1839 .bind(&session_id)
1840 .fetch_optional(ctx.pool())
1841 .await
1842 .unwrap()
1843 .flatten();
1844
1845 assert!(current.is_none());
1846 }
1847
1848 #[tokio::test]
1849 async fn test_done_task_with_uncompleted_children() {
1850 let ctx = TestContext::new().await;
1851 let manager = TaskManager::new(ctx.pool());
1852
1853 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1854 manager
1855 .add_task("Child", None, Some(parent.id), None)
1856 .await
1857 .unwrap();
1858
1859 manager.start_task(parent.id, false).await.unwrap();
1861
1862 let result = manager.done_task(false).await;
1863 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1864 }
1865
1866 #[tokio::test]
1867 async fn test_done_task_with_completed_children() {
1868 let ctx = TestContext::new().await;
1869 let manager = TaskManager::new(ctx.pool());
1870
1871 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1872 let child = manager
1873 .add_task("Child", None, Some(parent.id), None)
1874 .await
1875 .unwrap();
1876
1877 manager.start_task(child.id, false).await.unwrap();
1879 let child_response = manager.done_task(false).await.unwrap();
1880
1881 match child_response.next_step_suggestion {
1883 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1884 assert_eq!(parent_task_id, parent.id);
1885 },
1886 _ => panic!("Expected ParentIsReady suggestion"),
1887 }
1888
1889 manager.start_task(parent.id, false).await.unwrap();
1891 let parent_response = manager.done_task(false).await.unwrap();
1892 assert_eq!(parent_response.completed_task.status, "done");
1893
1894 match parent_response.next_step_suggestion {
1896 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1897 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1898 }
1899 }
1900
1901 #[tokio::test]
1902 async fn test_circular_dependency() {
1903 let ctx = TestContext::new().await;
1904 let manager = TaskManager::new(ctx.pool());
1905
1906 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1907 let task2 = manager
1908 .add_task("Task 2", None, Some(task1.id), None)
1909 .await
1910 .unwrap();
1911
1912 let result = manager
1914 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1915 .await;
1916
1917 assert!(matches!(
1918 result,
1919 Err(IntentError::CircularDependency { .. })
1920 ));
1921 }
1922
1923 #[tokio::test]
1924 async fn test_invalid_parent_id() {
1925 let ctx = TestContext::new().await;
1926 let manager = TaskManager::new(ctx.pool());
1927
1928 let result = manager.add_task("Test", None, Some(999), None).await;
1929 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1930 }
1931
1932 #[tokio::test]
1933 async fn test_update_task_complexity_and_priority() {
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 let updated = manager
1942 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1943 .await
1944 .unwrap();
1945
1946 assert_eq!(updated.complexity, Some(8));
1947 assert_eq!(updated.priority, Some(10));
1948 }
1949
1950 #[tokio::test]
1951 async fn test_spawn_subtask() {
1952 let ctx = TestContext::new().await;
1953 let manager = TaskManager::new(ctx.pool());
1954
1955 let parent = manager
1957 .add_task("Parent task", None, None, None)
1958 .await
1959 .unwrap();
1960 manager.start_task(parent.id, false).await.unwrap();
1961
1962 let response = manager
1964 .spawn_subtask("Child task", Some("Details"))
1965 .await
1966 .unwrap();
1967
1968 assert_eq!(response.subtask.parent_id, parent.id);
1969 assert_eq!(response.subtask.name, "Child task");
1970 assert_eq!(response.subtask.status, "doing");
1971 assert_eq!(response.parent_task.id, parent.id);
1972 assert_eq!(response.parent_task.name, "Parent task");
1973
1974 let session_id = crate::workspace::resolve_session_id(None);
1976 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1977 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1978 )
1979 .bind(&session_id)
1980 .fetch_optional(ctx.pool())
1981 .await
1982 .unwrap()
1983 .flatten();
1984
1985 assert_eq!(current, Some(response.subtask.id));
1986
1987 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1989 assert_eq!(retrieved.status, "doing");
1990 }
1991
1992 #[tokio::test]
1993 async fn test_spawn_subtask_no_current_task() {
1994 let ctx = TestContext::new().await;
1995 let manager = TaskManager::new(ctx.pool());
1996
1997 let result = manager.spawn_subtask("Child", None).await;
1999 assert!(result.is_err());
2000 }
2001
2002 #[tokio::test]
2003 async fn test_pick_next_tasks_basic() {
2004 let ctx = TestContext::new().await;
2005 let manager = TaskManager::new(ctx.pool());
2006
2007 for i in 1..=10 {
2009 manager
2010 .add_task(&format!("Task {}", i), None, None, None)
2011 .await
2012 .unwrap();
2013 }
2014
2015 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
2017
2018 assert_eq!(picked.len(), 5);
2019 for task in &picked {
2020 assert_eq!(task.status, "doing");
2021 assert!(task.first_doing_at.is_some());
2022 }
2023
2024 let doing_count: i64 =
2026 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2027 .fetch_one(ctx.pool())
2028 .await
2029 .unwrap();
2030
2031 assert_eq!(doing_count, 5);
2032 }
2033
2034 #[tokio::test]
2035 async fn test_pick_next_tasks_with_existing_doing() {
2036 let ctx = TestContext::new().await;
2037 let manager = TaskManager::new(ctx.pool());
2038
2039 for i in 1..=10 {
2041 manager
2042 .add_task(&format!("Task {}", i), None, None, None)
2043 .await
2044 .unwrap();
2045 }
2046
2047 let result = manager
2049 .find_tasks(Some("todo"), None, None, None, None)
2050 .await
2051 .unwrap();
2052 manager.start_task(result.tasks[0].id, false).await.unwrap();
2053 manager.start_task(result.tasks[1].id, false).await.unwrap();
2054
2055 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2057
2058 assert_eq!(picked.len(), 3);
2060
2061 let doing_count: i64 =
2063 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2064 .fetch_one(ctx.pool())
2065 .await
2066 .unwrap();
2067
2068 assert_eq!(doing_count, 5);
2069 }
2070
2071 #[tokio::test]
2072 async fn test_pick_next_tasks_at_capacity() {
2073 let ctx = TestContext::new().await;
2074 let manager = TaskManager::new(ctx.pool());
2075
2076 for i in 1..=10 {
2078 manager
2079 .add_task(&format!("Task {}", i), None, None, None)
2080 .await
2081 .unwrap();
2082 }
2083
2084 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2086 assert_eq!(first_batch.len(), 5);
2087
2088 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2090 assert_eq!(second_batch.len(), 0);
2091 }
2092
2093 #[tokio::test]
2094 async fn test_pick_next_tasks_priority_ordering() {
2095 let ctx = TestContext::new().await;
2096 let manager = TaskManager::new(ctx.pool());
2097
2098 let low = manager
2100 .add_task("Low priority", None, None, None)
2101 .await
2102 .unwrap();
2103 manager
2104 .update_task(low.id, None, None, None, None, None, Some(1))
2105 .await
2106 .unwrap();
2107
2108 let high = manager
2109 .add_task("High priority", None, None, None)
2110 .await
2111 .unwrap();
2112 manager
2113 .update_task(high.id, None, None, None, None, None, Some(10))
2114 .await
2115 .unwrap();
2116
2117 let medium = manager
2118 .add_task("Medium priority", None, None, None)
2119 .await
2120 .unwrap();
2121 manager
2122 .update_task(medium.id, None, None, None, None, None, Some(5))
2123 .await
2124 .unwrap();
2125
2126 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2128
2129 assert_eq!(picked.len(), 3);
2131 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
2135
2136 #[tokio::test]
2137 async fn test_pick_next_tasks_complexity_ordering() {
2138 let ctx = TestContext::new().await;
2139 let manager = TaskManager::new(ctx.pool());
2140
2141 let complex = manager.add_task("Complex", None, None, None).await.unwrap();
2143 manager
2144 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
2145 .await
2146 .unwrap();
2147
2148 let simple = manager.add_task("Simple", None, None, None).await.unwrap();
2149 manager
2150 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
2151 .await
2152 .unwrap();
2153
2154 let medium = manager.add_task("Medium", None, None, None).await.unwrap();
2155 manager
2156 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
2157 .await
2158 .unwrap();
2159
2160 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2162
2163 assert_eq!(picked.len(), 3);
2165 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
2169
2170 #[tokio::test]
2171 async fn test_done_task_sibling_tasks_remain() {
2172 let ctx = TestContext::new().await;
2173 let manager = TaskManager::new(ctx.pool());
2174
2175 let parent = manager
2177 .add_task("Parent Task", None, None, None)
2178 .await
2179 .unwrap();
2180 let child1 = manager
2181 .add_task("Child 1", None, Some(parent.id), None)
2182 .await
2183 .unwrap();
2184 let child2 = manager
2185 .add_task("Child 2", None, Some(parent.id), None)
2186 .await
2187 .unwrap();
2188 let _child3 = manager
2189 .add_task("Child 3", None, Some(parent.id), None)
2190 .await
2191 .unwrap();
2192
2193 manager.start_task(child1.id, false).await.unwrap();
2195 let response = manager.done_task(false).await.unwrap();
2196
2197 match response.next_step_suggestion {
2199 NextStepSuggestion::SiblingTasksRemain {
2200 parent_task_id,
2201 remaining_siblings_count,
2202 ..
2203 } => {
2204 assert_eq!(parent_task_id, parent.id);
2205 assert_eq!(remaining_siblings_count, 2); },
2207 _ => panic!("Expected SiblingTasksRemain suggestion"),
2208 }
2209
2210 manager.start_task(child2.id, false).await.unwrap();
2212 let response2 = manager.done_task(false).await.unwrap();
2213
2214 match response2.next_step_suggestion {
2216 NextStepSuggestion::SiblingTasksRemain {
2217 remaining_siblings_count,
2218 ..
2219 } => {
2220 assert_eq!(remaining_siblings_count, 1); },
2222 _ => panic!("Expected SiblingTasksRemain suggestion"),
2223 }
2224 }
2225
2226 #[tokio::test]
2227 async fn test_done_task_top_level_with_children() {
2228 let ctx = TestContext::new().await;
2229 let manager = TaskManager::new(ctx.pool());
2230
2231 let parent = manager
2233 .add_task("Epic Task", None, None, None)
2234 .await
2235 .unwrap();
2236 let child = manager
2237 .add_task("Sub Task", None, Some(parent.id), None)
2238 .await
2239 .unwrap();
2240
2241 manager.start_task(child.id, false).await.unwrap();
2243 manager.done_task(false).await.unwrap();
2244
2245 manager.start_task(parent.id, false).await.unwrap();
2247 let response = manager.done_task(false).await.unwrap();
2248
2249 match response.next_step_suggestion {
2251 NextStepSuggestion::TopLevelTaskCompleted {
2252 completed_task_id,
2253 completed_task_name,
2254 ..
2255 } => {
2256 assert_eq!(completed_task_id, parent.id);
2257 assert_eq!(completed_task_name, "Epic Task");
2258 },
2259 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2260 }
2261 }
2262
2263 #[tokio::test]
2264 async fn test_done_task_no_parent_context() {
2265 let ctx = TestContext::new().await;
2266 let manager = TaskManager::new(ctx.pool());
2267
2268 let task1 = manager
2270 .add_task("Standalone Task 1", None, None, None)
2271 .await
2272 .unwrap();
2273 let _task2 = manager
2274 .add_task("Standalone Task 2", None, None, None)
2275 .await
2276 .unwrap();
2277
2278 manager.start_task(task1.id, false).await.unwrap();
2280 let response = manager.done_task(false).await.unwrap();
2281
2282 match response.next_step_suggestion {
2284 NextStepSuggestion::NoParentContext {
2285 completed_task_id,
2286 completed_task_name,
2287 ..
2288 } => {
2289 assert_eq!(completed_task_id, task1.id);
2290 assert_eq!(completed_task_name, "Standalone Task 1");
2291 },
2292 _ => panic!("Expected NoParentContext suggestion"),
2293 }
2294 }
2295
2296 #[tokio::test]
2297 async fn test_pick_next_focused_subtask() {
2298 let ctx = TestContext::new().await;
2299 let manager = TaskManager::new(ctx.pool());
2300
2301 let parent = manager
2303 .add_task("Parent task", None, None, None)
2304 .await
2305 .unwrap();
2306 manager.start_task(parent.id, false).await.unwrap();
2307
2308 let subtask1 = manager
2310 .add_task("Subtask 1", None, Some(parent.id), None)
2311 .await
2312 .unwrap();
2313 let subtask2 = manager
2314 .add_task("Subtask 2", None, Some(parent.id), None)
2315 .await
2316 .unwrap();
2317
2318 manager
2320 .update_task(subtask1.id, None, None, None, None, None, Some(2))
2321 .await
2322 .unwrap();
2323 manager
2324 .update_task(subtask2.id, None, None, None, None, None, Some(1))
2325 .await
2326 .unwrap();
2327
2328 let response = manager.pick_next().await.unwrap();
2330
2331 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2332 assert!(response.task.is_some());
2333 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2334 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2335 }
2336
2337 #[tokio::test]
2338 async fn test_pick_next_top_level_task() {
2339 let ctx = TestContext::new().await;
2340 let manager = TaskManager::new(ctx.pool());
2341
2342 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
2344 let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
2345
2346 manager
2348 .update_task(task1.id, None, None, None, None, None, Some(5))
2349 .await
2350 .unwrap();
2351 manager
2352 .update_task(task2.id, None, None, None, None, None, Some(3))
2353 .await
2354 .unwrap();
2355
2356 let response = manager.pick_next().await.unwrap();
2358
2359 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2360 assert!(response.task.is_some());
2361 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2362 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2363 }
2364
2365 #[tokio::test]
2366 async fn test_pick_next_no_tasks() {
2367 let ctx = TestContext::new().await;
2368 let manager = TaskManager::new(ctx.pool());
2369
2370 let response = manager.pick_next().await.unwrap();
2372
2373 assert_eq!(response.suggestion_type, "NONE");
2374 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2375 assert!(response.message.is_some());
2376 }
2377
2378 #[tokio::test]
2379 async fn test_pick_next_all_completed() {
2380 let ctx = TestContext::new().await;
2381 let manager = TaskManager::new(ctx.pool());
2382
2383 let task = manager.add_task("Task 1", None, None, None).await.unwrap();
2385 manager.start_task(task.id, false).await.unwrap();
2386 manager.done_task(false).await.unwrap();
2387
2388 let response = manager.pick_next().await.unwrap();
2390
2391 assert_eq!(response.suggestion_type, "NONE");
2392 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2393 assert!(response.message.is_some());
2394 }
2395
2396 #[tokio::test]
2397 async fn test_pick_next_no_available_todos() {
2398 let ctx = TestContext::new().await;
2399 let manager = TaskManager::new(ctx.pool());
2400
2401 let parent = manager
2403 .add_task("Parent task", None, None, None)
2404 .await
2405 .unwrap();
2406 manager.start_task(parent.id, false).await.unwrap();
2407
2408 let subtask = manager
2410 .add_task("Subtask", None, Some(parent.id), None)
2411 .await
2412 .unwrap();
2413 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2415 .bind(subtask.id)
2416 .execute(ctx.pool())
2417 .await
2418 .unwrap();
2419
2420 let session_id = crate::workspace::resolve_session_id(None);
2422 sqlx::query(
2423 r#"
2424 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
2425 VALUES (?, ?, datetime('now'), datetime('now'))
2426 ON CONFLICT(session_id) DO UPDATE SET
2427 current_task_id = excluded.current_task_id,
2428 last_active_at = datetime('now')
2429 "#,
2430 )
2431 .bind(&session_id)
2432 .bind(subtask.id)
2433 .execute(ctx.pool())
2434 .await
2435 .unwrap();
2436
2437 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2439 .bind(parent.id)
2440 .execute(ctx.pool())
2441 .await
2442 .unwrap();
2443
2444 let response = manager.pick_next().await.unwrap();
2447
2448 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2449 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2450 assert_eq!(response.task.as_ref().unwrap().status, "doing");
2451 }
2452
2453 #[tokio::test]
2454 async fn test_pick_next_priority_ordering() {
2455 let ctx = TestContext::new().await;
2456 let manager = TaskManager::new(ctx.pool());
2457
2458 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2460 manager.start_task(parent.id, false).await.unwrap();
2461
2462 let sub1 = manager
2464 .add_task("Priority 10", None, Some(parent.id), None)
2465 .await
2466 .unwrap();
2467 manager
2468 .update_task(sub1.id, None, None, None, None, None, Some(10))
2469 .await
2470 .unwrap();
2471
2472 let sub2 = manager
2473 .add_task("Priority 1", None, Some(parent.id), None)
2474 .await
2475 .unwrap();
2476 manager
2477 .update_task(sub2.id, None, None, None, None, None, Some(1))
2478 .await
2479 .unwrap();
2480
2481 let sub3 = manager
2482 .add_task("Priority 5", None, Some(parent.id), None)
2483 .await
2484 .unwrap();
2485 manager
2486 .update_task(sub3.id, None, None, None, None, None, Some(5))
2487 .await
2488 .unwrap();
2489
2490 let response = manager.pick_next().await.unwrap();
2492
2493 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2494 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2495 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2496 }
2497
2498 #[tokio::test]
2499 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2500 let ctx = TestContext::new().await;
2501 let manager = TaskManager::new(ctx.pool());
2502
2503 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2505 manager.start_task(parent.id, false).await.unwrap();
2506
2507 let top_level = manager
2509 .add_task("Top level task", None, None, None)
2510 .await
2511 .unwrap();
2512
2513 let response = manager.pick_next().await.unwrap();
2515
2516 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2517 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2518 }
2519
2520 #[tokio::test]
2523 async fn test_get_task_with_events() {
2524 let ctx = TestContext::new().await;
2525 let task_mgr = TaskManager::new(ctx.pool());
2526 let event_mgr = EventManager::new(ctx.pool());
2527
2528 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2529
2530 event_mgr
2532 .add_event(task.id, "progress", "Event 1")
2533 .await
2534 .unwrap();
2535 event_mgr
2536 .add_event(task.id, "decision", "Event 2")
2537 .await
2538 .unwrap();
2539
2540 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2541
2542 assert_eq!(result.task.id, task.id);
2543 assert!(result.events_summary.is_some());
2544
2545 let summary = result.events_summary.unwrap();
2546 assert_eq!(summary.total_count, 2);
2547 assert_eq!(summary.recent_events.len(), 2);
2548 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2550 }
2551
2552 #[tokio::test]
2553 async fn test_get_task_with_events_nonexistent() {
2554 let ctx = TestContext::new().await;
2555 let task_mgr = TaskManager::new(ctx.pool());
2556
2557 let result = task_mgr.get_task_with_events(999).await;
2558 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2559 }
2560
2561 #[tokio::test]
2562 async fn test_get_task_with_many_events() {
2563 let ctx = TestContext::new().await;
2564 let task_mgr = TaskManager::new(ctx.pool());
2565 let event_mgr = EventManager::new(ctx.pool());
2566
2567 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2568
2569 for i in 0..20 {
2571 event_mgr
2572 .add_event(task.id, "test", &format!("Event {}", i))
2573 .await
2574 .unwrap();
2575 }
2576
2577 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2578 let summary = result.events_summary.unwrap();
2579
2580 assert_eq!(summary.total_count, 20);
2581 assert_eq!(summary.recent_events.len(), 10); }
2583
2584 #[tokio::test]
2585 async fn test_get_task_with_no_events() {
2586 let ctx = TestContext::new().await;
2587 let task_mgr = TaskManager::new(ctx.pool());
2588
2589 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2590
2591 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2592 let summary = result.events_summary.unwrap();
2593
2594 assert_eq!(summary.total_count, 0);
2595 assert_eq!(summary.recent_events.len(), 0);
2596 }
2597
2598 #[tokio::test]
2599 async fn test_pick_next_tasks_zero_capacity() {
2600 let ctx = TestContext::new().await;
2601 let task_mgr = TaskManager::new(ctx.pool());
2602
2603 task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2604
2605 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2607 assert_eq!(results.len(), 0);
2608 }
2609
2610 #[tokio::test]
2611 async fn test_pick_next_tasks_capacity_exceeds_available() {
2612 let ctx = TestContext::new().await;
2613 let task_mgr = TaskManager::new(ctx.pool());
2614
2615 task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2616 task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2617
2618 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2620 assert_eq!(results.len(), 2); }
2622
2623 #[tokio::test]
2626 async fn test_get_task_context_root_task_no_relations() {
2627 let ctx = TestContext::new().await;
2628 let task_mgr = TaskManager::new(ctx.pool());
2629
2630 let task = task_mgr
2632 .add_task("Root task", None, None, None)
2633 .await
2634 .unwrap();
2635
2636 let context = task_mgr.get_task_context(task.id).await.unwrap();
2637
2638 assert_eq!(context.task.id, task.id);
2640 assert_eq!(context.task.name, "Root task");
2641
2642 assert_eq!(context.ancestors.len(), 0);
2644
2645 assert_eq!(context.siblings.len(), 0);
2647
2648 assert_eq!(context.children.len(), 0);
2650 }
2651
2652 #[tokio::test]
2653 async fn test_get_task_context_with_siblings() {
2654 let ctx = TestContext::new().await;
2655 let task_mgr = TaskManager::new(ctx.pool());
2656
2657 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2659 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2660 let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2661
2662 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2663
2664 assert_eq!(context.task.id, task2.id);
2666
2667 assert_eq!(context.ancestors.len(), 0);
2669
2670 assert_eq!(context.siblings.len(), 2);
2672 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2673 assert!(sibling_ids.contains(&task1.id));
2674 assert!(sibling_ids.contains(&task3.id));
2675 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2679 }
2680
2681 #[tokio::test]
2682 async fn test_get_task_context_with_parent() {
2683 let ctx = TestContext::new().await;
2684 let task_mgr = TaskManager::new(ctx.pool());
2685
2686 let parent = task_mgr
2688 .add_task("Parent task", None, None, None)
2689 .await
2690 .unwrap();
2691 let child = task_mgr
2692 .add_task("Child task", None, Some(parent.id), None)
2693 .await
2694 .unwrap();
2695
2696 let context = task_mgr.get_task_context(child.id).await.unwrap();
2697
2698 assert_eq!(context.task.id, child.id);
2700 assert_eq!(context.task.parent_id, Some(parent.id));
2701
2702 assert_eq!(context.ancestors.len(), 1);
2704 assert_eq!(context.ancestors[0].id, parent.id);
2705 assert_eq!(context.ancestors[0].name, "Parent task");
2706
2707 assert_eq!(context.siblings.len(), 0);
2709
2710 assert_eq!(context.children.len(), 0);
2712 }
2713
2714 #[tokio::test]
2715 async fn test_get_task_context_with_children() {
2716 let ctx = TestContext::new().await;
2717 let task_mgr = TaskManager::new(ctx.pool());
2718
2719 let parent = task_mgr
2721 .add_task("Parent task", None, None, None)
2722 .await
2723 .unwrap();
2724 let child1 = task_mgr
2725 .add_task("Child 1", None, Some(parent.id), None)
2726 .await
2727 .unwrap();
2728 let child2 = task_mgr
2729 .add_task("Child 2", None, Some(parent.id), None)
2730 .await
2731 .unwrap();
2732 let child3 = task_mgr
2733 .add_task("Child 3", None, Some(parent.id), None)
2734 .await
2735 .unwrap();
2736
2737 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2738
2739 assert_eq!(context.task.id, parent.id);
2741
2742 assert_eq!(context.ancestors.len(), 0);
2744
2745 assert_eq!(context.siblings.len(), 0);
2747
2748 assert_eq!(context.children.len(), 3);
2750 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2751 assert!(child_ids.contains(&child1.id));
2752 assert!(child_ids.contains(&child2.id));
2753 assert!(child_ids.contains(&child3.id));
2754 }
2755
2756 #[tokio::test]
2757 async fn test_get_task_context_multi_level_hierarchy() {
2758 let ctx = TestContext::new().await;
2759 let task_mgr = TaskManager::new(ctx.pool());
2760
2761 let grandparent = task_mgr
2763 .add_task("Grandparent", None, None, None)
2764 .await
2765 .unwrap();
2766 let parent = task_mgr
2767 .add_task("Parent", None, Some(grandparent.id), None)
2768 .await
2769 .unwrap();
2770 let child = task_mgr
2771 .add_task("Child", None, Some(parent.id), None)
2772 .await
2773 .unwrap();
2774
2775 let context = task_mgr.get_task_context(child.id).await.unwrap();
2776
2777 assert_eq!(context.task.id, child.id);
2779
2780 assert_eq!(context.ancestors.len(), 2);
2782 assert_eq!(context.ancestors[0].id, parent.id);
2783 assert_eq!(context.ancestors[0].name, "Parent");
2784 assert_eq!(context.ancestors[1].id, grandparent.id);
2785 assert_eq!(context.ancestors[1].name, "Grandparent");
2786
2787 assert_eq!(context.siblings.len(), 0);
2789
2790 assert_eq!(context.children.len(), 0);
2792 }
2793
2794 #[tokio::test]
2795 async fn test_get_task_context_complex_family_tree() {
2796 let ctx = TestContext::new().await;
2797 let task_mgr = TaskManager::new(ctx.pool());
2798
2799 let root = task_mgr.add_task("Root", None, None, None).await.unwrap();
2807 let child1 = task_mgr
2808 .add_task("Child1", None, Some(root.id), None)
2809 .await
2810 .unwrap();
2811 let child2 = task_mgr
2812 .add_task("Child2", None, Some(root.id), None)
2813 .await
2814 .unwrap();
2815 let grandchild1 = task_mgr
2816 .add_task("Grandchild1", None, Some(child1.id), None)
2817 .await
2818 .unwrap();
2819 let grandchild2 = task_mgr
2820 .add_task("Grandchild2", None, Some(child1.id), None)
2821 .await
2822 .unwrap();
2823
2824 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2826
2827 assert_eq!(context.task.id, grandchild2.id);
2829
2830 assert_eq!(context.ancestors.len(), 2);
2832 assert_eq!(context.ancestors[0].id, child1.id);
2833 assert_eq!(context.ancestors[1].id, root.id);
2834
2835 assert_eq!(context.siblings.len(), 1);
2837 assert_eq!(context.siblings[0].id, grandchild1.id);
2838
2839 assert_eq!(context.children.len(), 0);
2841
2842 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2844 assert_eq!(context_child1.ancestors.len(), 1);
2845 assert_eq!(context_child1.ancestors[0].id, root.id);
2846 assert_eq!(context_child1.siblings.len(), 1);
2847 assert_eq!(context_child1.siblings[0].id, child2.id);
2848 assert_eq!(context_child1.children.len(), 2);
2849 }
2850
2851 #[tokio::test]
2852 async fn test_get_task_context_respects_priority_ordering() {
2853 let ctx = TestContext::new().await;
2854 let task_mgr = TaskManager::new(ctx.pool());
2855
2856 let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2858
2859 let child_low = task_mgr
2861 .add_task("Low priority", None, Some(parent.id), None)
2862 .await
2863 .unwrap();
2864 let _ = task_mgr
2865 .update_task(child_low.id, None, None, None, None, None, Some(10))
2866 .await
2867 .unwrap();
2868
2869 let child_high = task_mgr
2870 .add_task("High priority", None, Some(parent.id), None)
2871 .await
2872 .unwrap();
2873 let _ = task_mgr
2874 .update_task(child_high.id, None, None, None, None, None, Some(1))
2875 .await
2876 .unwrap();
2877
2878 let child_medium = task_mgr
2879 .add_task("Medium priority", None, Some(parent.id), None)
2880 .await
2881 .unwrap();
2882 let _ = task_mgr
2883 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2884 .await
2885 .unwrap();
2886
2887 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2888
2889 assert_eq!(context.children.len(), 3);
2891 assert_eq!(context.children[0].priority, Some(1));
2892 assert_eq!(context.children[1].priority, Some(5));
2893 assert_eq!(context.children[2].priority, Some(10));
2894 }
2895
2896 #[tokio::test]
2897 async fn test_get_task_context_nonexistent_task() {
2898 let ctx = TestContext::new().await;
2899 let task_mgr = TaskManager::new(ctx.pool());
2900
2901 let result = task_mgr.get_task_context(99999).await;
2902 assert!(result.is_err());
2903 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2904 }
2905
2906 #[tokio::test]
2907 async fn test_get_task_context_handles_null_priority() {
2908 let ctx = TestContext::new().await;
2909 let task_mgr = TaskManager::new(ctx.pool());
2910
2911 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2913 let _ = task_mgr
2914 .update_task(task1.id, None, None, None, None, None, Some(1))
2915 .await
2916 .unwrap();
2917
2918 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2919 let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2922 let _ = task_mgr
2923 .update_task(task3.id, None, None, None, None, None, Some(5))
2924 .await
2925 .unwrap();
2926
2927 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2928
2929 assert_eq!(context.siblings.len(), 2);
2931 assert_eq!(context.siblings[0].id, task1.id);
2933 assert_eq!(context.siblings[0].priority, Some(1));
2934 assert_eq!(context.siblings[1].id, task3.id);
2936 assert_eq!(context.siblings[1].priority, Some(5));
2937 }
2938
2939 #[tokio::test]
2940 async fn test_pick_next_tasks_priority_order() {
2941 let ctx = TestContext::new().await;
2942 let task_mgr = TaskManager::new(ctx.pool());
2943
2944 let critical = task_mgr
2946 .add_task("Critical Task", None, None, None)
2947 .await
2948 .unwrap();
2949 task_mgr
2950 .update_task(critical.id, None, None, None, None, None, Some(1))
2951 .await
2952 .unwrap();
2953
2954 let low = task_mgr
2955 .add_task("Low Task", None, None, None)
2956 .await
2957 .unwrap();
2958 task_mgr
2959 .update_task(low.id, None, None, None, None, None, Some(4))
2960 .await
2961 .unwrap();
2962
2963 let high = task_mgr
2964 .add_task("High Task", None, None, None)
2965 .await
2966 .unwrap();
2967 task_mgr
2968 .update_task(high.id, None, None, None, None, None, Some(2))
2969 .await
2970 .unwrap();
2971
2972 let medium = task_mgr
2973 .add_task("Medium Task", None, None, None)
2974 .await
2975 .unwrap();
2976 task_mgr
2977 .update_task(medium.id, None, None, None, None, None, Some(3))
2978 .await
2979 .unwrap();
2980
2981 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2983
2984 assert_eq!(tasks.len(), 4);
2985 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); }
2990
2991 #[tokio::test]
2992 async fn test_pick_next_prefers_doing_over_todo() {
2993 let ctx = TestContext::new().await;
2994 let task_mgr = TaskManager::new(ctx.pool());
2995 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2996
2997 let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2999 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
3000 workspace_mgr
3001 .set_current_task(parent_started.task.id, None)
3002 .await
3003 .unwrap();
3004
3005 let doing_subtask = task_mgr
3007 .add_task("Doing Subtask", None, Some(parent.id), None)
3008 .await
3009 .unwrap();
3010 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
3011 workspace_mgr
3013 .set_current_task(parent.id, None)
3014 .await
3015 .unwrap();
3016
3017 let _todo_subtask = task_mgr
3018 .add_task("Todo Subtask", None, Some(parent.id), None)
3019 .await
3020 .unwrap();
3021
3022 let result = task_mgr.pick_next().await.unwrap();
3024
3025 if let Some(task) = result.task {
3026 assert_eq!(
3027 task.id, doing_subtask.id,
3028 "Should recommend doing subtask over todo subtask"
3029 );
3030 assert_eq!(task.status, "doing");
3031 } else {
3032 panic!("Expected a task recommendation");
3033 }
3034 }
3035
3036 #[tokio::test]
3037 async fn test_multiple_doing_tasks_allowed() {
3038 let ctx = TestContext::new().await;
3039 let task_mgr = TaskManager::new(ctx.pool());
3040 let workspace_mgr = WorkspaceManager::new(ctx.pool());
3041
3042 let task_a = task_mgr.add_task("Task A", None, None, None).await.unwrap();
3044 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
3045 assert_eq!(task_a_started.task.status, "doing");
3046
3047 let current = workspace_mgr.get_current_task(None).await.unwrap();
3049 assert_eq!(current.current_task_id, Some(task_a.id));
3050
3051 let task_b = task_mgr.add_task("Task B", None, None, None).await.unwrap();
3053 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
3054 assert_eq!(task_b_started.task.status, "doing");
3055
3056 let current = workspace_mgr.get_current_task(None).await.unwrap();
3058 assert_eq!(current.current_task_id, Some(task_b.id));
3059
3060 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
3062 assert_eq!(
3063 task_a_after.status, "doing",
3064 "Task A should remain doing even though it is not current"
3065 );
3066
3067 let doing_tasks: Vec<Task> = sqlx::query_as(
3069 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
3070 FROM tasks WHERE status = 'doing' ORDER BY id"#
3071 )
3072 .fetch_all(ctx.pool())
3073 .await
3074 .unwrap();
3075
3076 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
3077 assert_eq!(doing_tasks[0].id, task_a.id);
3078 assert_eq!(doing_tasks[1].id, task_b.id);
3079 }
3080 #[tokio::test]
3081 async fn test_find_tasks_pagination() {
3082 let ctx = TestContext::new().await;
3083 let task_mgr = TaskManager::new(ctx.pool());
3084
3085 for i in 0..15 {
3087 task_mgr
3088 .add_task(&format!("Task {}", i), None, None, None)
3089 .await
3090 .unwrap();
3091 }
3092
3093 let page1 = task_mgr
3095 .find_tasks(None, None, None, Some(10), Some(0))
3096 .await
3097 .unwrap();
3098 assert_eq!(page1.tasks.len(), 10);
3099 assert_eq!(page1.total_count, 15);
3100 assert!(page1.has_more);
3101 assert_eq!(page1.offset, 0);
3102
3103 let page2 = task_mgr
3105 .find_tasks(None, None, None, Some(10), Some(10))
3106 .await
3107 .unwrap();
3108 assert_eq!(page2.tasks.len(), 5);
3109 assert_eq!(page2.total_count, 15);
3110 assert!(!page2.has_more);
3111 assert_eq!(page2.offset, 10);
3112 }
3113}
3114
3115