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
22#[derive(Debug, Default)]
25pub struct TaskUpdate<'a> {
26 pub name: Option<&'a str>,
27 pub spec: Option<&'a str>,
28 pub parent_id: Option<Option<i64>>,
29 pub status: Option<&'a str>,
30 pub complexity: Option<i32>,
31 pub priority: Option<i32>,
32 pub active_form: Option<&'a str>,
33 pub owner: Option<&'a str>,
34 pub metadata: Option<&'a str>,
35}
36
37pub struct TaskManager<'a> {
38 pool: &'a SqlitePool,
39 notifier: crate::notifications::NotificationSender,
40 cli_notifier: Option<crate::dashboard::cli_notifier::CliNotifier>,
41 project_path: Option<String>,
42}
43
44impl<'a> TaskManager<'a> {
45 pub fn new(pool: &'a SqlitePool) -> Self {
46 Self {
47 pool,
48 notifier: crate::notifications::NotificationSender::new(None),
49 cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
50 project_path: None,
51 }
52 }
53
54 pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
56 Self {
57 pool,
58 notifier: crate::notifications::NotificationSender::new(None),
59 cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
60 project_path: Some(project_path),
61 }
62 }
63
64 pub fn with_websocket(
66 pool: &'a SqlitePool,
67 ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
68 project_path: String,
69 ) -> Self {
70 Self {
71 pool,
72 notifier: crate::notifications::NotificationSender::new(Some(ws_state)),
73 cli_notifier: None, project_path: Some(project_path),
75 }
76 }
77
78 async fn notify_task_created(&self, task: &Task) {
80 use crate::dashboard::websocket::DatabaseOperationPayload;
81
82 if let Some(project_path) = &self.project_path {
84 let task_json = match serde_json::to_value(task) {
85 Ok(json) => json,
86 Err(e) => {
87 tracing::warn!(error = %e, "Failed to serialize task for notification");
88 return;
89 },
90 };
91
92 let payload =
93 DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
94 self.notifier.send(payload).await;
95 }
96
97 if let Some(cli_notifier) = &self.cli_notifier {
99 cli_notifier
100 .notify_task_changed(Some(task.id), "created", self.project_path.clone())
101 .await;
102 }
103 }
104
105 async fn notify_task_updated(&self, task: &Task) {
107 use crate::dashboard::websocket::DatabaseOperationPayload;
108
109 if let Some(project_path) = &self.project_path {
111 let task_json = match serde_json::to_value(task) {
112 Ok(json) => json,
113 Err(e) => {
114 tracing::warn!(error = %e, "Failed to serialize task for notification");
115 return;
116 },
117 };
118
119 let payload =
120 DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
121 self.notifier.send(payload).await;
122 }
123
124 if let Some(cli_notifier) = &self.cli_notifier {
126 cli_notifier
127 .notify_task_changed(Some(task.id), "updated", self.project_path.clone())
128 .await;
129 }
130 }
131
132 async fn notify_task_deleted(&self, task_id: i64) {
134 use crate::dashboard::websocket::DatabaseOperationPayload;
135
136 if let Some(project_path) = &self.project_path {
138 let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
139 self.notifier.send(payload).await;
140 }
141
142 if let Some(cli_notifier) = &self.cli_notifier {
144 cli_notifier
145 .notify_task_changed(Some(task_id), "deleted", self.project_path.clone())
146 .await;
147 }
148 }
149
150 #[tracing::instrument(skip(self), fields(task_name = %name))]
153 pub async fn add_task(
154 &self,
155 name: String,
156 spec: Option<String>,
157 parent_id: Option<i64>,
158 owner: Option<String>,
159 priority: Option<i32>,
160 metadata: Option<String>,
161 ) -> Result<Task> {
162 if let Some(pid) = parent_id {
164 self.check_task_exists(pid).await?;
165 }
166
167 let now = Utc::now();
168 let owner = owner.as_deref().unwrap_or("human").to_string();
169
170 let result = sqlx::query(
171 r#"
172 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner, priority, metadata)
173 VALUES (?, ?, ?, 'todo', ?, ?, ?, ?)
174 "#,
175 )
176 .bind(name)
177 .bind(spec)
178 .bind(parent_id)
179 .bind(now)
180 .bind(owner)
181 .bind(priority)
182 .bind(metadata)
183 .execute(self.pool)
184 .await?;
185
186 let id = result.last_insert_rowid();
187 let task = self.get_task(id).await?;
188
189 self.notify_task_created(&task).await;
191
192 Ok(task)
193 }
194
195 #[allow(clippy::too_many_arguments)]
218 pub async fn create_task_in_tx(
219 &self,
220 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
221 name: &str,
222 spec: Option<&str>,
223 priority: Option<i32>,
224 status: Option<&str>,
225 active_form: Option<&str>,
226 owner: &str,
227 ) -> Result<i64> {
228 let now = Utc::now();
229 let status = status.unwrap_or("todo");
230 let priority = priority.unwrap_or(3); let result = sqlx::query(
233 r#"
234 INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at, owner)
235 VALUES (?, ?, ?, ?, ?, ?, ?)
236 "#,
237 )
238 .bind(name)
239 .bind(spec)
240 .bind(priority)
241 .bind(status)
242 .bind(active_form)
243 .bind(now)
244 .bind(owner)
245 .execute(&mut **tx)
246 .await?;
247
248 Ok(result.last_insert_rowid())
249 }
250
251 pub async fn update_task_in_tx(
264 &self,
265 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
266 task_id: i64,
267 spec: Option<&str>,
268 priority: Option<i32>,
269 status: Option<&str>,
270 active_form: Option<&str>,
271 ) -> Result<()> {
272 if let Some(spec) = spec {
274 sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
275 .bind(spec)
276 .bind(task_id)
277 .execute(&mut **tx)
278 .await?;
279 }
280
281 if let Some(priority) = priority {
283 sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
284 .bind(priority)
285 .bind(task_id)
286 .execute(&mut **tx)
287 .await?;
288 }
289
290 if let Some(status) = status {
292 sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
293 .bind(status)
294 .bind(task_id)
295 .execute(&mut **tx)
296 .await?;
297 }
298
299 if let Some(active_form) = active_form {
301 sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
302 .bind(active_form)
303 .bind(task_id)
304 .execute(&mut **tx)
305 .await?;
306 }
307
308 Ok(())
309 }
310
311 pub async fn set_parent_in_tx(
315 &self,
316 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
317 task_id: i64,
318 parent_id: i64,
319 ) -> Result<()> {
320 sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
321 .bind(parent_id)
322 .bind(task_id)
323 .execute(&mut **tx)
324 .await?;
325
326 Ok(())
327 }
328
329 pub async fn clear_parent_in_tx(
333 &self,
334 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
335 task_id: i64,
336 ) -> Result<()> {
337 sqlx::query("UPDATE tasks SET parent_id = NULL WHERE id = ?")
338 .bind(task_id)
339 .execute(&mut **tx)
340 .await?;
341
342 Ok(())
343 }
344
345 pub async fn delete_task_in_tx(
357 &self,
358 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
359 task_id: i64,
360 ) -> Result<DeleteTaskResult> {
361 let task_info: Option<(i64,)> =
363 sqlx::query_as("SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL")
364 .bind(task_id)
365 .fetch_optional(&mut **tx)
366 .await?;
367
368 if task_info.is_none() {
369 return Ok(DeleteTaskResult {
370 found: false,
371 descendant_count: 0,
372 });
373 }
374
375 let descendant_count = self.count_descendants_in_tx(tx, task_id).await?;
377
378 let now = chrono::Utc::now();
380 sqlx::query(
381 r#"
382 WITH RECURSIVE subtree(id) AS (
383 SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL
384 UNION ALL
385 SELECT t.id FROM tasks t JOIN subtree s ON t.parent_id = s.id
386 WHERE t.deleted_at IS NULL
387 )
388 UPDATE tasks SET deleted_at = ? WHERE id IN (SELECT id FROM subtree)
389 "#,
390 )
391 .bind(task_id)
392 .bind(now)
393 .execute(&mut **tx)
394 .await?;
395
396 Ok(DeleteTaskResult {
397 found: true,
398 descendant_count,
399 })
400 }
401
402 async fn count_descendants_in_tx(
404 &self,
405 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
406 task_id: i64,
407 ) -> Result<i64> {
408 let count: (i64,) = sqlx::query_as(
410 r#"
411 WITH RECURSIVE descendants AS (
412 SELECT id FROM tasks WHERE parent_id = ? AND deleted_at IS NULL
413 UNION ALL
414 SELECT t.id FROM tasks t
415 INNER JOIN descendants d ON t.parent_id = d.id
416 WHERE t.deleted_at IS NULL
417 )
418 SELECT COUNT(*) FROM descendants
419 "#,
420 )
421 .bind(task_id)
422 .fetch_one(&mut **tx)
423 .await?;
424
425 Ok(count.0)
426 }
427
428 pub async fn find_focused_in_subtree_in_tx(
439 &self,
440 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
441 task_id: i64,
442 ) -> Result<Option<(i64, String)>> {
443 let row: Option<(i64, String)> = sqlx::query_as(
446 r#"
447 WITH RECURSIVE subtree AS (
448 SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL
449 UNION ALL
450 SELECT t.id FROM tasks t
451 INNER JOIN subtree s ON t.parent_id = s.id
452 WHERE t.deleted_at IS NULL
453 )
454 SELECT s.current_task_id, s.session_id FROM sessions s
455 WHERE s.current_task_id IN (SELECT id FROM subtree)
456 LIMIT 1
457 "#,
458 )
459 .bind(task_id)
460 .fetch_optional(&mut **tx)
461 .await?;
462
463 Ok(row)
464 }
465
466 pub async fn count_incomplete_children_in_tx(
471 &self,
472 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
473 task_id: i64,
474 ) -> Result<i64> {
475 let count: (i64,) = sqlx::query_as(crate::sql_constants::COUNT_INCOMPLETE_CHILDREN)
476 .bind(task_id)
477 .fetch_one(&mut **tx)
478 .await?;
479
480 Ok(count.0)
481 }
482
483 pub async fn complete_task_in_tx(
492 &self,
493 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
494 task_id: i64,
495 ) -> Result<()> {
496 let incomplete_count = self.count_incomplete_children_in_tx(tx, task_id).await?;
498 if incomplete_count > 0 {
499 return Err(IntentError::UncompletedChildren);
500 }
501
502 let now = chrono::Utc::now();
504 sqlx::query(
505 r#"
506 UPDATE tasks
507 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
508 WHERE id = ?
509 "#,
510 )
511 .bind(now)
512 .bind(task_id)
513 .execute(&mut **tx)
514 .await?;
515
516 Ok(())
517 }
518
519 pub async fn notify_batch_changed(&self) {
524 if let Some(cli_notifier) = &self.cli_notifier {
525 cli_notifier
526 .notify_task_changed(None, "batch_update", self.project_path.clone())
527 .await;
528 }
529 }
530
531 #[tracing::instrument(skip(self))]
537 pub async fn get_task(&self, id: i64) -> Result<Task> {
538 let task = sqlx::query_as::<_, Task>(
539 r#"
540 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
541 FROM tasks
542 WHERE id = ? AND deleted_at IS NULL
543 "#,
544 )
545 .bind(id)
546 .fetch_optional(self.pool)
547 .await?
548 .ok_or(IntentError::TaskNotFound(id))?;
549
550 Ok(task)
551 }
552
553 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
555 let task = self.get_task(id).await?;
556 let events_summary = self.get_events_summary(id).await?;
557
558 Ok(TaskWithEvents {
559 task,
560 events_summary: Some(events_summary),
561 })
562 }
563
564 pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
573 let mut chain = Vec::new();
574 let mut current_id = Some(task_id);
575
576 while let Some(id) = current_id {
577 let task = self.get_task(id).await?;
578 current_id = task.parent_id;
579 chain.push(task);
580 }
581
582 Ok(chain)
583 }
584
585 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
593 let task = self.get_task(id).await?;
594
595 let mut ancestors = Vec::new();
597 let mut current_parent_id = task.parent_id;
598 while let Some(parent_id) = current_parent_id {
599 let parent = self.get_task(parent_id).await?;
600 current_parent_id = parent.parent_id;
601 ancestors.push(parent);
602 }
603
604 let siblings = self.get_siblings(id, task.parent_id).await?;
605 let children = self.get_children(id).await?;
606 let blocking_tasks = self.get_blocking_tasks(id).await?;
607 let blocked_by_tasks = self.get_blocked_by_tasks(id).await?;
608
609 Ok(TaskContext {
610 task,
611 ancestors,
612 siblings,
613 children,
614 dependencies: crate::db::models::TaskDependencies {
615 blocking_tasks,
616 blocked_by_tasks,
617 },
618 })
619 }
620
621 pub async fn get_siblings(&self, id: i64, parent_id: Option<i64>) -> Result<Vec<Task>> {
623 if let Some(parent_id) = parent_id {
624 sqlx::query_as::<_, Task>(&format!(
625 "SELECT {} FROM tasks WHERE parent_id = ? AND id != ? AND deleted_at IS NULL ORDER BY priority ASC NULLS LAST, id ASC",
626 crate::sql_constants::TASK_COLUMNS
627 ))
628 .bind(parent_id)
629 .bind(id)
630 .fetch_all(self.pool)
631 .await
632 .map_err(Into::into)
633 } else {
634 sqlx::query_as::<_, Task>(&format!(
635 "SELECT {} FROM tasks WHERE parent_id IS NULL AND id != ? AND deleted_at IS NULL ORDER BY priority ASC NULLS LAST, id ASC",
636 crate::sql_constants::TASK_COLUMNS
637 ))
638 .bind(id)
639 .fetch_all(self.pool)
640 .await
641 .map_err(Into::into)
642 }
643 }
644
645 pub async fn get_children(&self, id: i64) -> Result<Vec<Task>> {
647 sqlx::query_as::<_, Task>(&format!(
648 "SELECT {} FROM tasks WHERE parent_id = ? AND deleted_at IS NULL ORDER BY priority ASC NULLS LAST, id ASC",
649 crate::sql_constants::TASK_COLUMNS
650 ))
651 .bind(id)
652 .fetch_all(self.pool)
653 .await
654 .map_err(Into::into)
655 }
656
657 pub async fn get_blocking_tasks(&self, id: i64) -> Result<Vec<Task>> {
659 sqlx::query_as::<_, Task>(&format!(
660 "SELECT {} FROM tasks t \
661 JOIN dependencies d ON t.id = d.blocking_task_id \
662 WHERE d.blocked_task_id = ? AND t.deleted_at IS NULL \
663 ORDER BY t.priority ASC NULLS LAST, t.id ASC",
664 crate::sql_constants::TASK_COLUMNS_PREFIXED
665 ))
666 .bind(id)
667 .fetch_all(self.pool)
668 .await
669 .map_err(Into::into)
670 }
671
672 pub async fn get_blocked_by_tasks(&self, id: i64) -> Result<Vec<Task>> {
674 sqlx::query_as::<_, Task>(&format!(
675 "SELECT {} FROM tasks t \
676 JOIN dependencies d ON t.id = d.blocked_task_id \
677 WHERE d.blocking_task_id = ? AND t.deleted_at IS NULL \
678 ORDER BY t.priority ASC NULLS LAST, t.id ASC",
679 crate::sql_constants::TASK_COLUMNS_PREFIXED
680 ))
681 .bind(id)
682 .fetch_all(self.pool)
683 .await
684 .map_err(Into::into)
685 }
686
687 pub async fn get_descendants(&self, task_id: i64) -> Result<Vec<Task>> {
690 let descendants = sqlx::query_as::<_, Task>(
691 r#"
692 WITH RECURSIVE descendants AS (
693 SELECT id, parent_id, name, spec, status, complexity, priority,
694 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
695 FROM tasks
696 WHERE parent_id = ? AND deleted_at IS NULL
697
698 UNION ALL
699
700 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
701 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner, t.metadata
702 FROM tasks t
703 INNER JOIN descendants d ON t.parent_id = d.id
704 WHERE t.deleted_at IS NULL
705 )
706 SELECT * FROM descendants
707 ORDER BY parent_id NULLS FIRST, priority ASC NULLS LAST, id ASC
708 "#,
709 )
710 .bind(task_id)
711 .fetch_all(self.pool)
712 .await?;
713
714 Ok(descendants)
715 }
716
717 pub async fn get_status(
720 &self,
721 task_id: i64,
722 with_events: bool,
723 ) -> Result<crate::db::models::StatusResponse> {
724 use crate::db::models::{StatusResponse, TaskBrief};
725
726 let context = self.get_task_context(task_id).await?;
728
729 let descendants_full = self.get_descendants(task_id).await?;
731
732 let siblings: Vec<TaskBrief> = context.siblings.iter().map(TaskBrief::from).collect();
734 let descendants: Vec<TaskBrief> = descendants_full.iter().map(TaskBrief::from).collect();
735
736 let events = if with_events {
738 let event_mgr = crate::events::EventManager::new(self.pool);
739 Some(
740 event_mgr
741 .list_events(Some(task_id), Some(50), None, None)
742 .await?,
743 )
744 } else {
745 None
746 };
747
748 Ok(StatusResponse {
749 focused_task: context.task,
750 ancestors: context.ancestors,
751 siblings,
752 descendants,
753 events,
754 })
755 }
756
757 pub async fn get_root_tasks(&self) -> Result<Vec<Task>> {
759 let tasks = sqlx::query_as::<_, Task>(
760 r#"
761 SELECT id, parent_id, name, spec, status, complexity, priority,
762 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
763 FROM tasks
764 WHERE parent_id IS NULL AND deleted_at IS NULL
765 ORDER BY
766 CASE status
767 WHEN 'doing' THEN 0
768 WHEN 'todo' THEN 1
769 WHEN 'done' THEN 2
770 END,
771 priority ASC NULLS LAST,
772 id ASC
773 "#,
774 )
775 .fetch_all(self.pool)
776 .await?;
777
778 Ok(tasks)
779 }
780
781 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
783 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
784 .bind(task_id)
785 .fetch_one(self.pool)
786 .await?;
787
788 let recent_events = sqlx::query_as::<_, Event>(
789 r#"
790 SELECT id, task_id, timestamp, log_type, discussion_data
791 FROM events
792 WHERE task_id = ?
793 ORDER BY timestamp DESC
794 LIMIT 10
795 "#,
796 )
797 .bind(task_id)
798 .fetch_all(self.pool)
799 .await?;
800
801 Ok(EventsSummary {
802 total_count,
803 recent_events,
804 })
805 }
806
807 pub async fn update_task(&self, id: i64, update: TaskUpdate<'_>) -> Result<Task> {
809 let TaskUpdate {
810 name,
811 spec,
812 parent_id,
813 status,
814 complexity,
815 priority,
816 active_form,
817 owner,
818 metadata,
819 } = update;
820
821 let task = self.get_task(id).await?;
823
824 let status = if let Some(s) = status {
828 match crate::plan::TaskStatus::from_db_str(s) {
829 Some(ts) => Some(ts.as_db_str()),
830 None => return Err(IntentError::InvalidInput(format!("Invalid status: {}", s))),
831 }
832 } else {
833 None
834 };
835
836 if let Some(Some(pid)) = parent_id {
838 if pid == id {
839 return Err(IntentError::CircularDependency {
840 blocking_task_id: pid,
841 blocked_task_id: id,
842 });
843 }
844 self.check_task_exists(pid).await?;
845 self.check_circular_dependency(id, pid).await?;
846 }
847
848 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
850 sqlx::QueryBuilder::new("UPDATE tasks SET ");
851 let mut has_updates = false;
852
853 if let Some(n) = name {
854 if has_updates {
855 builder.push(", ");
856 }
857 builder.push("name = ").push_bind(n);
858 has_updates = true;
859 }
860
861 if let Some(s) = spec {
862 if has_updates {
863 builder.push(", ");
864 }
865 builder.push("spec = ").push_bind(s);
866 has_updates = true;
867 }
868
869 if let Some(pid) = parent_id {
870 if has_updates {
871 builder.push(", ");
872 }
873 match pid {
874 Some(p) => {
875 builder.push("parent_id = ").push_bind(p);
876 },
877 None => {
878 builder.push("parent_id = NULL");
879 },
880 }
881 has_updates = true;
882 }
883
884 if let Some(c) = complexity {
885 if has_updates {
886 builder.push(", ");
887 }
888 builder.push("complexity = ").push_bind(c);
889 has_updates = true;
890 }
891
892 if let Some(p) = priority {
893 if has_updates {
894 builder.push(", ");
895 }
896 builder.push("priority = ").push_bind(p);
897 has_updates = true;
898 }
899
900 if let Some(af) = active_form {
901 if has_updates {
902 builder.push(", ");
903 }
904 builder.push("active_form = ").push_bind(af);
905 has_updates = true;
906 }
907
908 if let Some(o) = owner {
909 if o.is_empty() {
910 return Err(IntentError::InvalidInput(
911 "owner cannot be empty".to_string(),
912 ));
913 }
914 if has_updates {
915 builder.push(", ");
916 }
917 builder.push("owner = ").push_bind(o);
918 has_updates = true;
919 }
920
921 if let Some(m) = metadata {
922 if has_updates {
923 builder.push(", ");
924 }
925 builder.push("metadata = ").push_bind(m);
926 has_updates = true;
927 }
928
929 if let Some(s) = status {
930 if has_updates {
931 builder.push(", ");
932 }
933 builder.push("status = ").push_bind(s);
934 has_updates = true;
935
936 let now = Utc::now();
938 let timestamp = now.to_rfc3339();
939 match s {
940 "todo" if task.first_todo_at.is_none() => {
941 builder.push(", first_todo_at = ").push_bind(timestamp);
942 },
943 "doing" if task.first_doing_at.is_none() => {
944 builder.push(", first_doing_at = ").push_bind(timestamp);
945 },
946 "done" if task.first_done_at.is_none() => {
947 builder.push(", first_done_at = ").push_bind(timestamp);
948 },
949 _ => {},
950 }
951 }
952
953 if !has_updates {
954 return Ok(task);
955 }
956
957 builder.push(" WHERE id = ").push_bind(id);
958
959 builder.build().execute(self.pool).await?;
960
961 let task = self.get_task(id).await?;
962
963 self.notify_task_updated(&task).await;
965
966 Ok(task)
967 }
968
969 pub async fn delete_task(&self, id: i64) -> Result<()> {
986 self.check_task_exists(id).await?;
987
988 if let Some((_, sid)) = self.find_focused_in_set(&[id]).await? {
990 return Err(IntentError::ActionNotAllowed(format!(
991 "Task #{} is focused by session '{}'. Unfocus it first.",
992 id, sid
993 )));
994 }
995
996 let now = chrono::Utc::now();
997 sqlx::query("UPDATE tasks SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL")
998 .bind(now)
999 .bind(id)
1000 .execute(self.pool)
1001 .await?;
1002
1003 self.notify_task_deleted(id).await;
1005
1006 Ok(())
1007 }
1008
1009 pub async fn delete_task_cascade(&self, id: i64) -> Result<usize> {
1014 let mut tx = self.pool.begin().await?;
1015
1016 let subtree_ids = self.get_subtree_ids_in_tx(&mut tx, id).await?;
1018
1019 if let Some((tid, sid)) = self.find_focused_in_subtree_in_tx(&mut tx, id).await? {
1021 return Err(IntentError::ActionNotAllowed(format!(
1022 "Cannot cascade delete: task #{} is focused by session '{}'. Unfocus it first.",
1023 tid, sid
1024 )));
1025 }
1026
1027 let delete_result = self.delete_task_in_tx(&mut tx, id).await?;
1028 if !delete_result.found {
1029 return Err(IntentError::TaskNotFound(id));
1030 }
1031
1032 tx.commit().await?;
1033
1034 for &deleted_id in &subtree_ids {
1042 self.notify_task_deleted(deleted_id).await;
1043 }
1044
1045 Ok(delete_result.descendant_count as usize)
1046 }
1047
1048 async fn get_subtree_ids_in_tx(
1052 &self,
1053 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
1054 task_id: i64,
1055 ) -> Result<Vec<i64>> {
1056 let mut ids: Vec<i64> = sqlx::query_scalar(
1057 r#"
1058 WITH RECURSIVE descendants AS (
1059 SELECT id FROM tasks WHERE parent_id = ? AND deleted_at IS NULL
1060 UNION ALL
1061 SELECT t.id FROM tasks t
1062 INNER JOIN descendants d ON t.parent_id = d.id
1063 WHERE t.deleted_at IS NULL
1064 )
1065 SELECT id FROM descendants
1066 ORDER BY id ASC
1067 "#,
1068 )
1069 .bind(task_id)
1070 .fetch_all(&mut **tx)
1071 .await?;
1072
1073 ids.push(task_id);
1074 Ok(ids)
1075 }
1076
1077 async fn find_focused_in_set(&self, task_ids: &[i64]) -> Result<Option<(i64, String)>> {
1080 if task_ids.is_empty() {
1081 return Ok(None);
1082 }
1083
1084 let placeholders: Vec<&str> = task_ids.iter().map(|_| "?").collect();
1086 let sql = format!(
1087 "SELECT current_task_id, session_id FROM sessions WHERE current_task_id IN ({}) LIMIT 1",
1088 placeholders.join(", ")
1089 );
1090
1091 let mut query = sqlx::query_as::<_, (i64, String)>(&sql);
1092 for id in task_ids {
1093 query = query.bind(id);
1094 }
1095
1096 Ok(query.fetch_optional(self.pool).await?)
1097 }
1098
1099 pub async fn add_dependency(&self, blocking_id: i64, blocked_id: i64) -> Result<()> {
1101 crate::dependencies::add_dependency(self.pool, blocking_id, blocked_id)
1102 .await
1103 .map(|_| ())
1104 }
1105
1106 pub async fn remove_dependency(&self, blocking_id: i64, blocked_id: i64) -> Result<()> {
1108 sqlx::query("DELETE FROM dependencies WHERE blocking_task_id = ? AND blocked_task_id = ?")
1109 .bind(blocking_id)
1110 .bind(blocked_id)
1111 .execute(self.pool)
1112 .await?;
1113 Ok(())
1114 }
1115
1116 pub async fn find_tasks(
1118 &self,
1119 status: Option<String>,
1120 parent_id: Option<Option<i64>>,
1121 sort_by: Option<TaskSortBy>,
1122 limit: Option<i64>,
1123 offset: Option<i64>,
1124 ) -> Result<PaginatedTasks> {
1125 let sort_by = sort_by.unwrap_or_default(); let limit = limit.unwrap_or(100);
1128 let offset = offset.unwrap_or(0);
1129
1130 let session_id = crate::workspace::resolve_session_id(None);
1132
1133 let mut where_clause = String::from("WHERE deleted_at IS NULL");
1135 let mut conditions = Vec::new();
1136
1137 if let Some(s) = status {
1138 let canonical = match crate::plan::TaskStatus::from_db_str(&s) {
1139 Some(ts) => ts.as_db_str(),
1140 None => return Err(IntentError::InvalidInput(format!("Invalid status: {}", s))),
1141 };
1142 where_clause.push_str(" AND status = ?");
1143 conditions.push(canonical.to_string());
1144 }
1145
1146 if let Some(pid) = parent_id {
1147 if let Some(p) = pid {
1148 where_clause.push_str(" AND parent_id = ?");
1149 conditions.push(p.to_string());
1150 } else {
1151 where_clause.push_str(" AND parent_id IS NULL");
1152 }
1153 }
1154
1155 let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
1157
1158 let order_clause = match sort_by {
1160 TaskSortBy::Id => {
1161 "ORDER BY id ASC".to_string()
1163 },
1164 TaskSortBy::Priority => {
1165 "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
1167 .to_string()
1168 },
1169 TaskSortBy::Time => {
1170 r#"ORDER BY
1172 CASE status
1173 WHEN 'doing' THEN first_doing_at
1174 WHEN 'todo' THEN first_todo_at
1175 WHEN 'done' THEN first_done_at
1176 END ASC NULLS LAST,
1177 id ASC"#
1178 .to_string()
1179 },
1180 TaskSortBy::FocusAware => {
1181 r#"ORDER BY
1183 CASE
1184 WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
1185 WHEN t.status = 'doing' THEN 1
1186 WHEN t.status = 'todo' THEN 2
1187 ELSE 3
1188 END ASC,
1189 COALESCE(t.priority, 999) ASC,
1190 t.id ASC"#
1191 .to_string()
1192 },
1193 };
1194
1195 let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
1197 let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
1198 for cond in &conditions {
1199 count_q = count_q.bind(cond);
1200 }
1201 let total_count = count_q.fetch_one(self.pool).await?;
1202
1203 let main_query = format!(
1205 "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata FROM tasks t {} {} LIMIT ? OFFSET ?",
1206 where_clause, order_clause
1207 );
1208
1209 let mut q = sqlx::query_as::<_, Task>(&main_query);
1210 for cond in conditions {
1211 q = q.bind(cond);
1212 }
1213 if uses_session_bind {
1215 q = q.bind(&session_id);
1216 }
1217 q = q.bind(limit);
1218 q = q.bind(offset);
1219
1220 let tasks = q.fetch_all(self.pool).await?;
1221
1222 let has_more = offset + (tasks.len() as i64) < total_count;
1224
1225 Ok(PaginatedTasks {
1226 tasks,
1227 total_count,
1228 has_more,
1229 limit,
1230 offset,
1231 })
1232 }
1233
1234 pub async fn get_stats(&self) -> Result<WorkspaceStats> {
1239 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
1240 r#"SELECT
1241 COUNT(*) as total,
1242 COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
1243 COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
1244 COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
1245 FROM tasks WHERE deleted_at IS NULL"#,
1246 )
1247 .fetch_one(self.pool)
1248 .await?;
1249
1250 Ok(WorkspaceStats {
1251 total_tasks: row.0,
1252 todo: row.1,
1253 doing: row.2,
1254 done: row.3,
1255 })
1256 }
1257
1258 #[tracing::instrument(skip(self))]
1260 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
1261 let task_exists: bool =
1263 sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1264 .bind(id)
1265 .fetch_one(self.pool)
1266 .await?;
1267
1268 if !task_exists {
1269 return Err(IntentError::TaskNotFound(id));
1270 }
1271
1272 use crate::dependencies::get_incomplete_blocking_tasks;
1274 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
1275 return Err(IntentError::TaskBlocked {
1276 task_id: id,
1277 blocking_task_ids: blocking_tasks,
1278 });
1279 }
1280
1281 let mut tx = self.pool.begin().await?;
1282
1283 let now = Utc::now();
1284
1285 sqlx::query(
1287 r#"
1288 UPDATE tasks
1289 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
1290 WHERE id = ?
1291 "#,
1292 )
1293 .bind(now)
1294 .bind(id)
1295 .execute(&mut *tx)
1296 .await?;
1297
1298 let session_id = crate::workspace::resolve_session_id(None);
1301 sqlx::query(
1302 r#"
1303 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
1304 VALUES (?, ?, datetime('now'), datetime('now'))
1305 ON CONFLICT(session_id) DO UPDATE SET
1306 current_task_id = excluded.current_task_id,
1307 last_active_at = datetime('now')
1308 "#,
1309 )
1310 .bind(&session_id)
1311 .bind(id)
1312 .execute(&mut *tx)
1313 .await?;
1314
1315 tx.commit().await?;
1316
1317 if with_events {
1318 let result = self.get_task_with_events(id).await?;
1319 self.notify_task_updated(&result.task).await;
1320 Ok(result)
1321 } else {
1322 let task = self.get_task(id).await?;
1323 self.notify_task_updated(&task).await;
1324 Ok(TaskWithEvents {
1325 task,
1326 events_summary: None,
1327 })
1328 }
1329 }
1330
1331 async fn build_next_step_suggestion(
1335 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
1336 id: i64,
1337 task_name: &str,
1338 parent_id: Option<i64>,
1339 ) -> Result<NextStepSuggestion> {
1340 if let Some(parent_task_id) = parent_id {
1341 let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1342 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ? AND deleted_at IS NULL",
1343 )
1344 .bind(parent_task_id)
1345 .bind(id)
1346 .fetch_one(&mut **tx)
1347 .await?;
1348
1349 let parent_name: String =
1350 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1351 .bind(parent_task_id)
1352 .fetch_one(&mut **tx)
1353 .await?;
1354
1355 if remaining_siblings == 0 {
1356 Ok(NextStepSuggestion::ParentIsReady {
1357 message: format!(
1358 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1359 parent_task_id, parent_name
1360 ),
1361 parent_task_id,
1362 parent_task_name: parent_name,
1363 })
1364 } else {
1365 Ok(NextStepSuggestion::SiblingTasksRemain {
1366 message: format!(
1367 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1368 id, parent_task_id, parent_name
1369 ),
1370 parent_task_id,
1371 parent_task_name: parent_name,
1372 remaining_siblings_count: remaining_siblings,
1373 })
1374 }
1375 } else {
1376 let child_count: i64 =
1377 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1378 .bind(id)
1379 .fetch_one(&mut **tx)
1380 .await?;
1381
1382 if child_count > 0 {
1383 Ok(NextStepSuggestion::TopLevelTaskCompleted {
1384 message: format!(
1385 "Top-level task #{} '{}' has been completed. Well done!",
1386 id, task_name
1387 ),
1388 completed_task_id: id,
1389 completed_task_name: task_name.to_string(),
1390 })
1391 } else {
1392 let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1393 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ? AND deleted_at IS NULL",
1394 )
1395 .bind(id)
1396 .fetch_one(&mut **tx)
1397 .await?;
1398
1399 if remaining_tasks == 0 {
1400 Ok(NextStepSuggestion::WorkspaceIsClear {
1401 message: format!(
1402 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1403 id
1404 ),
1405 completed_task_id: id,
1406 })
1407 } else {
1408 Ok(NextStepSuggestion::NoParentContext {
1409 message: format!("Task #{} '{}' has been completed.", id, task_name),
1410 completed_task_id: id,
1411 completed_task_name: task_name.to_string(),
1412 })
1413 }
1414 }
1415 }
1416 }
1417
1418 #[tracing::instrument(skip(self))]
1427 pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1428 let session_id = crate::workspace::resolve_session_id(None);
1429 let mut tx = self.pool.begin().await?;
1430
1431 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1433 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1434 )
1435 .bind(&session_id)
1436 .fetch_optional(&mut *tx)
1437 .await?
1438 .flatten();
1439
1440 let id = current_task_id.ok_or(IntentError::InvalidInput(
1441 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
1442 ))?;
1443
1444 let task_info: (String, Option<i64>, String) =
1446 sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
1447 .bind(id)
1448 .fetch_one(&mut *tx)
1449 .await?;
1450 let (task_name, parent_id, owner) = task_info;
1451
1452 if owner == "human" && is_ai_caller {
1455 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1456 task_id: id,
1457 task_name: task_name.clone(),
1458 });
1459 }
1460
1461 self.complete_task_in_tx(&mut tx, id).await?;
1463
1464 sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1466 .bind(&session_id)
1467 .execute(&mut *tx)
1468 .await?;
1469
1470 let next_step_suggestion =
1471 Self::build_next_step_suggestion(&mut tx, id, &task_name, parent_id).await?;
1472
1473 tx.commit().await?;
1474
1475 let completed_task = self.get_task(id).await?;
1477 self.notify_task_updated(&completed_task).await;
1478
1479 Ok(DoneTaskResponse {
1480 completed_task,
1481 workspace_status: WorkspaceStatus {
1482 current_task_id: None,
1483 },
1484 next_step_suggestion,
1485 })
1486 }
1487
1488 #[tracing::instrument(skip(self))]
1498 pub async fn done_task_by_id(&self, id: i64, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1499 let session_id = crate::workspace::resolve_session_id(None);
1500 let mut tx = self.pool.begin().await?;
1501
1502 let task_info: (String, Option<i64>, String) = sqlx::query_as(
1504 "SELECT name, parent_id, owner FROM tasks WHERE id = ? AND deleted_at IS NULL",
1505 )
1506 .bind(id)
1507 .fetch_optional(&mut *tx)
1508 .await?
1509 .ok_or(IntentError::TaskNotFound(id))?;
1510 let (task_name, parent_id, owner) = task_info;
1511
1512 if owner == "human" && is_ai_caller {
1514 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1515 task_id: id,
1516 task_name: task_name.clone(),
1517 });
1518 }
1519
1520 self.complete_task_in_tx(&mut tx, id).await?;
1522
1523 sqlx::query(
1525 "UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ? AND current_task_id = ?",
1526 )
1527 .bind(&session_id)
1528 .bind(id)
1529 .execute(&mut *tx)
1530 .await?;
1531
1532 let actual_current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1534 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1535 )
1536 .bind(&session_id)
1537 .fetch_optional(&mut *tx)
1538 .await?
1539 .flatten();
1540
1541 let next_step_suggestion =
1542 Self::build_next_step_suggestion(&mut tx, id, &task_name, parent_id).await?;
1543
1544 let synthesis_result = self.try_synthesize_task_description(id, &task_name).await;
1546
1547 tx.commit().await?;
1548
1549 let mut completed_task = self.get_task(id).await?;
1551
1552 if let Ok(Some(new_spec)) = synthesis_result {
1554 completed_task = self
1555 .apply_synthesis_if_appropriate(completed_task, &new_spec, &owner)
1556 .await?;
1557 }
1558
1559 crate::llm::analyze_task_structure_background(self.pool.clone());
1561
1562 self.notify_task_updated(&completed_task).await;
1563
1564 Ok(DoneTaskResponse {
1565 completed_task,
1566 workspace_status: WorkspaceStatus {
1567 current_task_id: actual_current_task_id,
1568 },
1569 next_step_suggestion,
1570 })
1571 }
1572
1573 async fn check_task_exists(&self, id: i64) -> Result<()> {
1575 let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1576 .bind(id)
1577 .fetch_one(self.pool)
1578 .await?;
1579
1580 if !exists {
1581 return Err(IntentError::TaskNotFound(id));
1582 }
1583
1584 Ok(())
1585 }
1586
1587 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1589 let mut current_id = new_parent_id;
1590
1591 loop {
1592 if current_id == task_id {
1593 return Err(IntentError::CircularDependency {
1594 blocking_task_id: new_parent_id,
1595 blocked_task_id: task_id,
1596 });
1597 }
1598
1599 let parent: Option<i64> =
1600 sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1601 .bind(current_id)
1602 .fetch_optional(self.pool)
1603 .await?
1604 .flatten();
1605
1606 match parent {
1607 Some(pid) => current_id = pid,
1608 None => break,
1609 }
1610 }
1611
1612 Ok(())
1613 }
1614 pub async fn spawn_subtask(
1618 &self,
1619 name: &str,
1620 spec: Option<&str>,
1621 ) -> Result<SpawnSubtaskResponse> {
1622 let session_id = crate::workspace::resolve_session_id(None);
1624 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1625 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1626 )
1627 .bind(&session_id)
1628 .fetch_optional(self.pool)
1629 .await?
1630 .flatten();
1631
1632 let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1633 "No current task to create subtask under".to_string(),
1634 ))?;
1635
1636 let parent_name: String =
1638 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1639 .bind(parent_id)
1640 .fetch_one(self.pool)
1641 .await?;
1642
1643 let subtask = self
1645 .add_task(
1646 name.to_string(),
1647 spec.map(|s| s.to_string()),
1648 Some(parent_id),
1649 Some("ai".to_string()),
1650 None,
1651 None,
1652 )
1653 .await?;
1654
1655 self.start_task(subtask.id, false).await?;
1658
1659 Ok(SpawnSubtaskResponse {
1660 subtask: SubtaskInfo {
1661 id: subtask.id,
1662 name: subtask.name,
1663 parent_id,
1664 status: "doing".to_string(),
1665 },
1666 parent_task: ParentTaskInfo {
1667 id: parent_id,
1668 name: parent_name,
1669 },
1670 })
1671 }
1672
1673 pub async fn pick_next_tasks(
1686 &self,
1687 max_count: usize,
1688 capacity_limit: usize,
1689 ) -> Result<Vec<Task>> {
1690 let mut tx = self.pool.begin().await?;
1691
1692 let doing_count: i64 =
1694 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1695 .fetch_one(&mut *tx)
1696 .await?;
1697
1698 let available = capacity_limit.saturating_sub(doing_count as usize);
1700 if available == 0 {
1701 return Ok(vec![]);
1702 }
1703
1704 let limit = std::cmp::min(max_count, available);
1705
1706 let todo_tasks = sqlx::query_as::<_, Task>(
1708 r#"
1709 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1710 FROM tasks
1711 WHERE status = 'todo' AND deleted_at IS NULL
1712 ORDER BY
1713 COALESCE(priority, 0) ASC,
1714 COALESCE(complexity, 5) ASC,
1715 id ASC
1716 LIMIT ?
1717 "#,
1718 )
1719 .bind(limit as i64)
1720 .fetch_all(&mut *tx)
1721 .await?;
1722
1723 if todo_tasks.is_empty() {
1724 return Ok(vec![]);
1725 }
1726
1727 let now = Utc::now();
1728
1729 for task in &todo_tasks {
1731 sqlx::query(
1732 r#"
1733 UPDATE tasks
1734 SET status = 'doing',
1735 first_doing_at = COALESCE(first_doing_at, ?)
1736 WHERE id = ?
1737 "#,
1738 )
1739 .bind(now)
1740 .bind(task.id)
1741 .execute(&mut *tx)
1742 .await?;
1743 }
1744
1745 tx.commit().await?;
1746
1747 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1749 let placeholders = vec!["?"; task_ids.len()].join(",");
1750 let query = format!(
1751 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1752 FROM tasks WHERE id IN ({})
1753 ORDER BY
1754 COALESCE(priority, 0) ASC,
1755 COALESCE(complexity, 5) ASC,
1756 id ASC",
1757 placeholders
1758 );
1759
1760 let mut q = sqlx::query_as::<_, Task>(&query);
1761 for id in task_ids {
1762 q = q.bind(id);
1763 }
1764
1765 let updated_tasks = q.fetch_all(self.pool).await?;
1766 Ok(updated_tasks)
1767 }
1768
1769 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1778 let session_id = crate::workspace::resolve_session_id(None);
1780 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1781 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1782 )
1783 .bind(&session_id)
1784 .fetch_optional(self.pool)
1785 .await?
1786 .flatten();
1787
1788 if let Some(current_id) = current_task_id {
1789 let doing_subtasks = sqlx::query_as::<_, Task>(
1792 r#"
1793 SELECT id, parent_id, name, spec, status, complexity, priority,
1794 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1795 FROM tasks
1796 WHERE parent_id = ? AND status = 'doing' AND deleted_at IS NULL
1797 AND NOT EXISTS (
1798 SELECT 1 FROM dependencies d
1799 JOIN tasks bt ON d.blocking_task_id = bt.id
1800 WHERE d.blocked_task_id = tasks.id
1801 AND bt.status != 'done' AND bt.deleted_at IS NULL
1802 )
1803 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1804 LIMIT 1
1805 "#,
1806 )
1807 .bind(current_id)
1808 .fetch_optional(self.pool)
1809 .await?;
1810
1811 if let Some(task) = doing_subtasks {
1812 return Ok(PickNextResponse::focused_subtask(task));
1813 }
1814
1815 let todo_subtasks = sqlx::query_as::<_, Task>(
1817 r#"
1818 SELECT id, parent_id, name, spec, status, complexity, priority,
1819 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1820 FROM tasks
1821 WHERE parent_id = ? AND status = 'todo' AND deleted_at IS NULL
1822 AND NOT EXISTS (
1823 SELECT 1 FROM dependencies d
1824 JOIN tasks bt ON d.blocking_task_id = bt.id
1825 WHERE d.blocked_task_id = tasks.id
1826 AND bt.status != 'done' AND bt.deleted_at IS NULL
1827 )
1828 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1829 LIMIT 1
1830 "#,
1831 )
1832 .bind(current_id)
1833 .fetch_optional(self.pool)
1834 .await?;
1835
1836 if let Some(task) = todo_subtasks {
1837 return Ok(PickNextResponse::focused_subtask(task));
1838 }
1839 }
1840
1841 let doing_top_level = if let Some(current_id) = current_task_id {
1844 sqlx::query_as::<_, Task>(
1845 r#"
1846 SELECT id, parent_id, name, spec, status, complexity, priority,
1847 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1848 FROM tasks
1849 WHERE parent_id IS NULL AND status = 'doing' AND id != ? AND deleted_at IS NULL
1850 AND NOT EXISTS (
1851 SELECT 1 FROM dependencies d
1852 JOIN tasks bt ON d.blocking_task_id = bt.id
1853 WHERE d.blocked_task_id = tasks.id
1854 AND bt.status != 'done' AND bt.deleted_at IS NULL
1855 )
1856 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1857 LIMIT 1
1858 "#,
1859 )
1860 .bind(current_id)
1861 .fetch_optional(self.pool)
1862 .await?
1863 } else {
1864 sqlx::query_as::<_, Task>(
1865 r#"
1866 SELECT id, parent_id, name, spec, status, complexity, priority,
1867 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1868 FROM tasks
1869 WHERE parent_id IS NULL AND status = 'doing' AND deleted_at IS NULL
1870 AND NOT EXISTS (
1871 SELECT 1 FROM dependencies d
1872 JOIN tasks bt ON d.blocking_task_id = bt.id
1873 WHERE d.blocked_task_id = tasks.id
1874 AND bt.status != 'done' AND bt.deleted_at IS NULL
1875 )
1876 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1877 LIMIT 1
1878 "#,
1879 )
1880 .fetch_optional(self.pool)
1881 .await?
1882 };
1883
1884 if let Some(task) = doing_top_level {
1885 return Ok(PickNextResponse::top_level_task(task));
1886 }
1887
1888 let todo_top_level = sqlx::query_as::<_, Task>(
1891 r#"
1892 SELECT id, parent_id, name, spec, status, complexity, priority,
1893 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1894 FROM tasks
1895 WHERE parent_id IS NULL AND status = 'todo' AND deleted_at IS NULL
1896 AND NOT EXISTS (
1897 SELECT 1 FROM dependencies d
1898 JOIN tasks bt ON d.blocking_task_id = bt.id
1899 WHERE d.blocked_task_id = tasks.id
1900 AND bt.status != 'done' AND bt.deleted_at IS NULL
1901 )
1902 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1903 LIMIT 1
1904 "#,
1905 )
1906 .fetch_optional(self.pool)
1907 .await?;
1908
1909 if let Some(task) = todo_top_level {
1910 return Ok(PickNextResponse::top_level_task(task));
1911 }
1912
1913 let total_tasks: i64 =
1916 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1917 .fetch_one(self.pool)
1918 .await?;
1919
1920 if total_tasks == 0 {
1921 return Ok(PickNextResponse::no_tasks_in_project());
1922 }
1923
1924 let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1926 "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing') AND deleted_at IS NULL",
1927 )
1928 .fetch_one(self.pool)
1929 .await?;
1930
1931 if todo_or_doing_count == 0 {
1932 return Ok(PickNextResponse::all_tasks_completed());
1933 }
1934
1935 Ok(PickNextResponse::no_available_todos())
1937 }
1938
1939 async fn try_synthesize_task_description(
1945 &self,
1946 task_id: i64,
1947 task_name: &str,
1948 ) -> Result<Option<String>> {
1949 let task = self.get_task(task_id).await?;
1951 let events = crate::events::EventManager::new(self.pool)
1952 .list_events(Some(task_id), None, None, None)
1953 .await?;
1954
1955 match crate::llm::synthesize_task_description(
1957 self.pool,
1958 task_name,
1959 task.spec.as_deref(),
1960 &events,
1961 )
1962 .await
1963 {
1964 Ok(synthesis) => Ok(synthesis),
1965 Err(e) => {
1966 tracing::warn!("LLM synthesis failed: {}", e);
1968 Ok(None)
1969 },
1970 }
1971 }
1972
1973 async fn apply_synthesis_if_appropriate(
1978 &self,
1979 task: Task,
1980 new_spec: &str,
1981 owner: &str,
1982 ) -> Result<Task> {
1983 if owner == "ai" {
1984 tracing::info!("Auto-applying LLM synthesis for AI-owned task #{}", task.id);
1986
1987 let updated = self
1988 .update_task(
1989 task.id,
1990 TaskUpdate {
1991 spec: Some(new_spec),
1992 ..Default::default()
1993 },
1994 )
1995 .await?;
1996
1997 Ok(updated)
1998 } else {
1999 tracing::info!(
2002 "LLM synthesis available for human-owned task #{}, but auto-apply disabled. \
2003 User would be prompted in interactive mode.",
2004 task.id
2005 );
2006 eprintln!("\n💡 LLM generated a task summary:");
2007 eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2008 eprintln!("{}", new_spec);
2009 eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2010 eprintln!("(Auto-apply disabled for human-owned tasks)");
2011 eprintln!(
2012 "To apply manually: ie task update {} --description \"<new spec>\"",
2013 task.id
2014 );
2015
2016 Ok(task) }
2018 }
2019}
2020
2021impl crate::backend::TaskBackend for TaskManager<'_> {
2022 fn get_task(&self, id: i64) -> impl std::future::Future<Output = Result<Task>> + Send {
2023 self.get_task(id)
2024 }
2025
2026 fn get_task_with_events(
2027 &self,
2028 id: i64,
2029 ) -> impl std::future::Future<Output = Result<TaskWithEvents>> + Send {
2030 self.get_task_with_events(id)
2031 }
2032
2033 fn get_task_ancestry(
2034 &self,
2035 task_id: i64,
2036 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2037 self.get_task_ancestry(task_id)
2038 }
2039
2040 fn get_task_context(
2041 &self,
2042 id: i64,
2043 ) -> impl std::future::Future<Output = Result<TaskContext>> + Send {
2044 self.get_task_context(id)
2045 }
2046
2047 fn get_siblings(
2048 &self,
2049 id: i64,
2050 parent_id: Option<i64>,
2051 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2052 self.get_siblings(id, parent_id)
2053 }
2054
2055 fn get_children(&self, id: i64) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2056 self.get_children(id)
2057 }
2058
2059 fn get_blocking_tasks(
2060 &self,
2061 id: i64,
2062 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2063 self.get_blocking_tasks(id)
2064 }
2065
2066 fn get_blocked_by_tasks(
2067 &self,
2068 id: i64,
2069 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2070 self.get_blocked_by_tasks(id)
2071 }
2072
2073 fn get_descendants(
2074 &self,
2075 task_id: i64,
2076 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2077 self.get_descendants(task_id)
2078 }
2079
2080 fn get_status(
2081 &self,
2082 task_id: i64,
2083 with_events: bool,
2084 ) -> impl std::future::Future<Output = Result<crate::db::models::StatusResponse>> + Send {
2085 self.get_status(task_id, with_events)
2086 }
2087
2088 fn get_root_tasks(&self) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2089 self.get_root_tasks()
2090 }
2091
2092 fn find_tasks(
2093 &self,
2094 status: Option<String>,
2095 parent_id: Option<Option<i64>>,
2096 sort_by: Option<TaskSortBy>,
2097 limit: Option<i64>,
2098 offset: Option<i64>,
2099 ) -> impl std::future::Future<Output = Result<PaginatedTasks>> + Send {
2100 self.find_tasks(status, parent_id, sort_by, limit, offset)
2101 }
2102
2103 fn add_task(
2104 &self,
2105 name: String,
2106 spec: Option<String>,
2107 parent_id: Option<i64>,
2108 owner: Option<String>,
2109 priority: Option<i32>,
2110 metadata: Option<String>,
2111 ) -> impl std::future::Future<Output = Result<Task>> + Send {
2112 self.add_task(name, spec, parent_id, owner, priority, metadata)
2113 }
2114
2115 fn update_task(
2116 &self,
2117 id: i64,
2118 update: TaskUpdate<'_>,
2119 ) -> impl std::future::Future<Output = Result<Task>> + Send {
2120 self.update_task(id, update)
2121 }
2122
2123 fn delete_task(&self, id: i64) -> impl std::future::Future<Output = Result<()>> + Send {
2124 self.delete_task(id)
2125 }
2126
2127 fn delete_task_cascade(
2128 &self,
2129 id: i64,
2130 ) -> impl std::future::Future<Output = Result<usize>> + Send {
2131 self.delete_task_cascade(id)
2132 }
2133
2134 fn add_dependency(
2135 &self,
2136 blocking_id: i64,
2137 blocked_id: i64,
2138 ) -> impl std::future::Future<Output = Result<()>> + Send {
2139 self.add_dependency(blocking_id, blocked_id)
2140 }
2141
2142 fn remove_dependency(
2143 &self,
2144 blocking_id: i64,
2145 blocked_id: i64,
2146 ) -> impl std::future::Future<Output = Result<()>> + Send {
2147 self.remove_dependency(blocking_id, blocked_id)
2148 }
2149
2150 fn start_task(
2151 &self,
2152 id: i64,
2153 with_events: bool,
2154 ) -> impl std::future::Future<Output = Result<TaskWithEvents>> + Send {
2155 self.start_task(id, with_events)
2156 }
2157
2158 fn done_task(
2159 &self,
2160 is_ai_caller: bool,
2161 ) -> impl std::future::Future<Output = Result<DoneTaskResponse>> + Send {
2162 self.done_task(is_ai_caller)
2163 }
2164
2165 fn done_task_by_id(
2166 &self,
2167 id: i64,
2168 is_ai_caller: bool,
2169 ) -> impl std::future::Future<Output = Result<DoneTaskResponse>> + Send {
2170 self.done_task_by_id(id, is_ai_caller)
2171 }
2172
2173 fn pick_next(&self) -> impl std::future::Future<Output = Result<PickNextResponse>> + Send {
2174 self.pick_next()
2175 }
2176}
2177
2178impl crate::backend::SearchBackend for TaskManager<'_> {
2179 fn search(
2180 &self,
2181 query: String,
2182 include_tasks: bool,
2183 include_events: bool,
2184 limit: Option<i64>,
2185 offset: Option<i64>,
2186 ) -> impl std::future::Future<Output = Result<crate::db::models::PaginatedSearchResults>> + Send
2187 {
2188 use crate::search::SearchManager;
2189 let mgr = SearchManager::new(self.pool);
2190 async move {
2191 mgr.search(&query, include_tasks, include_events, limit, offset, false)
2192 .await
2193 }
2194 }
2195}
2196
2197#[cfg(test)]
2198mod tests {
2199 use super::*;
2200 use crate::events::EventManager;
2201 use crate::test_utils::test_helpers::TestContext;
2202 use crate::workspace::WorkspaceManager;
2203
2204 #[tokio::test]
2205 async fn test_get_stats_empty() {
2206 let ctx = TestContext::new().await;
2207 let manager = TaskManager::new(ctx.pool());
2208
2209 let stats = manager.get_stats().await.unwrap();
2210
2211 assert_eq!(stats.total_tasks, 0);
2212 assert_eq!(stats.todo, 0);
2213 assert_eq!(stats.doing, 0);
2214 assert_eq!(stats.done, 0);
2215 }
2216
2217 #[tokio::test]
2218 async fn test_get_stats_with_tasks() {
2219 let ctx = TestContext::new().await;
2220 let manager = TaskManager::new(ctx.pool());
2221
2222 let task1 = manager
2224 .add_task("Task 1".to_string(), None, None, None, None, None)
2225 .await
2226 .unwrap();
2227 let task2 = manager
2228 .add_task("Task 2".to_string(), None, None, None, None, None)
2229 .await
2230 .unwrap();
2231 let _task3 = manager
2232 .add_task("Task 3".to_string(), None, None, None, None, None)
2233 .await
2234 .unwrap();
2235
2236 manager
2238 .update_task(
2239 task1.id,
2240 TaskUpdate {
2241 status: Some("doing"),
2242 ..Default::default()
2243 },
2244 )
2245 .await
2246 .unwrap();
2247 manager
2248 .update_task(
2249 task2.id,
2250 TaskUpdate {
2251 status: Some("done"),
2252 ..Default::default()
2253 },
2254 )
2255 .await
2256 .unwrap();
2257 let stats = manager.get_stats().await.unwrap();
2260
2261 assert_eq!(stats.total_tasks, 3);
2262 assert_eq!(stats.todo, 1);
2263 assert_eq!(stats.doing, 1);
2264 assert_eq!(stats.done, 1);
2265 }
2266
2267 #[tokio::test]
2268 async fn test_add_task() {
2269 let ctx = TestContext::new().await;
2270 let manager = TaskManager::new(ctx.pool());
2271
2272 let task = manager
2273 .add_task("Test task".to_string(), None, None, None, None, None)
2274 .await
2275 .unwrap();
2276
2277 assert_eq!(task.name, "Test task");
2278 assert_eq!(task.status, "todo");
2279 assert!(task.first_todo_at.is_some());
2280 assert!(task.first_doing_at.is_none());
2281 assert!(task.first_done_at.is_none());
2282 }
2283
2284 #[tokio::test]
2285 async fn test_add_task_with_spec() {
2286 let ctx = TestContext::new().await;
2287 let manager = TaskManager::new(ctx.pool());
2288
2289 let spec = "This is a task specification";
2290 let task = manager
2291 .add_task(
2292 "Test task".to_string(),
2293 Some(spec.to_string()),
2294 None,
2295 None,
2296 None,
2297 None,
2298 )
2299 .await
2300 .unwrap();
2301
2302 assert_eq!(task.name, "Test task");
2303 assert_eq!(task.spec.as_deref(), Some(spec));
2304 }
2305
2306 #[tokio::test]
2307 async fn test_add_task_with_parent() {
2308 let ctx = TestContext::new().await;
2309 let manager = TaskManager::new(ctx.pool());
2310
2311 let parent = manager
2312 .add_task("Parent task".to_string(), None, None, None, None, None)
2313 .await
2314 .unwrap();
2315 let child = manager
2316 .add_task(
2317 "Child task".to_string(),
2318 None,
2319 Some(parent.id),
2320 None,
2321 None,
2322 None,
2323 )
2324 .await
2325 .unwrap();
2326
2327 assert_eq!(child.parent_id, Some(parent.id));
2328 }
2329
2330 #[tokio::test]
2331 async fn test_get_task() {
2332 let ctx = TestContext::new().await;
2333 let manager = TaskManager::new(ctx.pool());
2334
2335 let created = manager
2336 .add_task("Test task".to_string(), None, None, None, None, None)
2337 .await
2338 .unwrap();
2339 let retrieved = manager.get_task(created.id).await.unwrap();
2340
2341 assert_eq!(created.id, retrieved.id);
2342 assert_eq!(created.name, retrieved.name);
2343 }
2344
2345 #[tokio::test]
2346 async fn test_get_task_not_found() {
2347 let ctx = TestContext::new().await;
2348 let manager = TaskManager::new(ctx.pool());
2349
2350 let result = manager.get_task(999).await;
2351 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2352 }
2353
2354 #[tokio::test]
2355 async fn test_update_task_name() {
2356 let ctx = TestContext::new().await;
2357 let manager = TaskManager::new(ctx.pool());
2358
2359 let task = manager
2360 .add_task("Original name".to_string(), None, None, None, None, None)
2361 .await
2362 .unwrap();
2363 let updated = manager
2364 .update_task(
2365 task.id,
2366 TaskUpdate {
2367 name: Some("New name"),
2368 ..Default::default()
2369 },
2370 )
2371 .await
2372 .unwrap();
2373
2374 assert_eq!(updated.name, "New name");
2375 }
2376
2377 #[tokio::test]
2378 async fn test_update_task_status() {
2379 let ctx = TestContext::new().await;
2380 let manager = TaskManager::new(ctx.pool());
2381
2382 let task = manager
2383 .add_task("Test task".to_string(), None, None, None, None, None)
2384 .await
2385 .unwrap();
2386 let updated = manager
2387 .update_task(
2388 task.id,
2389 TaskUpdate {
2390 status: Some("doing"),
2391 ..Default::default()
2392 },
2393 )
2394 .await
2395 .unwrap();
2396
2397 assert_eq!(updated.status, "doing");
2398 assert!(updated.first_doing_at.is_some());
2399 }
2400
2401 #[tokio::test]
2402 async fn test_delete_task() {
2403 let ctx = TestContext::new().await;
2404 let manager = TaskManager::new(ctx.pool());
2405
2406 let task = manager
2407 .add_task("Test task".to_string(), None, None, None, None, None)
2408 .await
2409 .unwrap();
2410 manager.delete_task(task.id).await.unwrap();
2411
2412 let result = manager.get_task(task.id).await;
2413 assert!(result.is_err());
2414 }
2415
2416 #[tokio::test]
2417 async fn test_find_tasks_by_status() {
2418 let ctx = TestContext::new().await;
2419 let manager = TaskManager::new(ctx.pool());
2420
2421 manager
2422 .add_task("Todo task".to_string(), None, None, None, None, None)
2423 .await
2424 .unwrap();
2425 let doing_task = manager
2426 .add_task("Doing task".to_string(), None, None, None, None, None)
2427 .await
2428 .unwrap();
2429 manager
2430 .update_task(
2431 doing_task.id,
2432 TaskUpdate {
2433 status: Some("doing"),
2434 ..Default::default()
2435 },
2436 )
2437 .await
2438 .unwrap();
2439
2440 let todo_result = manager
2441 .find_tasks(Some("todo".to_string()), None, None, None, None)
2442 .await
2443 .unwrap();
2444 let doing_result = manager
2445 .find_tasks(Some("doing".to_string()), None, None, None, None)
2446 .await
2447 .unwrap();
2448
2449 assert_eq!(todo_result.tasks.len(), 1);
2450 assert_eq!(doing_result.tasks.len(), 1);
2451 assert_eq!(doing_result.tasks[0].status, "doing");
2452 }
2453
2454 #[tokio::test]
2455 async fn test_find_tasks_by_parent() {
2456 let ctx = TestContext::new().await;
2457 let manager = TaskManager::new(ctx.pool());
2458
2459 let parent = manager
2460 .add_task("Parent".to_string(), None, None, None, None, None)
2461 .await
2462 .unwrap();
2463 manager
2464 .add_task(
2465 "Child 1".to_string(),
2466 None,
2467 Some(parent.id),
2468 None,
2469 None,
2470 None,
2471 )
2472 .await
2473 .unwrap();
2474 manager
2475 .add_task(
2476 "Child 2".to_string(),
2477 None,
2478 Some(parent.id),
2479 None,
2480 None,
2481 None,
2482 )
2483 .await
2484 .unwrap();
2485
2486 let result = manager
2487 .find_tasks(None, Some(Some(parent.id)), None, None, None)
2488 .await
2489 .unwrap();
2490
2491 assert_eq!(result.tasks.len(), 2);
2492 }
2493
2494 #[tokio::test]
2495 async fn test_start_task() {
2496 let ctx = TestContext::new().await;
2497 let manager = TaskManager::new(ctx.pool());
2498
2499 let task = manager
2500 .add_task("Test task".to_string(), None, None, None, None, None)
2501 .await
2502 .unwrap();
2503 let started = manager.start_task(task.id, false).await.unwrap();
2504
2505 assert_eq!(started.task.status, "doing");
2506 assert!(started.task.first_doing_at.is_some());
2507
2508 let session_id = crate::workspace::resolve_session_id(None);
2510 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2511 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2512 )
2513 .bind(&session_id)
2514 .fetch_optional(ctx.pool())
2515 .await
2516 .unwrap()
2517 .flatten();
2518
2519 assert_eq!(current, Some(task.id));
2520 }
2521
2522 #[tokio::test]
2523 async fn test_start_task_with_events() {
2524 let ctx = TestContext::new().await;
2525 let manager = TaskManager::new(ctx.pool());
2526
2527 let task = manager
2528 .add_task("Test task".to_string(), None, None, None, None, None)
2529 .await
2530 .unwrap();
2531
2532 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
2534 .bind(task.id)
2535 .bind("test")
2536 .bind("test event")
2537 .execute(ctx.pool())
2538 .await
2539 .unwrap();
2540
2541 let started = manager.start_task(task.id, true).await.unwrap();
2542
2543 assert!(started.events_summary.is_some());
2544 let summary = started.events_summary.unwrap();
2545 assert_eq!(summary.total_count, 1);
2546 }
2547
2548 #[tokio::test]
2549 async fn test_done_task() {
2550 let ctx = TestContext::new().await;
2551 let manager = TaskManager::new(ctx.pool());
2552
2553 let task = manager
2554 .add_task("Test task".to_string(), None, None, None, None, None)
2555 .await
2556 .unwrap();
2557 manager.start_task(task.id, false).await.unwrap();
2558 let response = manager.done_task(false).await.unwrap();
2559
2560 assert_eq!(response.completed_task.status, "done");
2561 assert!(response.completed_task.first_done_at.is_some());
2562 assert_eq!(response.workspace_status.current_task_id, None);
2563
2564 match response.next_step_suggestion {
2566 NextStepSuggestion::WorkspaceIsClear { .. } => {},
2567 _ => panic!("Expected WorkspaceIsClear suggestion"),
2568 }
2569
2570 let session_id = crate::workspace::resolve_session_id(None);
2572 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2573 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2574 )
2575 .bind(&session_id)
2576 .fetch_optional(ctx.pool())
2577 .await
2578 .unwrap()
2579 .flatten();
2580
2581 assert!(current.is_none());
2582 }
2583
2584 #[tokio::test]
2585 async fn test_done_task_with_uncompleted_children() {
2586 let ctx = TestContext::new().await;
2587 let manager = TaskManager::new(ctx.pool());
2588
2589 let parent = manager
2590 .add_task("Parent".to_string(), None, None, None, None, None)
2591 .await
2592 .unwrap();
2593 manager
2594 .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
2595 .await
2596 .unwrap();
2597
2598 manager.start_task(parent.id, false).await.unwrap();
2600
2601 let result = manager.done_task(false).await;
2602 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
2603 }
2604
2605 #[tokio::test]
2606 async fn test_done_task_with_completed_children() {
2607 let ctx = TestContext::new().await;
2608 let manager = TaskManager::new(ctx.pool());
2609
2610 let parent = manager
2611 .add_task("Parent".to_string(), None, None, None, None, None)
2612 .await
2613 .unwrap();
2614 let child = manager
2615 .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
2616 .await
2617 .unwrap();
2618
2619 manager.start_task(child.id, false).await.unwrap();
2621 let child_response = manager.done_task(false).await.unwrap();
2622
2623 match child_response.next_step_suggestion {
2625 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
2626 assert_eq!(parent_task_id, parent.id);
2627 },
2628 _ => panic!("Expected ParentIsReady suggestion"),
2629 }
2630
2631 manager.start_task(parent.id, false).await.unwrap();
2633 let parent_response = manager.done_task(false).await.unwrap();
2634 assert_eq!(parent_response.completed_task.status, "done");
2635
2636 match parent_response.next_step_suggestion {
2638 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
2639 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2640 }
2641 }
2642
2643 #[tokio::test]
2644 async fn test_circular_dependency() {
2645 let ctx = TestContext::new().await;
2646 let manager = TaskManager::new(ctx.pool());
2647
2648 let task1 = manager
2649 .add_task("Task 1".to_string(), None, None, None, None, None)
2650 .await
2651 .unwrap();
2652 let task2 = manager
2653 .add_task("Task 2".to_string(), None, Some(task1.id), None, None, None)
2654 .await
2655 .unwrap();
2656
2657 let result = manager
2659 .update_task(
2660 task1.id,
2661 TaskUpdate {
2662 parent_id: Some(Some(task2.id)),
2663 ..Default::default()
2664 },
2665 )
2666 .await;
2667
2668 assert!(matches!(
2669 result,
2670 Err(IntentError::CircularDependency { .. })
2671 ));
2672 }
2673
2674 #[tokio::test]
2675 async fn test_invalid_parent_id() {
2676 let ctx = TestContext::new().await;
2677 let manager = TaskManager::new(ctx.pool());
2678
2679 let result = manager
2680 .add_task("Test".to_string(), None, Some(999), None, None, None)
2681 .await;
2682 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2683 }
2684
2685 #[tokio::test]
2686 async fn test_update_task_complexity_and_priority() {
2687 let ctx = TestContext::new().await;
2688 let manager = TaskManager::new(ctx.pool());
2689
2690 let task = manager
2691 .add_task("Test task".to_string(), None, None, None, None, None)
2692 .await
2693 .unwrap();
2694 let updated = manager
2695 .update_task(
2696 task.id,
2697 TaskUpdate {
2698 complexity: Some(8),
2699 priority: Some(10),
2700 ..Default::default()
2701 },
2702 )
2703 .await
2704 .unwrap();
2705
2706 assert_eq!(updated.complexity, Some(8));
2707 assert_eq!(updated.priority, Some(10));
2708 }
2709
2710 #[tokio::test]
2711 async fn test_spawn_subtask() {
2712 let ctx = TestContext::new().await;
2713 let manager = TaskManager::new(ctx.pool());
2714
2715 let parent = manager
2717 .add_task("Parent task".to_string(), None, None, None, None, None)
2718 .await
2719 .unwrap();
2720 manager.start_task(parent.id, false).await.unwrap();
2721
2722 let response = manager
2724 .spawn_subtask("Child task", Some("Details"))
2725 .await
2726 .unwrap();
2727
2728 assert_eq!(response.subtask.parent_id, parent.id);
2729 assert_eq!(response.subtask.name, "Child task");
2730 assert_eq!(response.subtask.status, "doing");
2731 assert_eq!(response.parent_task.id, parent.id);
2732 assert_eq!(response.parent_task.name, "Parent task");
2733
2734 let session_id = crate::workspace::resolve_session_id(None);
2736 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2737 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2738 )
2739 .bind(&session_id)
2740 .fetch_optional(ctx.pool())
2741 .await
2742 .unwrap()
2743 .flatten();
2744
2745 assert_eq!(current, Some(response.subtask.id));
2746
2747 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
2749 assert_eq!(retrieved.status, "doing");
2750 }
2751
2752 #[tokio::test]
2753 async fn test_spawn_subtask_no_current_task() {
2754 let ctx = TestContext::new().await;
2755 let manager = TaskManager::new(ctx.pool());
2756
2757 let result = manager.spawn_subtask("Child", None).await;
2759 assert!(result.is_err());
2760 }
2761
2762 #[tokio::test]
2763 async fn test_pick_next_tasks_basic() {
2764 let ctx = TestContext::new().await;
2765 let manager = TaskManager::new(ctx.pool());
2766
2767 for i in 1..=10 {
2769 manager
2770 .add_task(format!("Task {}", i), None, None, None, None, None)
2771 .await
2772 .unwrap();
2773 }
2774
2775 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
2777
2778 assert_eq!(picked.len(), 5);
2779 for task in &picked {
2780 assert_eq!(task.status, "doing");
2781 assert!(task.first_doing_at.is_some());
2782 }
2783
2784 let doing_count: i64 =
2786 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2787 .fetch_one(ctx.pool())
2788 .await
2789 .unwrap();
2790
2791 assert_eq!(doing_count, 5);
2792 }
2793
2794 #[tokio::test]
2795 async fn test_pick_next_tasks_with_existing_doing() {
2796 let ctx = TestContext::new().await;
2797 let manager = TaskManager::new(ctx.pool());
2798
2799 for i in 1..=10 {
2801 manager
2802 .add_task(format!("Task {}", i), None, None, None, None, None)
2803 .await
2804 .unwrap();
2805 }
2806
2807 let result = manager
2809 .find_tasks(Some("todo".to_string()), None, None, None, None)
2810 .await
2811 .unwrap();
2812 manager.start_task(result.tasks[0].id, false).await.unwrap();
2813 manager.start_task(result.tasks[1].id, false).await.unwrap();
2814
2815 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2817
2818 assert_eq!(picked.len(), 3);
2820
2821 let doing_count: i64 =
2823 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2824 .fetch_one(ctx.pool())
2825 .await
2826 .unwrap();
2827
2828 assert_eq!(doing_count, 5);
2829 }
2830
2831 #[tokio::test]
2832 async fn test_pick_next_tasks_at_capacity() {
2833 let ctx = TestContext::new().await;
2834 let manager = TaskManager::new(ctx.pool());
2835
2836 for i in 1..=10 {
2838 manager
2839 .add_task(format!("Task {}", i), None, None, None, None, None)
2840 .await
2841 .unwrap();
2842 }
2843
2844 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2846 assert_eq!(first_batch.len(), 5);
2847
2848 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2850 assert_eq!(second_batch.len(), 0);
2851 }
2852
2853 #[tokio::test]
2854 async fn test_pick_next_tasks_priority_ordering() {
2855 let ctx = TestContext::new().await;
2856 let manager = TaskManager::new(ctx.pool());
2857
2858 let low = manager
2860 .add_task("Low priority".to_string(), None, None, None, None, None)
2861 .await
2862 .unwrap();
2863 manager
2864 .update_task(
2865 low.id,
2866 TaskUpdate {
2867 priority: Some(1),
2868 ..Default::default()
2869 },
2870 )
2871 .await
2872 .unwrap();
2873
2874 let high = manager
2875 .add_task("High priority".to_string(), None, None, None, None, None)
2876 .await
2877 .unwrap();
2878 manager
2879 .update_task(
2880 high.id,
2881 TaskUpdate {
2882 priority: Some(10),
2883 ..Default::default()
2884 },
2885 )
2886 .await
2887 .unwrap();
2888
2889 let medium = manager
2890 .add_task("Medium priority".to_string(), None, None, None, None, None)
2891 .await
2892 .unwrap();
2893 manager
2894 .update_task(
2895 medium.id,
2896 TaskUpdate {
2897 priority: Some(5),
2898 ..Default::default()
2899 },
2900 )
2901 .await
2902 .unwrap();
2903
2904 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2906
2907 assert_eq!(picked.len(), 3);
2909 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
2913
2914 #[tokio::test]
2915 async fn test_pick_next_tasks_complexity_ordering() {
2916 let ctx = TestContext::new().await;
2917 let manager = TaskManager::new(ctx.pool());
2918
2919 let complex = manager
2921 .add_task("Complex".to_string(), None, None, None, None, None)
2922 .await
2923 .unwrap();
2924 manager
2925 .update_task(
2926 complex.id,
2927 TaskUpdate {
2928 complexity: Some(9),
2929 priority: Some(5),
2930 ..Default::default()
2931 },
2932 )
2933 .await
2934 .unwrap();
2935
2936 let simple = manager
2937 .add_task("Simple".to_string(), None, None, None, None, None)
2938 .await
2939 .unwrap();
2940 manager
2941 .update_task(
2942 simple.id,
2943 TaskUpdate {
2944 complexity: Some(1),
2945 priority: Some(5),
2946 ..Default::default()
2947 },
2948 )
2949 .await
2950 .unwrap();
2951
2952 let medium = manager
2953 .add_task("Medium".to_string(), None, None, None, None, None)
2954 .await
2955 .unwrap();
2956 manager
2957 .update_task(
2958 medium.id,
2959 TaskUpdate {
2960 complexity: Some(5),
2961 priority: Some(5),
2962 ..Default::default()
2963 },
2964 )
2965 .await
2966 .unwrap();
2967
2968 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2970
2971 assert_eq!(picked.len(), 3);
2973 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
2977
2978 #[tokio::test]
2979 async fn test_done_task_sibling_tasks_remain() {
2980 let ctx = TestContext::new().await;
2981 let manager = TaskManager::new(ctx.pool());
2982
2983 let parent = manager
2985 .add_task("Parent Task".to_string(), None, None, None, None, None)
2986 .await
2987 .unwrap();
2988 let child1 = manager
2989 .add_task(
2990 "Child 1".to_string(),
2991 None,
2992 Some(parent.id),
2993 None,
2994 None,
2995 None,
2996 )
2997 .await
2998 .unwrap();
2999 let child2 = manager
3000 .add_task(
3001 "Child 2".to_string(),
3002 None,
3003 Some(parent.id),
3004 None,
3005 None,
3006 None,
3007 )
3008 .await
3009 .unwrap();
3010 let _child3 = manager
3011 .add_task(
3012 "Child 3".to_string(),
3013 None,
3014 Some(parent.id),
3015 None,
3016 None,
3017 None,
3018 )
3019 .await
3020 .unwrap();
3021
3022 manager.start_task(child1.id, false).await.unwrap();
3024 let response = manager.done_task(false).await.unwrap();
3025
3026 match response.next_step_suggestion {
3028 NextStepSuggestion::SiblingTasksRemain {
3029 parent_task_id,
3030 remaining_siblings_count,
3031 ..
3032 } => {
3033 assert_eq!(parent_task_id, parent.id);
3034 assert_eq!(remaining_siblings_count, 2); },
3036 _ => panic!("Expected SiblingTasksRemain suggestion"),
3037 }
3038
3039 manager.start_task(child2.id, false).await.unwrap();
3041 let response2 = manager.done_task(false).await.unwrap();
3042
3043 match response2.next_step_suggestion {
3045 NextStepSuggestion::SiblingTasksRemain {
3046 remaining_siblings_count,
3047 ..
3048 } => {
3049 assert_eq!(remaining_siblings_count, 1); },
3051 _ => panic!("Expected SiblingTasksRemain suggestion"),
3052 }
3053 }
3054
3055 #[tokio::test]
3056 async fn test_done_task_top_level_with_children() {
3057 let ctx = TestContext::new().await;
3058 let manager = TaskManager::new(ctx.pool());
3059
3060 let parent = manager
3062 .add_task("Epic Task".to_string(), None, None, None, None, None)
3063 .await
3064 .unwrap();
3065 let child = manager
3066 .add_task(
3067 "Sub Task".to_string(),
3068 None,
3069 Some(parent.id),
3070 None,
3071 None,
3072 None,
3073 )
3074 .await
3075 .unwrap();
3076
3077 manager.start_task(child.id, false).await.unwrap();
3079 manager.done_task(false).await.unwrap();
3080
3081 manager.start_task(parent.id, false).await.unwrap();
3083 let response = manager.done_task(false).await.unwrap();
3084
3085 match response.next_step_suggestion {
3087 NextStepSuggestion::TopLevelTaskCompleted {
3088 completed_task_id,
3089 completed_task_name,
3090 ..
3091 } => {
3092 assert_eq!(completed_task_id, parent.id);
3093 assert_eq!(completed_task_name, "Epic Task");
3094 },
3095 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
3096 }
3097 }
3098
3099 #[tokio::test]
3100 async fn test_done_task_no_parent_context() {
3101 let ctx = TestContext::new().await;
3102 let manager = TaskManager::new(ctx.pool());
3103
3104 let task1 = manager
3106 .add_task(
3107 "Standalone Task 1".to_string(),
3108 None,
3109 None,
3110 None,
3111 None,
3112 None,
3113 )
3114 .await
3115 .unwrap();
3116 let _task2 = manager
3117 .add_task(
3118 "Standalone Task 2".to_string(),
3119 None,
3120 None,
3121 None,
3122 None,
3123 None,
3124 )
3125 .await
3126 .unwrap();
3127
3128 manager.start_task(task1.id, false).await.unwrap();
3130 let response = manager.done_task(false).await.unwrap();
3131
3132 match response.next_step_suggestion {
3134 NextStepSuggestion::NoParentContext {
3135 completed_task_id,
3136 completed_task_name,
3137 ..
3138 } => {
3139 assert_eq!(completed_task_id, task1.id);
3140 assert_eq!(completed_task_name, "Standalone Task 1");
3141 },
3142 _ => panic!("Expected NoParentContext suggestion"),
3143 }
3144 }
3145
3146 #[tokio::test]
3151 async fn test_done_task_by_id_non_focused_task_preserves_focus() {
3152 let ctx = TestContext::new().await;
3153 let manager = TaskManager::new(ctx.pool());
3154
3155 let task_a = manager
3157 .add_task("Task A".to_string(), None, None, None, None, None)
3158 .await
3159 .unwrap();
3160 let task_b = manager
3161 .add_task("Task B".to_string(), None, None, None, None, None)
3162 .await
3163 .unwrap();
3164 manager.start_task(task_a.id, false).await.unwrap();
3165
3166 let response = manager.done_task_by_id(task_b.id, false).await.unwrap();
3168
3169 assert_eq!(response.completed_task.status, "done");
3171 assert_eq!(response.completed_task.id, task_b.id);
3172
3173 assert_eq!(response.workspace_status.current_task_id, Some(task_a.id));
3175
3176 let session_id = crate::workspace::resolve_session_id(None);
3178 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
3179 "SELECT current_task_id FROM sessions WHERE session_id = ?",
3180 )
3181 .bind(&session_id)
3182 .fetch_optional(ctx.pool())
3183 .await
3184 .unwrap()
3185 .flatten();
3186 assert_eq!(current, Some(task_a.id));
3187 }
3188
3189 #[tokio::test]
3190 async fn test_done_task_by_id_focused_task_clears_focus() {
3191 let ctx = TestContext::new().await;
3192 let manager = TaskManager::new(ctx.pool());
3193
3194 let task = manager
3195 .add_task("Focused task".to_string(), None, None, None, None, None)
3196 .await
3197 .unwrap();
3198 manager.start_task(task.id, false).await.unwrap();
3199
3200 let response = manager.done_task_by_id(task.id, false).await.unwrap();
3202
3203 assert_eq!(response.completed_task.status, "done");
3204 assert_eq!(response.workspace_status.current_task_id, None);
3205
3206 let session_id = crate::workspace::resolve_session_id(None);
3208 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
3209 "SELECT current_task_id FROM sessions WHERE session_id = ?",
3210 )
3211 .bind(&session_id)
3212 .fetch_optional(ctx.pool())
3213 .await
3214 .unwrap()
3215 .flatten();
3216 assert!(current.is_none());
3217 }
3218
3219 #[tokio::test]
3220 async fn test_done_task_by_id_human_task_rejected_for_ai_caller() {
3221 let ctx = TestContext::new().await;
3222 let manager = TaskManager::new(ctx.pool());
3223
3224 let task = manager
3226 .add_task(
3227 "Human task".to_string(),
3228 None,
3229 None,
3230 Some("human".to_string()),
3231 None,
3232 None,
3233 )
3234 .await
3235 .unwrap();
3236 manager
3237 .update_task(
3238 task.id,
3239 TaskUpdate {
3240 status: Some("doing"),
3241 ..Default::default()
3242 },
3243 )
3244 .await
3245 .unwrap();
3246
3247 let result = manager.done_task_by_id(task.id, true).await;
3249 assert!(matches!(
3250 result,
3251 Err(IntentError::HumanTaskCannotBeCompletedByAI { .. })
3252 ));
3253
3254 let response = manager.done_task_by_id(task.id, false).await.unwrap();
3256 assert_eq!(response.completed_task.status, "done");
3257 }
3258
3259 #[tokio::test]
3260 async fn test_done_task_by_id_with_uncompleted_children() {
3261 let ctx = TestContext::new().await;
3262 let manager = TaskManager::new(ctx.pool());
3263
3264 let parent = manager
3265 .add_task("Parent".to_string(), None, None, None, None, None)
3266 .await
3267 .unwrap();
3268 manager
3269 .add_task(
3270 "Incomplete child".to_string(),
3271 None,
3272 Some(parent.id),
3273 None,
3274 None,
3275 None,
3276 )
3277 .await
3278 .unwrap();
3279
3280 let result = manager.done_task_by_id(parent.id, false).await;
3281 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
3282 }
3283
3284 #[tokio::test]
3285 async fn test_done_task_by_id_nonexistent_task() {
3286 let ctx = TestContext::new().await;
3287 let manager = TaskManager::new(ctx.pool());
3288
3289 let result = manager.done_task_by_id(99999, false).await;
3290 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
3291 }
3292
3293 #[tokio::test]
3294 async fn test_done_task_synthesis_graceful_when_llm_unconfigured() {
3295 let ctx = TestContext::new().await;
3297 let manager = TaskManager::new(ctx.pool());
3298 let event_mgr = EventManager::new(ctx.pool());
3299
3300 let task = manager
3302 .add_task(
3303 "Test Task".to_string(),
3304 Some("Original spec".to_string()),
3305 None,
3306 Some("ai".to_string()),
3307 None,
3308 None,
3309 )
3310 .await
3311 .unwrap();
3312
3313 event_mgr
3315 .add_event(task.id, "decision".to_string(), "Test decision".to_string())
3316 .await
3317 .unwrap();
3318
3319 manager.start_task(task.id, false).await.unwrap();
3320
3321 let result = manager.done_task_by_id(task.id, false).await;
3323 assert!(result.is_ok(), "Task completion should succeed without LLM");
3324
3325 let completed_task = manager.get_task(task.id).await.unwrap();
3327 assert_eq!(completed_task.status, "done");
3328
3329 assert_eq!(completed_task.spec, Some("Original spec".to_string()));
3331 }
3332
3333 #[tokio::test]
3334 async fn test_done_task_synthesis_respects_owner_field() {
3335 let ctx = TestContext::new().await;
3337 let manager = TaskManager::new(ctx.pool());
3338
3339 let ai_task = manager
3341 .add_task(
3342 "AI Task".to_string(),
3343 Some("AI spec".to_string()),
3344 None,
3345 Some("ai".to_string()),
3346 None,
3347 None,
3348 )
3349 .await
3350 .unwrap();
3351 assert_eq!(ai_task.owner, "ai");
3352
3353 let human_task = manager
3355 .add_task(
3356 "Human Task".to_string(),
3357 Some("Human spec".to_string()),
3358 None,
3359 Some("human".to_string()),
3360 None,
3361 None,
3362 )
3363 .await
3364 .unwrap();
3365 assert_eq!(human_task.owner, "human");
3366
3367 manager.start_task(ai_task.id, false).await.unwrap();
3369 let result = manager.done_task_by_id(ai_task.id, false).await;
3370 assert!(result.is_ok());
3371
3372 manager.start_task(human_task.id, false).await.unwrap();
3373 let result = manager.done_task_by_id(human_task.id, false).await;
3374 assert!(result.is_ok());
3375 }
3376
3377 #[tokio::test]
3378 async fn test_try_synthesize_task_description_basic() {
3379 let ctx = TestContext::new().await;
3380 let manager = TaskManager::new(ctx.pool());
3381
3382 let task = manager
3383 .add_task(
3384 "Synthesis Test".to_string(),
3385 Some("Original".to_string()),
3386 None,
3387 None,
3388 None,
3389 None,
3390 )
3391 .await
3392 .unwrap();
3393
3394 let result = manager
3396 .try_synthesize_task_description(task.id, &task.name)
3397 .await;
3398
3399 assert!(result.is_ok(), "Should not error when LLM unconfigured");
3400 assert_eq!(
3401 result.unwrap(),
3402 None,
3403 "Should return None when LLM unconfigured"
3404 );
3405 }
3406
3407 #[tokio::test]
3408 async fn test_pick_next_focused_subtask() {
3409 let ctx = TestContext::new().await;
3410 let manager = TaskManager::new(ctx.pool());
3411
3412 let parent = manager
3414 .add_task("Parent task".to_string(), None, None, None, None, None)
3415 .await
3416 .unwrap();
3417 manager.start_task(parent.id, false).await.unwrap();
3418
3419 let subtask1 = manager
3421 .add_task(
3422 "Subtask 1".to_string(),
3423 None,
3424 Some(parent.id),
3425 None,
3426 None,
3427 None,
3428 )
3429 .await
3430 .unwrap();
3431 let subtask2 = manager
3432 .add_task(
3433 "Subtask 2".to_string(),
3434 None,
3435 Some(parent.id),
3436 None,
3437 None,
3438 None,
3439 )
3440 .await
3441 .unwrap();
3442
3443 manager
3445 .update_task(
3446 subtask1.id,
3447 TaskUpdate {
3448 priority: Some(2),
3449 ..Default::default()
3450 },
3451 )
3452 .await
3453 .unwrap();
3454 manager
3455 .update_task(
3456 subtask2.id,
3457 TaskUpdate {
3458 priority: Some(1),
3459 ..Default::default()
3460 },
3461 )
3462 .await
3463 .unwrap();
3464
3465 let response = manager.pick_next().await.unwrap();
3467
3468 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
3469 assert!(response.task.is_some());
3470 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
3471 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
3472 }
3473
3474 #[tokio::test]
3475 async fn test_pick_next_top_level_task() {
3476 let ctx = TestContext::new().await;
3477 let manager = TaskManager::new(ctx.pool());
3478
3479 let task1 = manager
3481 .add_task("Task 1".to_string(), None, None, None, None, None)
3482 .await
3483 .unwrap();
3484 let task2 = manager
3485 .add_task("Task 2".to_string(), None, None, None, None, None)
3486 .await
3487 .unwrap();
3488
3489 manager
3491 .update_task(
3492 task1.id,
3493 TaskUpdate {
3494 priority: Some(5),
3495 ..Default::default()
3496 },
3497 )
3498 .await
3499 .unwrap();
3500 manager
3501 .update_task(
3502 task2.id,
3503 TaskUpdate {
3504 priority: Some(3),
3505 ..Default::default()
3506 },
3507 )
3508 .await
3509 .unwrap();
3510
3511 let response = manager.pick_next().await.unwrap();
3513
3514 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3515 assert!(response.task.is_some());
3516 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
3517 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
3518 }
3519
3520 #[tokio::test]
3521 async fn test_pick_next_no_tasks() {
3522 let ctx = TestContext::new().await;
3523 let manager = TaskManager::new(ctx.pool());
3524
3525 let response = manager.pick_next().await.unwrap();
3527
3528 assert_eq!(response.suggestion_type, "NONE");
3529 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
3530 assert!(response.message.is_some());
3531 }
3532
3533 #[tokio::test]
3534 async fn test_pick_next_all_completed() {
3535 let ctx = TestContext::new().await;
3536 let manager = TaskManager::new(ctx.pool());
3537
3538 let task = manager
3540 .add_task("Task 1".to_string(), None, None, None, None, None)
3541 .await
3542 .unwrap();
3543 manager.start_task(task.id, false).await.unwrap();
3544 manager.done_task(false).await.unwrap();
3545
3546 let response = manager.pick_next().await.unwrap();
3548
3549 assert_eq!(response.suggestion_type, "NONE");
3550 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
3551 assert!(response.message.is_some());
3552 }
3553
3554 #[tokio::test]
3555 async fn test_pick_next_no_available_todos() {
3556 let ctx = TestContext::new().await;
3557 let manager = TaskManager::new(ctx.pool());
3558
3559 let parent = manager
3561 .add_task("Parent task".to_string(), None, None, None, None, None)
3562 .await
3563 .unwrap();
3564 manager.start_task(parent.id, false).await.unwrap();
3565
3566 let subtask = manager
3568 .add_task(
3569 "Subtask".to_string(),
3570 None,
3571 Some(parent.id),
3572 None,
3573 None,
3574 None,
3575 )
3576 .await
3577 .unwrap();
3578 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
3580 .bind(subtask.id)
3581 .execute(ctx.pool())
3582 .await
3583 .unwrap();
3584
3585 let session_id = crate::workspace::resolve_session_id(None);
3587 sqlx::query(
3588 r#"
3589 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
3590 VALUES (?, ?, datetime('now'), datetime('now'))
3591 ON CONFLICT(session_id) DO UPDATE SET
3592 current_task_id = excluded.current_task_id,
3593 last_active_at = datetime('now')
3594 "#,
3595 )
3596 .bind(&session_id)
3597 .bind(subtask.id)
3598 .execute(ctx.pool())
3599 .await
3600 .unwrap();
3601
3602 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
3604 .bind(parent.id)
3605 .execute(ctx.pool())
3606 .await
3607 .unwrap();
3608
3609 let response = manager.pick_next().await.unwrap();
3612
3613 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3614 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
3615 assert_eq!(response.task.as_ref().unwrap().status, "doing");
3616 }
3617
3618 #[tokio::test]
3619 async fn test_pick_next_priority_ordering() {
3620 let ctx = TestContext::new().await;
3621 let manager = TaskManager::new(ctx.pool());
3622
3623 let parent = manager
3625 .add_task("Parent".to_string(), None, None, None, None, None)
3626 .await
3627 .unwrap();
3628 manager.start_task(parent.id, false).await.unwrap();
3629
3630 let sub1 = manager
3632 .add_task(
3633 "Priority 10".to_string(),
3634 None,
3635 Some(parent.id),
3636 None,
3637 None,
3638 None,
3639 )
3640 .await
3641 .unwrap();
3642 manager
3643 .update_task(
3644 sub1.id,
3645 TaskUpdate {
3646 priority: Some(10),
3647 ..Default::default()
3648 },
3649 )
3650 .await
3651 .unwrap();
3652
3653 let sub2 = manager
3654 .add_task(
3655 "Priority 1".to_string(),
3656 None,
3657 Some(parent.id),
3658 None,
3659 None,
3660 None,
3661 )
3662 .await
3663 .unwrap();
3664 manager
3665 .update_task(
3666 sub2.id,
3667 TaskUpdate {
3668 priority: Some(1),
3669 ..Default::default()
3670 },
3671 )
3672 .await
3673 .unwrap();
3674
3675 let sub3 = manager
3676 .add_task(
3677 "Priority 5".to_string(),
3678 None,
3679 Some(parent.id),
3680 None,
3681 None,
3682 None,
3683 )
3684 .await
3685 .unwrap();
3686 manager
3687 .update_task(
3688 sub3.id,
3689 TaskUpdate {
3690 priority: Some(5),
3691 ..Default::default()
3692 },
3693 )
3694 .await
3695 .unwrap();
3696
3697 let response = manager.pick_next().await.unwrap();
3699
3700 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
3701 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
3702 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
3703 }
3704
3705 #[tokio::test]
3706 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
3707 let ctx = TestContext::new().await;
3708 let manager = TaskManager::new(ctx.pool());
3709
3710 let parent = manager
3712 .add_task("Parent".to_string(), None, None, None, None, None)
3713 .await
3714 .unwrap();
3715 manager.start_task(parent.id, false).await.unwrap();
3716
3717 let top_level = manager
3719 .add_task("Top level task".to_string(), None, None, None, None, None)
3720 .await
3721 .unwrap();
3722
3723 let response = manager.pick_next().await.unwrap();
3725
3726 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3727 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
3728 }
3729
3730 #[tokio::test]
3733 async fn test_get_task_with_events() {
3734 let ctx = TestContext::new().await;
3735 let task_mgr = TaskManager::new(ctx.pool());
3736 let event_mgr = EventManager::new(ctx.pool());
3737
3738 let task = task_mgr
3739 .add_task("Test".to_string(), None, None, None, None, None)
3740 .await
3741 .unwrap();
3742
3743 event_mgr
3745 .add_event(task.id, "progress".to_string(), "Event 1".to_string())
3746 .await
3747 .unwrap();
3748 event_mgr
3749 .add_event(task.id, "decision".to_string(), "Event 2".to_string())
3750 .await
3751 .unwrap();
3752
3753 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3754
3755 assert_eq!(result.task.id, task.id);
3756 assert!(result.events_summary.is_some());
3757
3758 let summary = result.events_summary.unwrap();
3759 assert_eq!(summary.total_count, 2);
3760 assert_eq!(summary.recent_events.len(), 2);
3761 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
3763 }
3764
3765 #[tokio::test]
3766 async fn test_get_task_with_events_nonexistent() {
3767 let ctx = TestContext::new().await;
3768 let task_mgr = TaskManager::new(ctx.pool());
3769
3770 let result = task_mgr.get_task_with_events(999).await;
3771 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
3772 }
3773
3774 #[tokio::test]
3775 async fn test_get_task_with_many_events() {
3776 let ctx = TestContext::new().await;
3777 let task_mgr = TaskManager::new(ctx.pool());
3778 let event_mgr = EventManager::new(ctx.pool());
3779
3780 let task = task_mgr
3781 .add_task("Test".to_string(), None, None, None, None, None)
3782 .await
3783 .unwrap();
3784
3785 for i in 0..20 {
3787 event_mgr
3788 .add_event(task.id, "test".to_string(), format!("Event {}", i))
3789 .await
3790 .unwrap();
3791 }
3792
3793 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3794 let summary = result.events_summary.unwrap();
3795
3796 assert_eq!(summary.total_count, 20);
3797 assert_eq!(summary.recent_events.len(), 10); }
3799
3800 #[tokio::test]
3801 async fn test_get_task_with_no_events() {
3802 let ctx = TestContext::new().await;
3803 let task_mgr = TaskManager::new(ctx.pool());
3804
3805 let task = task_mgr
3806 .add_task("Test".to_string(), None, None, None, None, None)
3807 .await
3808 .unwrap();
3809
3810 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3811 let summary = result.events_summary.unwrap();
3812
3813 assert_eq!(summary.total_count, 0);
3814 assert_eq!(summary.recent_events.len(), 0);
3815 }
3816
3817 #[tokio::test]
3818 async fn test_pick_next_tasks_zero_capacity() {
3819 let ctx = TestContext::new().await;
3820 let task_mgr = TaskManager::new(ctx.pool());
3821
3822 task_mgr
3823 .add_task("Task 1".to_string(), None, None, None, None, None)
3824 .await
3825 .unwrap();
3826
3827 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
3829 assert_eq!(results.len(), 0);
3830 }
3831
3832 #[tokio::test]
3833 async fn test_pick_next_tasks_capacity_exceeds_available() {
3834 let ctx = TestContext::new().await;
3835 let task_mgr = TaskManager::new(ctx.pool());
3836
3837 task_mgr
3838 .add_task("Task 1".to_string(), None, None, None, None, None)
3839 .await
3840 .unwrap();
3841 task_mgr
3842 .add_task("Task 2".to_string(), None, None, None, None, None)
3843 .await
3844 .unwrap();
3845
3846 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
3848 assert_eq!(results.len(), 2); }
3850
3851 #[tokio::test]
3854 async fn test_get_task_context_root_task_no_relations() {
3855 let ctx = TestContext::new().await;
3856 let task_mgr = TaskManager::new(ctx.pool());
3857
3858 let task = task_mgr
3860 .add_task("Root task".to_string(), None, None, None, None, None)
3861 .await
3862 .unwrap();
3863
3864 let context = task_mgr.get_task_context(task.id).await.unwrap();
3865
3866 assert_eq!(context.task.id, task.id);
3868 assert_eq!(context.task.name, "Root task");
3869
3870 assert_eq!(context.ancestors.len(), 0);
3872
3873 assert_eq!(context.siblings.len(), 0);
3875
3876 assert_eq!(context.children.len(), 0);
3878 }
3879
3880 #[tokio::test]
3881 async fn test_get_task_context_with_siblings() {
3882 let ctx = TestContext::new().await;
3883 let task_mgr = TaskManager::new(ctx.pool());
3884
3885 let task1 = task_mgr
3887 .add_task("Task 1".to_string(), None, None, None, None, None)
3888 .await
3889 .unwrap();
3890 let task2 = task_mgr
3891 .add_task("Task 2".to_string(), None, None, None, None, None)
3892 .await
3893 .unwrap();
3894 let task3 = task_mgr
3895 .add_task("Task 3".to_string(), None, None, None, None, None)
3896 .await
3897 .unwrap();
3898
3899 let context = task_mgr.get_task_context(task2.id).await.unwrap();
3900
3901 assert_eq!(context.task.id, task2.id);
3903
3904 assert_eq!(context.ancestors.len(), 0);
3906
3907 assert_eq!(context.siblings.len(), 2);
3909 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
3910 assert!(sibling_ids.contains(&task1.id));
3911 assert!(sibling_ids.contains(&task3.id));
3912 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
3916 }
3917
3918 #[tokio::test]
3919 async fn test_get_task_context_with_parent() {
3920 let ctx = TestContext::new().await;
3921 let task_mgr = TaskManager::new(ctx.pool());
3922
3923 let parent = task_mgr
3925 .add_task("Parent task".to_string(), None, None, None, None, None)
3926 .await
3927 .unwrap();
3928 let child = task_mgr
3929 .add_task(
3930 "Child task".to_string(),
3931 None,
3932 Some(parent.id),
3933 None,
3934 None,
3935 None,
3936 )
3937 .await
3938 .unwrap();
3939
3940 let context = task_mgr.get_task_context(child.id).await.unwrap();
3941
3942 assert_eq!(context.task.id, child.id);
3944 assert_eq!(context.task.parent_id, Some(parent.id));
3945
3946 assert_eq!(context.ancestors.len(), 1);
3948 assert_eq!(context.ancestors[0].id, parent.id);
3949 assert_eq!(context.ancestors[0].name, "Parent task");
3950
3951 assert_eq!(context.siblings.len(), 0);
3953
3954 assert_eq!(context.children.len(), 0);
3956 }
3957
3958 #[tokio::test]
3959 async fn test_get_task_context_with_children() {
3960 let ctx = TestContext::new().await;
3961 let task_mgr = TaskManager::new(ctx.pool());
3962
3963 let parent = task_mgr
3965 .add_task("Parent task".to_string(), None, None, None, None, None)
3966 .await
3967 .unwrap();
3968 let child1 = task_mgr
3969 .add_task(
3970 "Child 1".to_string(),
3971 None,
3972 Some(parent.id),
3973 None,
3974 None,
3975 None,
3976 )
3977 .await
3978 .unwrap();
3979 let child2 = task_mgr
3980 .add_task(
3981 "Child 2".to_string(),
3982 None,
3983 Some(parent.id),
3984 None,
3985 None,
3986 None,
3987 )
3988 .await
3989 .unwrap();
3990 let child3 = task_mgr
3991 .add_task(
3992 "Child 3".to_string(),
3993 None,
3994 Some(parent.id),
3995 None,
3996 None,
3997 None,
3998 )
3999 .await
4000 .unwrap();
4001
4002 let context = task_mgr.get_task_context(parent.id).await.unwrap();
4003
4004 assert_eq!(context.task.id, parent.id);
4006
4007 assert_eq!(context.ancestors.len(), 0);
4009
4010 assert_eq!(context.siblings.len(), 0);
4012
4013 assert_eq!(context.children.len(), 3);
4015 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
4016 assert!(child_ids.contains(&child1.id));
4017 assert!(child_ids.contains(&child2.id));
4018 assert!(child_ids.contains(&child3.id));
4019 }
4020
4021 #[tokio::test]
4022 async fn test_get_task_context_multi_level_hierarchy() {
4023 let ctx = TestContext::new().await;
4024 let task_mgr = TaskManager::new(ctx.pool());
4025
4026 let grandparent = task_mgr
4028 .add_task("Grandparent".to_string(), None, None, None, None, None)
4029 .await
4030 .unwrap();
4031 let parent = task_mgr
4032 .add_task(
4033 "Parent".to_string(),
4034 None,
4035 Some(grandparent.id),
4036 None,
4037 None,
4038 None,
4039 )
4040 .await
4041 .unwrap();
4042 let child = task_mgr
4043 .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
4044 .await
4045 .unwrap();
4046
4047 let context = task_mgr.get_task_context(child.id).await.unwrap();
4048
4049 assert_eq!(context.task.id, child.id);
4051
4052 assert_eq!(context.ancestors.len(), 2);
4054 assert_eq!(context.ancestors[0].id, parent.id);
4055 assert_eq!(context.ancestors[0].name, "Parent");
4056 assert_eq!(context.ancestors[1].id, grandparent.id);
4057 assert_eq!(context.ancestors[1].name, "Grandparent");
4058
4059 assert_eq!(context.siblings.len(), 0);
4061
4062 assert_eq!(context.children.len(), 0);
4064 }
4065
4066 #[tokio::test]
4067 async fn test_get_task_context_complex_family_tree() {
4068 let ctx = TestContext::new().await;
4069 let task_mgr = TaskManager::new(ctx.pool());
4070
4071 let root = task_mgr
4079 .add_task("Root".to_string(), None, None, None, None, None)
4080 .await
4081 .unwrap();
4082 let child1 = task_mgr
4083 .add_task("Child1".to_string(), None, Some(root.id), None, None, None)
4084 .await
4085 .unwrap();
4086 let child2 = task_mgr
4087 .add_task("Child2".to_string(), None, Some(root.id), None, None, None)
4088 .await
4089 .unwrap();
4090 let grandchild1 = task_mgr
4091 .add_task(
4092 "Grandchild1".to_string(),
4093 None,
4094 Some(child1.id),
4095 None,
4096 None,
4097 None,
4098 )
4099 .await
4100 .unwrap();
4101 let grandchild2 = task_mgr
4102 .add_task(
4103 "Grandchild2".to_string(),
4104 None,
4105 Some(child1.id),
4106 None,
4107 None,
4108 None,
4109 )
4110 .await
4111 .unwrap();
4112
4113 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
4115
4116 assert_eq!(context.task.id, grandchild2.id);
4118
4119 assert_eq!(context.ancestors.len(), 2);
4121 assert_eq!(context.ancestors[0].id, child1.id);
4122 assert_eq!(context.ancestors[1].id, root.id);
4123
4124 assert_eq!(context.siblings.len(), 1);
4126 assert_eq!(context.siblings[0].id, grandchild1.id);
4127
4128 assert_eq!(context.children.len(), 0);
4130
4131 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
4133 assert_eq!(context_child1.ancestors.len(), 1);
4134 assert_eq!(context_child1.ancestors[0].id, root.id);
4135 assert_eq!(context_child1.siblings.len(), 1);
4136 assert_eq!(context_child1.siblings[0].id, child2.id);
4137 assert_eq!(context_child1.children.len(), 2);
4138 }
4139
4140 #[tokio::test]
4141 async fn test_get_task_context_respects_priority_ordering() {
4142 let ctx = TestContext::new().await;
4143 let task_mgr = TaskManager::new(ctx.pool());
4144
4145 let parent = task_mgr
4147 .add_task("Parent".to_string(), None, None, None, None, None)
4148 .await
4149 .unwrap();
4150
4151 let child_low = task_mgr
4153 .add_task(
4154 "Low priority".to_string(),
4155 None,
4156 Some(parent.id),
4157 None,
4158 None,
4159 None,
4160 )
4161 .await
4162 .unwrap();
4163 let _ = task_mgr
4164 .update_task(
4165 child_low.id,
4166 TaskUpdate {
4167 priority: Some(10),
4168 ..Default::default()
4169 },
4170 )
4171 .await
4172 .unwrap();
4173
4174 let child_high = task_mgr
4175 .add_task(
4176 "High priority".to_string(),
4177 None,
4178 Some(parent.id),
4179 None,
4180 None,
4181 None,
4182 )
4183 .await
4184 .unwrap();
4185 let _ = task_mgr
4186 .update_task(
4187 child_high.id,
4188 TaskUpdate {
4189 priority: Some(1),
4190 ..Default::default()
4191 },
4192 )
4193 .await
4194 .unwrap();
4195
4196 let child_medium = task_mgr
4197 .add_task(
4198 "Medium priority".to_string(),
4199 None,
4200 Some(parent.id),
4201 None,
4202 None,
4203 None,
4204 )
4205 .await
4206 .unwrap();
4207 let _ = task_mgr
4208 .update_task(
4209 child_medium.id,
4210 TaskUpdate {
4211 priority: Some(5),
4212 ..Default::default()
4213 },
4214 )
4215 .await
4216 .unwrap();
4217
4218 let context = task_mgr.get_task_context(parent.id).await.unwrap();
4219
4220 assert_eq!(context.children.len(), 3);
4222 assert_eq!(context.children[0].priority, Some(1));
4223 assert_eq!(context.children[1].priority, Some(5));
4224 assert_eq!(context.children[2].priority, Some(10));
4225 }
4226
4227 #[tokio::test]
4228 async fn test_get_task_context_nonexistent_task() {
4229 let ctx = TestContext::new().await;
4230 let task_mgr = TaskManager::new(ctx.pool());
4231
4232 let result = task_mgr.get_task_context(99999).await;
4233 assert!(result.is_err());
4234 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
4235 }
4236
4237 #[tokio::test]
4238 async fn test_get_task_context_handles_null_priority() {
4239 let ctx = TestContext::new().await;
4240 let task_mgr = TaskManager::new(ctx.pool());
4241
4242 let task1 = task_mgr
4244 .add_task("Task 1".to_string(), None, None, None, None, None)
4245 .await
4246 .unwrap();
4247 let _ = task_mgr
4248 .update_task(
4249 task1.id,
4250 TaskUpdate {
4251 priority: Some(1),
4252 ..Default::default()
4253 },
4254 )
4255 .await
4256 .unwrap();
4257
4258 let task2 = task_mgr
4259 .add_task("Task 2".to_string(), None, None, None, None, None)
4260 .await
4261 .unwrap();
4262 let task3 = task_mgr
4265 .add_task("Task 3".to_string(), None, None, None, None, None)
4266 .await
4267 .unwrap();
4268 let _ = task_mgr
4269 .update_task(
4270 task3.id,
4271 TaskUpdate {
4272 priority: Some(5),
4273 ..Default::default()
4274 },
4275 )
4276 .await
4277 .unwrap();
4278
4279 let context = task_mgr.get_task_context(task2.id).await.unwrap();
4280
4281 assert_eq!(context.siblings.len(), 2);
4283 assert_eq!(context.siblings[0].id, task1.id);
4285 assert_eq!(context.siblings[0].priority, Some(1));
4286 assert_eq!(context.siblings[1].id, task3.id);
4288 assert_eq!(context.siblings[1].priority, Some(5));
4289 }
4290
4291 #[tokio::test]
4292 async fn test_pick_next_tasks_priority_order() {
4293 let ctx = TestContext::new().await;
4294 let task_mgr = TaskManager::new(ctx.pool());
4295
4296 let critical = task_mgr
4298 .add_task("Critical Task".to_string(), None, None, None, None, None)
4299 .await
4300 .unwrap();
4301 task_mgr
4302 .update_task(
4303 critical.id,
4304 TaskUpdate {
4305 priority: Some(1),
4306 ..Default::default()
4307 },
4308 )
4309 .await
4310 .unwrap();
4311
4312 let low = task_mgr
4313 .add_task("Low Task".to_string(), None, None, None, None, None)
4314 .await
4315 .unwrap();
4316 task_mgr
4317 .update_task(
4318 low.id,
4319 TaskUpdate {
4320 priority: Some(4),
4321 ..Default::default()
4322 },
4323 )
4324 .await
4325 .unwrap();
4326
4327 let high = task_mgr
4328 .add_task("High Task".to_string(), None, None, None, None, None)
4329 .await
4330 .unwrap();
4331 task_mgr
4332 .update_task(
4333 high.id,
4334 TaskUpdate {
4335 priority: Some(2),
4336 ..Default::default()
4337 },
4338 )
4339 .await
4340 .unwrap();
4341
4342 let medium = task_mgr
4343 .add_task("Medium Task".to_string(), None, None, None, None, None)
4344 .await
4345 .unwrap();
4346 task_mgr
4347 .update_task(
4348 medium.id,
4349 TaskUpdate {
4350 priority: Some(3),
4351 ..Default::default()
4352 },
4353 )
4354 .await
4355 .unwrap();
4356
4357 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
4359
4360 assert_eq!(tasks.len(), 4);
4361 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); }
4366
4367 #[tokio::test]
4368 async fn test_pick_next_prefers_doing_over_todo() {
4369 let ctx = TestContext::new().await;
4370 let task_mgr = TaskManager::new(ctx.pool());
4371 let workspace_mgr = WorkspaceManager::new(ctx.pool());
4372
4373 let parent = task_mgr
4375 .add_task("Parent".to_string(), None, None, None, None, None)
4376 .await
4377 .unwrap();
4378 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
4379 workspace_mgr
4380 .set_current_task(parent_started.task.id, None)
4381 .await
4382 .unwrap();
4383
4384 let doing_subtask = task_mgr
4386 .add_task(
4387 "Doing Subtask".to_string(),
4388 None,
4389 Some(parent.id),
4390 None,
4391 None,
4392 None,
4393 )
4394 .await
4395 .unwrap();
4396 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
4397 workspace_mgr
4399 .set_current_task(parent.id, None)
4400 .await
4401 .unwrap();
4402
4403 let _todo_subtask = task_mgr
4404 .add_task(
4405 "Todo Subtask".to_string(),
4406 None,
4407 Some(parent.id),
4408 None,
4409 None,
4410 None,
4411 )
4412 .await
4413 .unwrap();
4414
4415 let result = task_mgr.pick_next().await.unwrap();
4417
4418 if let Some(task) = result.task {
4419 assert_eq!(
4420 task.id, doing_subtask.id,
4421 "Should recommend doing subtask over todo subtask"
4422 );
4423 assert_eq!(task.status, "doing");
4424 } else {
4425 panic!("Expected a task recommendation");
4426 }
4427 }
4428
4429 #[tokio::test]
4430 async fn test_multiple_doing_tasks_allowed() {
4431 let ctx = TestContext::new().await;
4432 let task_mgr = TaskManager::new(ctx.pool());
4433 let workspace_mgr = WorkspaceManager::new(ctx.pool());
4434
4435 let task_a = task_mgr
4437 .add_task("Task A".to_string(), None, None, None, None, None)
4438 .await
4439 .unwrap();
4440 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
4441 assert_eq!(task_a_started.task.status, "doing");
4442
4443 let current = workspace_mgr.get_current_task(None).await.unwrap();
4445 assert_eq!(current.current_task_id, Some(task_a.id));
4446
4447 let task_b = task_mgr
4449 .add_task("Task B".to_string(), None, None, None, None, None)
4450 .await
4451 .unwrap();
4452 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
4453 assert_eq!(task_b_started.task.status, "doing");
4454
4455 let current = workspace_mgr.get_current_task(None).await.unwrap();
4457 assert_eq!(current.current_task_id, Some(task_b.id));
4458
4459 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
4461 assert_eq!(
4462 task_a_after.status, "doing",
4463 "Task A should remain doing even though it is not current"
4464 );
4465
4466 let doing_tasks: Vec<Task> = sqlx::query_as(
4468 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
4469 FROM tasks WHERE status = 'doing' AND deleted_at IS NULL ORDER BY id"#
4470 )
4471 .fetch_all(ctx.pool())
4472 .await
4473 .unwrap();
4474
4475 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
4476 assert_eq!(doing_tasks[0].id, task_a.id);
4477 assert_eq!(doing_tasks[1].id, task_b.id);
4478 }
4479 #[tokio::test]
4480 async fn test_find_tasks_pagination() {
4481 let ctx = TestContext::new().await;
4482 let task_mgr = TaskManager::new(ctx.pool());
4483
4484 for i in 0..15 {
4486 task_mgr
4487 .add_task(format!("Task {}", i), None, None, None, None, None)
4488 .await
4489 .unwrap();
4490 }
4491
4492 let page1 = task_mgr
4494 .find_tasks(None, None, None, Some(10), Some(0))
4495 .await
4496 .unwrap();
4497 assert_eq!(page1.tasks.len(), 10);
4498 assert_eq!(page1.total_count, 15);
4499 assert!(page1.has_more);
4500 assert_eq!(page1.offset, 0);
4501
4502 let page2 = task_mgr
4504 .find_tasks(None, None, None, Some(10), Some(10))
4505 .await
4506 .unwrap();
4507 assert_eq!(page2.tasks.len(), 5);
4508 assert_eq!(page2.total_count, 15);
4509 assert!(!page2.has_more);
4510 assert_eq!(page2.offset, 10);
4511 }
4512}
4513
4514