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 descendants = self.get_descendants(id).await?;
1015
1016 let mut subtree_ids: Vec<i64> = descendants.iter().map(|t| t.id).collect();
1018 subtree_ids.push(id);
1019
1020 if let Some((tid, sid)) = self.find_focused_in_set(&subtree_ids).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 now = chrono::Utc::now();
1035 sqlx::query(
1036 r#"
1037 WITH RECURSIVE subtree(id) AS (
1038 SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL
1039 UNION ALL
1040 SELECT t.id FROM tasks t JOIN subtree s ON t.parent_id = s.id
1041 WHERE t.deleted_at IS NULL
1042 )
1043 UPDATE tasks SET deleted_at = ? WHERE id IN (SELECT id FROM subtree)
1044 "#,
1045 )
1046 .bind(id)
1047 .bind(now)
1048 .execute(self.pool)
1049 .await?;
1050
1051 for &deleted_id in &subtree_ids {
1059 self.notify_task_deleted(deleted_id).await;
1060 }
1061
1062 Ok(subtree_ids.len())
1064 }
1065
1066 async fn find_focused_in_set(&self, task_ids: &[i64]) -> Result<Option<(i64, String)>> {
1069 if task_ids.is_empty() {
1070 return Ok(None);
1071 }
1072
1073 let placeholders: Vec<&str> = task_ids.iter().map(|_| "?").collect();
1075 let sql = format!(
1076 "SELECT current_task_id, session_id FROM sessions WHERE current_task_id IN ({}) LIMIT 1",
1077 placeholders.join(", ")
1078 );
1079
1080 let mut query = sqlx::query_as::<_, (i64, String)>(&sql);
1081 for id in task_ids {
1082 query = query.bind(id);
1083 }
1084
1085 Ok(query.fetch_optional(self.pool).await?)
1086 }
1087
1088 pub async fn add_dependency(&self, blocking_id: i64, blocked_id: i64) -> Result<()> {
1090 crate::dependencies::add_dependency(self.pool, blocking_id, blocked_id)
1091 .await
1092 .map(|_| ())
1093 }
1094
1095 pub async fn remove_dependency(&self, blocking_id: i64, blocked_id: i64) -> Result<()> {
1097 sqlx::query("DELETE FROM dependencies WHERE blocking_task_id = ? AND blocked_task_id = ?")
1098 .bind(blocking_id)
1099 .bind(blocked_id)
1100 .execute(self.pool)
1101 .await?;
1102 Ok(())
1103 }
1104
1105 pub async fn find_tasks(
1107 &self,
1108 status: Option<String>,
1109 parent_id: Option<Option<i64>>,
1110 sort_by: Option<TaskSortBy>,
1111 limit: Option<i64>,
1112 offset: Option<i64>,
1113 ) -> Result<PaginatedTasks> {
1114 let sort_by = sort_by.unwrap_or_default(); let limit = limit.unwrap_or(100);
1117 let offset = offset.unwrap_or(0);
1118
1119 let session_id = crate::workspace::resolve_session_id(None);
1121
1122 let mut where_clause = String::from("WHERE deleted_at IS NULL");
1124 let mut conditions = Vec::new();
1125
1126 if let Some(s) = status {
1127 let canonical = match crate::plan::TaskStatus::from_db_str(&s) {
1128 Some(ts) => ts.as_db_str(),
1129 None => return Err(IntentError::InvalidInput(format!("Invalid status: {}", s))),
1130 };
1131 where_clause.push_str(" AND status = ?");
1132 conditions.push(canonical.to_string());
1133 }
1134
1135 if let Some(pid) = parent_id {
1136 if let Some(p) = pid {
1137 where_clause.push_str(" AND parent_id = ?");
1138 conditions.push(p.to_string());
1139 } else {
1140 where_clause.push_str(" AND parent_id IS NULL");
1141 }
1142 }
1143
1144 let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
1146
1147 let order_clause = match sort_by {
1149 TaskSortBy::Id => {
1150 "ORDER BY id ASC".to_string()
1152 },
1153 TaskSortBy::Priority => {
1154 "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
1156 .to_string()
1157 },
1158 TaskSortBy::Time => {
1159 r#"ORDER BY
1161 CASE status
1162 WHEN 'doing' THEN first_doing_at
1163 WHEN 'todo' THEN first_todo_at
1164 WHEN 'done' THEN first_done_at
1165 END ASC NULLS LAST,
1166 id ASC"#
1167 .to_string()
1168 },
1169 TaskSortBy::FocusAware => {
1170 r#"ORDER BY
1172 CASE
1173 WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
1174 WHEN t.status = 'doing' THEN 1
1175 WHEN t.status = 'todo' THEN 2
1176 ELSE 3
1177 END ASC,
1178 COALESCE(t.priority, 999) ASC,
1179 t.id ASC"#
1180 .to_string()
1181 },
1182 };
1183
1184 let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
1186 let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
1187 for cond in &conditions {
1188 count_q = count_q.bind(cond);
1189 }
1190 let total_count = count_q.fetch_one(self.pool).await?;
1191
1192 let main_query = format!(
1194 "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 ?",
1195 where_clause, order_clause
1196 );
1197
1198 let mut q = sqlx::query_as::<_, Task>(&main_query);
1199 for cond in conditions {
1200 q = q.bind(cond);
1201 }
1202 if uses_session_bind {
1204 q = q.bind(&session_id);
1205 }
1206 q = q.bind(limit);
1207 q = q.bind(offset);
1208
1209 let tasks = q.fetch_all(self.pool).await?;
1210
1211 let has_more = offset + (tasks.len() as i64) < total_count;
1213
1214 Ok(PaginatedTasks {
1215 tasks,
1216 total_count,
1217 has_more,
1218 limit,
1219 offset,
1220 })
1221 }
1222
1223 pub async fn get_stats(&self) -> Result<WorkspaceStats> {
1228 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
1229 r#"SELECT
1230 COUNT(*) as total,
1231 COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
1232 COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
1233 COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
1234 FROM tasks WHERE deleted_at IS NULL"#,
1235 )
1236 .fetch_one(self.pool)
1237 .await?;
1238
1239 Ok(WorkspaceStats {
1240 total_tasks: row.0,
1241 todo: row.1,
1242 doing: row.2,
1243 done: row.3,
1244 })
1245 }
1246
1247 #[tracing::instrument(skip(self))]
1249 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
1250 let task_exists: bool =
1252 sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1253 .bind(id)
1254 .fetch_one(self.pool)
1255 .await?;
1256
1257 if !task_exists {
1258 return Err(IntentError::TaskNotFound(id));
1259 }
1260
1261 use crate::dependencies::get_incomplete_blocking_tasks;
1263 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
1264 return Err(IntentError::TaskBlocked {
1265 task_id: id,
1266 blocking_task_ids: blocking_tasks,
1267 });
1268 }
1269
1270 let mut tx = self.pool.begin().await?;
1271
1272 let now = Utc::now();
1273
1274 sqlx::query(
1276 r#"
1277 UPDATE tasks
1278 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
1279 WHERE id = ?
1280 "#,
1281 )
1282 .bind(now)
1283 .bind(id)
1284 .execute(&mut *tx)
1285 .await?;
1286
1287 let session_id = crate::workspace::resolve_session_id(None);
1290 sqlx::query(
1291 r#"
1292 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
1293 VALUES (?, ?, datetime('now'), datetime('now'))
1294 ON CONFLICT(session_id) DO UPDATE SET
1295 current_task_id = excluded.current_task_id,
1296 last_active_at = datetime('now')
1297 "#,
1298 )
1299 .bind(&session_id)
1300 .bind(id)
1301 .execute(&mut *tx)
1302 .await?;
1303
1304 tx.commit().await?;
1305
1306 if with_events {
1307 let result = self.get_task_with_events(id).await?;
1308 self.notify_task_updated(&result.task).await;
1309 Ok(result)
1310 } else {
1311 let task = self.get_task(id).await?;
1312 self.notify_task_updated(&task).await;
1313 Ok(TaskWithEvents {
1314 task,
1315 events_summary: None,
1316 })
1317 }
1318 }
1319
1320 async fn build_next_step_suggestion(
1324 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
1325 id: i64,
1326 task_name: &str,
1327 parent_id: Option<i64>,
1328 ) -> Result<NextStepSuggestion> {
1329 if let Some(parent_task_id) = parent_id {
1330 let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1331 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ? AND deleted_at IS NULL",
1332 )
1333 .bind(parent_task_id)
1334 .bind(id)
1335 .fetch_one(&mut **tx)
1336 .await?;
1337
1338 let parent_name: String =
1339 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1340 .bind(parent_task_id)
1341 .fetch_one(&mut **tx)
1342 .await?;
1343
1344 if remaining_siblings == 0 {
1345 Ok(NextStepSuggestion::ParentIsReady {
1346 message: format!(
1347 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1348 parent_task_id, parent_name
1349 ),
1350 parent_task_id,
1351 parent_task_name: parent_name,
1352 })
1353 } else {
1354 Ok(NextStepSuggestion::SiblingTasksRemain {
1355 message: format!(
1356 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1357 id, parent_task_id, parent_name
1358 ),
1359 parent_task_id,
1360 parent_task_name: parent_name,
1361 remaining_siblings_count: remaining_siblings,
1362 })
1363 }
1364 } else {
1365 let child_count: i64 =
1366 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1367 .bind(id)
1368 .fetch_one(&mut **tx)
1369 .await?;
1370
1371 if child_count > 0 {
1372 Ok(NextStepSuggestion::TopLevelTaskCompleted {
1373 message: format!(
1374 "Top-level task #{} '{}' has been completed. Well done!",
1375 id, task_name
1376 ),
1377 completed_task_id: id,
1378 completed_task_name: task_name.to_string(),
1379 })
1380 } else {
1381 let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1382 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ? AND deleted_at IS NULL",
1383 )
1384 .bind(id)
1385 .fetch_one(&mut **tx)
1386 .await?;
1387
1388 if remaining_tasks == 0 {
1389 Ok(NextStepSuggestion::WorkspaceIsClear {
1390 message: format!(
1391 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1392 id
1393 ),
1394 completed_task_id: id,
1395 })
1396 } else {
1397 Ok(NextStepSuggestion::NoParentContext {
1398 message: format!("Task #{} '{}' has been completed.", id, task_name),
1399 completed_task_id: id,
1400 completed_task_name: task_name.to_string(),
1401 })
1402 }
1403 }
1404 }
1405 }
1406
1407 #[tracing::instrument(skip(self))]
1416 pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1417 let session_id = crate::workspace::resolve_session_id(None);
1418 let mut tx = self.pool.begin().await?;
1419
1420 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1422 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1423 )
1424 .bind(&session_id)
1425 .fetch_optional(&mut *tx)
1426 .await?
1427 .flatten();
1428
1429 let id = current_task_id.ok_or(IntentError::InvalidInput(
1430 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
1431 ))?;
1432
1433 let task_info: (String, Option<i64>, String) =
1435 sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
1436 .bind(id)
1437 .fetch_one(&mut *tx)
1438 .await?;
1439 let (task_name, parent_id, owner) = task_info;
1440
1441 if owner == "human" && is_ai_caller {
1444 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1445 task_id: id,
1446 task_name: task_name.clone(),
1447 });
1448 }
1449
1450 self.complete_task_in_tx(&mut tx, id).await?;
1452
1453 sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1455 .bind(&session_id)
1456 .execute(&mut *tx)
1457 .await?;
1458
1459 let next_step_suggestion =
1460 Self::build_next_step_suggestion(&mut tx, id, &task_name, parent_id).await?;
1461
1462 tx.commit().await?;
1463
1464 let completed_task = self.get_task(id).await?;
1466 self.notify_task_updated(&completed_task).await;
1467
1468 Ok(DoneTaskResponse {
1469 completed_task,
1470 workspace_status: WorkspaceStatus {
1471 current_task_id: None,
1472 },
1473 next_step_suggestion,
1474 })
1475 }
1476
1477 #[tracing::instrument(skip(self))]
1487 pub async fn done_task_by_id(&self, id: i64, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1488 let session_id = crate::workspace::resolve_session_id(None);
1489 let mut tx = self.pool.begin().await?;
1490
1491 let task_info: (String, Option<i64>, String) = sqlx::query_as(
1493 "SELECT name, parent_id, owner FROM tasks WHERE id = ? AND deleted_at IS NULL",
1494 )
1495 .bind(id)
1496 .fetch_optional(&mut *tx)
1497 .await?
1498 .ok_or(IntentError::TaskNotFound(id))?;
1499 let (task_name, parent_id, owner) = task_info;
1500
1501 if owner == "human" && is_ai_caller {
1503 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1504 task_id: id,
1505 task_name: task_name.clone(),
1506 });
1507 }
1508
1509 self.complete_task_in_tx(&mut tx, id).await?;
1511
1512 sqlx::query(
1514 "UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ? AND current_task_id = ?",
1515 )
1516 .bind(&session_id)
1517 .bind(id)
1518 .execute(&mut *tx)
1519 .await?;
1520
1521 let actual_current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1523 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1524 )
1525 .bind(&session_id)
1526 .fetch_optional(&mut *tx)
1527 .await?
1528 .flatten();
1529
1530 let next_step_suggestion =
1531 Self::build_next_step_suggestion(&mut tx, id, &task_name, parent_id).await?;
1532
1533 let synthesis_result = self.try_synthesize_task_description(id, &task_name).await;
1535
1536 tx.commit().await?;
1537
1538 let mut completed_task = self.get_task(id).await?;
1540
1541 if let Ok(Some(new_spec)) = synthesis_result {
1543 completed_task = self
1544 .apply_synthesis_if_appropriate(completed_task, &new_spec, &owner)
1545 .await?;
1546 }
1547
1548 crate::llm::analyze_task_structure_background(self.pool.clone());
1550
1551 self.notify_task_updated(&completed_task).await;
1552
1553 Ok(DoneTaskResponse {
1554 completed_task,
1555 workspace_status: WorkspaceStatus {
1556 current_task_id: actual_current_task_id,
1557 },
1558 next_step_suggestion,
1559 })
1560 }
1561
1562 async fn check_task_exists(&self, id: i64) -> Result<()> {
1564 let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1565 .bind(id)
1566 .fetch_one(self.pool)
1567 .await?;
1568
1569 if !exists {
1570 return Err(IntentError::TaskNotFound(id));
1571 }
1572
1573 Ok(())
1574 }
1575
1576 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1578 let mut current_id = new_parent_id;
1579
1580 loop {
1581 if current_id == task_id {
1582 return Err(IntentError::CircularDependency {
1583 blocking_task_id: new_parent_id,
1584 blocked_task_id: task_id,
1585 });
1586 }
1587
1588 let parent: Option<i64> =
1589 sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1590 .bind(current_id)
1591 .fetch_optional(self.pool)
1592 .await?
1593 .flatten();
1594
1595 match parent {
1596 Some(pid) => current_id = pid,
1597 None => break,
1598 }
1599 }
1600
1601 Ok(())
1602 }
1603 pub async fn spawn_subtask(
1607 &self,
1608 name: &str,
1609 spec: Option<&str>,
1610 ) -> Result<SpawnSubtaskResponse> {
1611 let session_id = crate::workspace::resolve_session_id(None);
1613 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1614 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1615 )
1616 .bind(&session_id)
1617 .fetch_optional(self.pool)
1618 .await?
1619 .flatten();
1620
1621 let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1622 "No current task to create subtask under".to_string(),
1623 ))?;
1624
1625 let parent_name: String =
1627 sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1628 .bind(parent_id)
1629 .fetch_one(self.pool)
1630 .await?;
1631
1632 let subtask = self
1634 .add_task(
1635 name.to_string(),
1636 spec.map(|s| s.to_string()),
1637 Some(parent_id),
1638 Some("ai".to_string()),
1639 None,
1640 None,
1641 )
1642 .await?;
1643
1644 self.start_task(subtask.id, false).await?;
1647
1648 Ok(SpawnSubtaskResponse {
1649 subtask: SubtaskInfo {
1650 id: subtask.id,
1651 name: subtask.name,
1652 parent_id,
1653 status: "doing".to_string(),
1654 },
1655 parent_task: ParentTaskInfo {
1656 id: parent_id,
1657 name: parent_name,
1658 },
1659 })
1660 }
1661
1662 pub async fn pick_next_tasks(
1675 &self,
1676 max_count: usize,
1677 capacity_limit: usize,
1678 ) -> Result<Vec<Task>> {
1679 let mut tx = self.pool.begin().await?;
1680
1681 let doing_count: i64 =
1683 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1684 .fetch_one(&mut *tx)
1685 .await?;
1686
1687 let available = capacity_limit.saturating_sub(doing_count as usize);
1689 if available == 0 {
1690 return Ok(vec![]);
1691 }
1692
1693 let limit = std::cmp::min(max_count, available);
1694
1695 let todo_tasks = sqlx::query_as::<_, Task>(
1697 r#"
1698 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1699 FROM tasks
1700 WHERE status = 'todo' AND deleted_at IS NULL
1701 ORDER BY
1702 COALESCE(priority, 0) ASC,
1703 COALESCE(complexity, 5) ASC,
1704 id ASC
1705 LIMIT ?
1706 "#,
1707 )
1708 .bind(limit as i64)
1709 .fetch_all(&mut *tx)
1710 .await?;
1711
1712 if todo_tasks.is_empty() {
1713 return Ok(vec![]);
1714 }
1715
1716 let now = Utc::now();
1717
1718 for task in &todo_tasks {
1720 sqlx::query(
1721 r#"
1722 UPDATE tasks
1723 SET status = 'doing',
1724 first_doing_at = COALESCE(first_doing_at, ?)
1725 WHERE id = ?
1726 "#,
1727 )
1728 .bind(now)
1729 .bind(task.id)
1730 .execute(&mut *tx)
1731 .await?;
1732 }
1733
1734 tx.commit().await?;
1735
1736 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1738 let placeholders = vec!["?"; task_ids.len()].join(",");
1739 let query = format!(
1740 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1741 FROM tasks WHERE id IN ({})
1742 ORDER BY
1743 COALESCE(priority, 0) ASC,
1744 COALESCE(complexity, 5) ASC,
1745 id ASC",
1746 placeholders
1747 );
1748
1749 let mut q = sqlx::query_as::<_, Task>(&query);
1750 for id in task_ids {
1751 q = q.bind(id);
1752 }
1753
1754 let updated_tasks = q.fetch_all(self.pool).await?;
1755 Ok(updated_tasks)
1756 }
1757
1758 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1767 let session_id = crate::workspace::resolve_session_id(None);
1769 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1770 "SELECT current_task_id FROM sessions WHERE session_id = ?",
1771 )
1772 .bind(&session_id)
1773 .fetch_optional(self.pool)
1774 .await?
1775 .flatten();
1776
1777 if let Some(current_id) = current_task_id {
1778 let doing_subtasks = sqlx::query_as::<_, Task>(
1781 r#"
1782 SELECT id, parent_id, name, spec, status, complexity, priority,
1783 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1784 FROM tasks
1785 WHERE parent_id = ? AND status = 'doing' AND deleted_at IS NULL
1786 AND NOT EXISTS (
1787 SELECT 1 FROM dependencies d
1788 JOIN tasks bt ON d.blocking_task_id = bt.id
1789 WHERE d.blocked_task_id = tasks.id
1790 AND bt.status != 'done' AND bt.deleted_at IS NULL
1791 )
1792 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1793 LIMIT 1
1794 "#,
1795 )
1796 .bind(current_id)
1797 .fetch_optional(self.pool)
1798 .await?;
1799
1800 if let Some(task) = doing_subtasks {
1801 return Ok(PickNextResponse::focused_subtask(task));
1802 }
1803
1804 let todo_subtasks = sqlx::query_as::<_, Task>(
1806 r#"
1807 SELECT id, parent_id, name, spec, status, complexity, priority,
1808 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1809 FROM tasks
1810 WHERE parent_id = ? AND status = 'todo' AND deleted_at IS NULL
1811 AND NOT EXISTS (
1812 SELECT 1 FROM dependencies d
1813 JOIN tasks bt ON d.blocking_task_id = bt.id
1814 WHERE d.blocked_task_id = tasks.id
1815 AND bt.status != 'done' AND bt.deleted_at IS NULL
1816 )
1817 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1818 LIMIT 1
1819 "#,
1820 )
1821 .bind(current_id)
1822 .fetch_optional(self.pool)
1823 .await?;
1824
1825 if let Some(task) = todo_subtasks {
1826 return Ok(PickNextResponse::focused_subtask(task));
1827 }
1828 }
1829
1830 let doing_top_level = if let Some(current_id) = current_task_id {
1833 sqlx::query_as::<_, Task>(
1834 r#"
1835 SELECT id, parent_id, name, spec, status, complexity, priority,
1836 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1837 FROM tasks
1838 WHERE parent_id IS NULL AND status = 'doing' AND id != ? AND deleted_at IS NULL
1839 AND NOT EXISTS (
1840 SELECT 1 FROM dependencies d
1841 JOIN tasks bt ON d.blocking_task_id = bt.id
1842 WHERE d.blocked_task_id = tasks.id
1843 AND bt.status != 'done' AND bt.deleted_at IS NULL
1844 )
1845 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1846 LIMIT 1
1847 "#,
1848 )
1849 .bind(current_id)
1850 .fetch_optional(self.pool)
1851 .await?
1852 } else {
1853 sqlx::query_as::<_, Task>(
1854 r#"
1855 SELECT id, parent_id, name, spec, status, complexity, priority,
1856 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1857 FROM tasks
1858 WHERE parent_id IS NULL AND status = 'doing' AND deleted_at IS NULL
1859 AND NOT EXISTS (
1860 SELECT 1 FROM dependencies d
1861 JOIN tasks bt ON d.blocking_task_id = bt.id
1862 WHERE d.blocked_task_id = tasks.id
1863 AND bt.status != 'done' AND bt.deleted_at IS NULL
1864 )
1865 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1866 LIMIT 1
1867 "#,
1868 )
1869 .fetch_optional(self.pool)
1870 .await?
1871 };
1872
1873 if let Some(task) = doing_top_level {
1874 return Ok(PickNextResponse::top_level_task(task));
1875 }
1876
1877 let todo_top_level = sqlx::query_as::<_, Task>(
1880 r#"
1881 SELECT id, parent_id, name, spec, status, complexity, priority,
1882 first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1883 FROM tasks
1884 WHERE parent_id IS NULL AND status = 'todo' AND deleted_at IS NULL
1885 AND NOT EXISTS (
1886 SELECT 1 FROM dependencies d
1887 JOIN tasks bt ON d.blocking_task_id = bt.id
1888 WHERE d.blocked_task_id = tasks.id
1889 AND bt.status != 'done' AND bt.deleted_at IS NULL
1890 )
1891 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1892 LIMIT 1
1893 "#,
1894 )
1895 .fetch_optional(self.pool)
1896 .await?;
1897
1898 if let Some(task) = todo_top_level {
1899 return Ok(PickNextResponse::top_level_task(task));
1900 }
1901
1902 let total_tasks: i64 =
1905 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1906 .fetch_one(self.pool)
1907 .await?;
1908
1909 if total_tasks == 0 {
1910 return Ok(PickNextResponse::no_tasks_in_project());
1911 }
1912
1913 let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1915 "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing') AND deleted_at IS NULL",
1916 )
1917 .fetch_one(self.pool)
1918 .await?;
1919
1920 if todo_or_doing_count == 0 {
1921 return Ok(PickNextResponse::all_tasks_completed());
1922 }
1923
1924 Ok(PickNextResponse::no_available_todos())
1926 }
1927
1928 async fn try_synthesize_task_description(
1934 &self,
1935 task_id: i64,
1936 task_name: &str,
1937 ) -> Result<Option<String>> {
1938 let task = self.get_task(task_id).await?;
1940 let events = crate::events::EventManager::new(self.pool)
1941 .list_events(Some(task_id), None, None, None)
1942 .await?;
1943
1944 match crate::llm::synthesize_task_description(
1946 self.pool,
1947 task_name,
1948 task.spec.as_deref(),
1949 &events,
1950 )
1951 .await
1952 {
1953 Ok(synthesis) => Ok(synthesis),
1954 Err(e) => {
1955 tracing::warn!("LLM synthesis failed: {}", e);
1957 Ok(None)
1958 },
1959 }
1960 }
1961
1962 async fn apply_synthesis_if_appropriate(
1967 &self,
1968 task: Task,
1969 new_spec: &str,
1970 owner: &str,
1971 ) -> Result<Task> {
1972 if owner == "ai" {
1973 tracing::info!("Auto-applying LLM synthesis for AI-owned task #{}", task.id);
1975
1976 let updated = self
1977 .update_task(
1978 task.id,
1979 TaskUpdate {
1980 spec: Some(new_spec),
1981 ..Default::default()
1982 },
1983 )
1984 .await?;
1985
1986 Ok(updated)
1987 } else {
1988 tracing::info!(
1991 "LLM synthesis available for human-owned task #{}, but auto-apply disabled. \
1992 User would be prompted in interactive mode.",
1993 task.id
1994 );
1995 eprintln!("\n💡 LLM generated a task summary:");
1996 eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1997 eprintln!("{}", new_spec);
1998 eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1999 eprintln!("(Auto-apply disabled for human-owned tasks)");
2000 eprintln!(
2001 "To apply manually: ie task update {} --description \"<new spec>\"",
2002 task.id
2003 );
2004
2005 Ok(task) }
2007 }
2008}
2009
2010impl crate::backend::TaskBackend for TaskManager<'_> {
2011 fn get_task(&self, id: i64) -> impl std::future::Future<Output = Result<Task>> + Send {
2012 self.get_task(id)
2013 }
2014
2015 fn get_task_with_events(
2016 &self,
2017 id: i64,
2018 ) -> impl std::future::Future<Output = Result<TaskWithEvents>> + Send {
2019 self.get_task_with_events(id)
2020 }
2021
2022 fn get_task_ancestry(
2023 &self,
2024 task_id: i64,
2025 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2026 self.get_task_ancestry(task_id)
2027 }
2028
2029 fn get_task_context(
2030 &self,
2031 id: i64,
2032 ) -> impl std::future::Future<Output = Result<TaskContext>> + Send {
2033 self.get_task_context(id)
2034 }
2035
2036 fn get_siblings(
2037 &self,
2038 id: i64,
2039 parent_id: Option<i64>,
2040 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2041 self.get_siblings(id, parent_id)
2042 }
2043
2044 fn get_children(&self, id: i64) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2045 self.get_children(id)
2046 }
2047
2048 fn get_blocking_tasks(
2049 &self,
2050 id: i64,
2051 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2052 self.get_blocking_tasks(id)
2053 }
2054
2055 fn get_blocked_by_tasks(
2056 &self,
2057 id: i64,
2058 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2059 self.get_blocked_by_tasks(id)
2060 }
2061
2062 fn get_descendants(
2063 &self,
2064 task_id: i64,
2065 ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2066 self.get_descendants(task_id)
2067 }
2068
2069 fn get_status(
2070 &self,
2071 task_id: i64,
2072 with_events: bool,
2073 ) -> impl std::future::Future<Output = Result<crate::db::models::StatusResponse>> + Send {
2074 self.get_status(task_id, with_events)
2075 }
2076
2077 fn get_root_tasks(&self) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2078 self.get_root_tasks()
2079 }
2080
2081 fn find_tasks(
2082 &self,
2083 status: Option<String>,
2084 parent_id: Option<Option<i64>>,
2085 sort_by: Option<TaskSortBy>,
2086 limit: Option<i64>,
2087 offset: Option<i64>,
2088 ) -> impl std::future::Future<Output = Result<PaginatedTasks>> + Send {
2089 self.find_tasks(status, parent_id, sort_by, limit, offset)
2090 }
2091
2092 fn add_task(
2093 &self,
2094 name: String,
2095 spec: Option<String>,
2096 parent_id: Option<i64>,
2097 owner: Option<String>,
2098 priority: Option<i32>,
2099 metadata: Option<String>,
2100 ) -> impl std::future::Future<Output = Result<Task>> + Send {
2101 self.add_task(name, spec, parent_id, owner, priority, metadata)
2102 }
2103
2104 fn update_task(
2105 &self,
2106 id: i64,
2107 update: TaskUpdate<'_>,
2108 ) -> impl std::future::Future<Output = Result<Task>> + Send {
2109 self.update_task(id, update)
2110 }
2111
2112 fn delete_task(&self, id: i64) -> impl std::future::Future<Output = Result<()>> + Send {
2113 self.delete_task(id)
2114 }
2115
2116 fn delete_task_cascade(
2117 &self,
2118 id: i64,
2119 ) -> impl std::future::Future<Output = Result<usize>> + Send {
2120 self.delete_task_cascade(id)
2121 }
2122
2123 fn add_dependency(
2124 &self,
2125 blocking_id: i64,
2126 blocked_id: i64,
2127 ) -> impl std::future::Future<Output = Result<()>> + Send {
2128 self.add_dependency(blocking_id, blocked_id)
2129 }
2130
2131 fn remove_dependency(
2132 &self,
2133 blocking_id: i64,
2134 blocked_id: i64,
2135 ) -> impl std::future::Future<Output = Result<()>> + Send {
2136 self.remove_dependency(blocking_id, blocked_id)
2137 }
2138
2139 fn start_task(
2140 &self,
2141 id: i64,
2142 with_events: bool,
2143 ) -> impl std::future::Future<Output = Result<TaskWithEvents>> + Send {
2144 self.start_task(id, with_events)
2145 }
2146
2147 fn done_task(
2148 &self,
2149 is_ai_caller: bool,
2150 ) -> impl std::future::Future<Output = Result<DoneTaskResponse>> + Send {
2151 self.done_task(is_ai_caller)
2152 }
2153
2154 fn done_task_by_id(
2155 &self,
2156 id: i64,
2157 is_ai_caller: bool,
2158 ) -> impl std::future::Future<Output = Result<DoneTaskResponse>> + Send {
2159 self.done_task_by_id(id, is_ai_caller)
2160 }
2161
2162 fn pick_next(&self) -> impl std::future::Future<Output = Result<PickNextResponse>> + Send {
2163 self.pick_next()
2164 }
2165}
2166
2167impl crate::backend::SearchBackend for TaskManager<'_> {
2168 fn search(
2169 &self,
2170 query: String,
2171 include_tasks: bool,
2172 include_events: bool,
2173 limit: Option<i64>,
2174 offset: Option<i64>,
2175 ) -> impl std::future::Future<Output = Result<crate::db::models::PaginatedSearchResults>> + Send
2176 {
2177 use crate::search::SearchManager;
2178 let mgr = SearchManager::new(self.pool);
2179 async move {
2180 mgr.search(&query, include_tasks, include_events, limit, offset, false)
2181 .await
2182 }
2183 }
2184}
2185
2186#[cfg(test)]
2187mod tests {
2188 use super::*;
2189 use crate::events::EventManager;
2190 use crate::test_utils::test_helpers::TestContext;
2191 use crate::workspace::WorkspaceManager;
2192
2193 #[tokio::test]
2194 async fn test_get_stats_empty() {
2195 let ctx = TestContext::new().await;
2196 let manager = TaskManager::new(ctx.pool());
2197
2198 let stats = manager.get_stats().await.unwrap();
2199
2200 assert_eq!(stats.total_tasks, 0);
2201 assert_eq!(stats.todo, 0);
2202 assert_eq!(stats.doing, 0);
2203 assert_eq!(stats.done, 0);
2204 }
2205
2206 #[tokio::test]
2207 async fn test_get_stats_with_tasks() {
2208 let ctx = TestContext::new().await;
2209 let manager = TaskManager::new(ctx.pool());
2210
2211 let task1 = manager
2213 .add_task("Task 1".to_string(), None, None, None, None, None)
2214 .await
2215 .unwrap();
2216 let task2 = manager
2217 .add_task("Task 2".to_string(), None, None, None, None, None)
2218 .await
2219 .unwrap();
2220 let _task3 = manager
2221 .add_task("Task 3".to_string(), None, None, None, None, None)
2222 .await
2223 .unwrap();
2224
2225 manager
2227 .update_task(
2228 task1.id,
2229 TaskUpdate {
2230 status: Some("doing"),
2231 ..Default::default()
2232 },
2233 )
2234 .await
2235 .unwrap();
2236 manager
2237 .update_task(
2238 task2.id,
2239 TaskUpdate {
2240 status: Some("done"),
2241 ..Default::default()
2242 },
2243 )
2244 .await
2245 .unwrap();
2246 let stats = manager.get_stats().await.unwrap();
2249
2250 assert_eq!(stats.total_tasks, 3);
2251 assert_eq!(stats.todo, 1);
2252 assert_eq!(stats.doing, 1);
2253 assert_eq!(stats.done, 1);
2254 }
2255
2256 #[tokio::test]
2257 async fn test_add_task() {
2258 let ctx = TestContext::new().await;
2259 let manager = TaskManager::new(ctx.pool());
2260
2261 let task = manager
2262 .add_task("Test task".to_string(), None, None, None, None, None)
2263 .await
2264 .unwrap();
2265
2266 assert_eq!(task.name, "Test task");
2267 assert_eq!(task.status, "todo");
2268 assert!(task.first_todo_at.is_some());
2269 assert!(task.first_doing_at.is_none());
2270 assert!(task.first_done_at.is_none());
2271 }
2272
2273 #[tokio::test]
2274 async fn test_add_task_with_spec() {
2275 let ctx = TestContext::new().await;
2276 let manager = TaskManager::new(ctx.pool());
2277
2278 let spec = "This is a task specification";
2279 let task = manager
2280 .add_task(
2281 "Test task".to_string(),
2282 Some(spec.to_string()),
2283 None,
2284 None,
2285 None,
2286 None,
2287 )
2288 .await
2289 .unwrap();
2290
2291 assert_eq!(task.name, "Test task");
2292 assert_eq!(task.spec.as_deref(), Some(spec));
2293 }
2294
2295 #[tokio::test]
2296 async fn test_add_task_with_parent() {
2297 let ctx = TestContext::new().await;
2298 let manager = TaskManager::new(ctx.pool());
2299
2300 let parent = manager
2301 .add_task("Parent task".to_string(), None, None, None, None, None)
2302 .await
2303 .unwrap();
2304 let child = manager
2305 .add_task(
2306 "Child task".to_string(),
2307 None,
2308 Some(parent.id),
2309 None,
2310 None,
2311 None,
2312 )
2313 .await
2314 .unwrap();
2315
2316 assert_eq!(child.parent_id, Some(parent.id));
2317 }
2318
2319 #[tokio::test]
2320 async fn test_get_task() {
2321 let ctx = TestContext::new().await;
2322 let manager = TaskManager::new(ctx.pool());
2323
2324 let created = manager
2325 .add_task("Test task".to_string(), None, None, None, None, None)
2326 .await
2327 .unwrap();
2328 let retrieved = manager.get_task(created.id).await.unwrap();
2329
2330 assert_eq!(created.id, retrieved.id);
2331 assert_eq!(created.name, retrieved.name);
2332 }
2333
2334 #[tokio::test]
2335 async fn test_get_task_not_found() {
2336 let ctx = TestContext::new().await;
2337 let manager = TaskManager::new(ctx.pool());
2338
2339 let result = manager.get_task(999).await;
2340 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2341 }
2342
2343 #[tokio::test]
2344 async fn test_update_task_name() {
2345 let ctx = TestContext::new().await;
2346 let manager = TaskManager::new(ctx.pool());
2347
2348 let task = manager
2349 .add_task("Original name".to_string(), None, None, None, None, None)
2350 .await
2351 .unwrap();
2352 let updated = manager
2353 .update_task(
2354 task.id,
2355 TaskUpdate {
2356 name: Some("New name"),
2357 ..Default::default()
2358 },
2359 )
2360 .await
2361 .unwrap();
2362
2363 assert_eq!(updated.name, "New name");
2364 }
2365
2366 #[tokio::test]
2367 async fn test_update_task_status() {
2368 let ctx = TestContext::new().await;
2369 let manager = TaskManager::new(ctx.pool());
2370
2371 let task = manager
2372 .add_task("Test task".to_string(), None, None, None, None, None)
2373 .await
2374 .unwrap();
2375 let updated = manager
2376 .update_task(
2377 task.id,
2378 TaskUpdate {
2379 status: Some("doing"),
2380 ..Default::default()
2381 },
2382 )
2383 .await
2384 .unwrap();
2385
2386 assert_eq!(updated.status, "doing");
2387 assert!(updated.first_doing_at.is_some());
2388 }
2389
2390 #[tokio::test]
2391 async fn test_delete_task() {
2392 let ctx = TestContext::new().await;
2393 let manager = TaskManager::new(ctx.pool());
2394
2395 let task = manager
2396 .add_task("Test task".to_string(), None, None, None, None, None)
2397 .await
2398 .unwrap();
2399 manager.delete_task(task.id).await.unwrap();
2400
2401 let result = manager.get_task(task.id).await;
2402 assert!(result.is_err());
2403 }
2404
2405 #[tokio::test]
2406 async fn test_find_tasks_by_status() {
2407 let ctx = TestContext::new().await;
2408 let manager = TaskManager::new(ctx.pool());
2409
2410 manager
2411 .add_task("Todo task".to_string(), None, None, None, None, None)
2412 .await
2413 .unwrap();
2414 let doing_task = manager
2415 .add_task("Doing task".to_string(), None, None, None, None, None)
2416 .await
2417 .unwrap();
2418 manager
2419 .update_task(
2420 doing_task.id,
2421 TaskUpdate {
2422 status: Some("doing"),
2423 ..Default::default()
2424 },
2425 )
2426 .await
2427 .unwrap();
2428
2429 let todo_result = manager
2430 .find_tasks(Some("todo".to_string()), None, None, None, None)
2431 .await
2432 .unwrap();
2433 let doing_result = manager
2434 .find_tasks(Some("doing".to_string()), None, None, None, None)
2435 .await
2436 .unwrap();
2437
2438 assert_eq!(todo_result.tasks.len(), 1);
2439 assert_eq!(doing_result.tasks.len(), 1);
2440 assert_eq!(doing_result.tasks[0].status, "doing");
2441 }
2442
2443 #[tokio::test]
2444 async fn test_find_tasks_by_parent() {
2445 let ctx = TestContext::new().await;
2446 let manager = TaskManager::new(ctx.pool());
2447
2448 let parent = manager
2449 .add_task("Parent".to_string(), None, None, None, None, None)
2450 .await
2451 .unwrap();
2452 manager
2453 .add_task(
2454 "Child 1".to_string(),
2455 None,
2456 Some(parent.id),
2457 None,
2458 None,
2459 None,
2460 )
2461 .await
2462 .unwrap();
2463 manager
2464 .add_task(
2465 "Child 2".to_string(),
2466 None,
2467 Some(parent.id),
2468 None,
2469 None,
2470 None,
2471 )
2472 .await
2473 .unwrap();
2474
2475 let result = manager
2476 .find_tasks(None, Some(Some(parent.id)), None, None, None)
2477 .await
2478 .unwrap();
2479
2480 assert_eq!(result.tasks.len(), 2);
2481 }
2482
2483 #[tokio::test]
2484 async fn test_start_task() {
2485 let ctx = TestContext::new().await;
2486 let manager = TaskManager::new(ctx.pool());
2487
2488 let task = manager
2489 .add_task("Test task".to_string(), None, None, None, None, None)
2490 .await
2491 .unwrap();
2492 let started = manager.start_task(task.id, false).await.unwrap();
2493
2494 assert_eq!(started.task.status, "doing");
2495 assert!(started.task.first_doing_at.is_some());
2496
2497 let session_id = crate::workspace::resolve_session_id(None);
2499 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2500 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2501 )
2502 .bind(&session_id)
2503 .fetch_optional(ctx.pool())
2504 .await
2505 .unwrap()
2506 .flatten();
2507
2508 assert_eq!(current, Some(task.id));
2509 }
2510
2511 #[tokio::test]
2512 async fn test_start_task_with_events() {
2513 let ctx = TestContext::new().await;
2514 let manager = TaskManager::new(ctx.pool());
2515
2516 let task = manager
2517 .add_task("Test task".to_string(), None, None, None, None, None)
2518 .await
2519 .unwrap();
2520
2521 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
2523 .bind(task.id)
2524 .bind("test")
2525 .bind("test event")
2526 .execute(ctx.pool())
2527 .await
2528 .unwrap();
2529
2530 let started = manager.start_task(task.id, true).await.unwrap();
2531
2532 assert!(started.events_summary.is_some());
2533 let summary = started.events_summary.unwrap();
2534 assert_eq!(summary.total_count, 1);
2535 }
2536
2537 #[tokio::test]
2538 async fn test_done_task() {
2539 let ctx = TestContext::new().await;
2540 let manager = TaskManager::new(ctx.pool());
2541
2542 let task = manager
2543 .add_task("Test task".to_string(), None, None, None, None, None)
2544 .await
2545 .unwrap();
2546 manager.start_task(task.id, false).await.unwrap();
2547 let response = manager.done_task(false).await.unwrap();
2548
2549 assert_eq!(response.completed_task.status, "done");
2550 assert!(response.completed_task.first_done_at.is_some());
2551 assert_eq!(response.workspace_status.current_task_id, None);
2552
2553 match response.next_step_suggestion {
2555 NextStepSuggestion::WorkspaceIsClear { .. } => {},
2556 _ => panic!("Expected WorkspaceIsClear suggestion"),
2557 }
2558
2559 let session_id = crate::workspace::resolve_session_id(None);
2561 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2562 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2563 )
2564 .bind(&session_id)
2565 .fetch_optional(ctx.pool())
2566 .await
2567 .unwrap()
2568 .flatten();
2569
2570 assert!(current.is_none());
2571 }
2572
2573 #[tokio::test]
2574 async fn test_done_task_with_uncompleted_children() {
2575 let ctx = TestContext::new().await;
2576 let manager = TaskManager::new(ctx.pool());
2577
2578 let parent = manager
2579 .add_task("Parent".to_string(), None, None, None, None, None)
2580 .await
2581 .unwrap();
2582 manager
2583 .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
2584 .await
2585 .unwrap();
2586
2587 manager.start_task(parent.id, false).await.unwrap();
2589
2590 let result = manager.done_task(false).await;
2591 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
2592 }
2593
2594 #[tokio::test]
2595 async fn test_done_task_with_completed_children() {
2596 let ctx = TestContext::new().await;
2597 let manager = TaskManager::new(ctx.pool());
2598
2599 let parent = manager
2600 .add_task("Parent".to_string(), None, None, None, None, None)
2601 .await
2602 .unwrap();
2603 let child = manager
2604 .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
2605 .await
2606 .unwrap();
2607
2608 manager.start_task(child.id, false).await.unwrap();
2610 let child_response = manager.done_task(false).await.unwrap();
2611
2612 match child_response.next_step_suggestion {
2614 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
2615 assert_eq!(parent_task_id, parent.id);
2616 },
2617 _ => panic!("Expected ParentIsReady suggestion"),
2618 }
2619
2620 manager.start_task(parent.id, false).await.unwrap();
2622 let parent_response = manager.done_task(false).await.unwrap();
2623 assert_eq!(parent_response.completed_task.status, "done");
2624
2625 match parent_response.next_step_suggestion {
2627 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
2628 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2629 }
2630 }
2631
2632 #[tokio::test]
2633 async fn test_circular_dependency() {
2634 let ctx = TestContext::new().await;
2635 let manager = TaskManager::new(ctx.pool());
2636
2637 let task1 = manager
2638 .add_task("Task 1".to_string(), None, None, None, None, None)
2639 .await
2640 .unwrap();
2641 let task2 = manager
2642 .add_task("Task 2".to_string(), None, Some(task1.id), None, None, None)
2643 .await
2644 .unwrap();
2645
2646 let result = manager
2648 .update_task(
2649 task1.id,
2650 TaskUpdate {
2651 parent_id: Some(Some(task2.id)),
2652 ..Default::default()
2653 },
2654 )
2655 .await;
2656
2657 assert!(matches!(
2658 result,
2659 Err(IntentError::CircularDependency { .. })
2660 ));
2661 }
2662
2663 #[tokio::test]
2664 async fn test_invalid_parent_id() {
2665 let ctx = TestContext::new().await;
2666 let manager = TaskManager::new(ctx.pool());
2667
2668 let result = manager
2669 .add_task("Test".to_string(), None, Some(999), None, None, None)
2670 .await;
2671 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2672 }
2673
2674 #[tokio::test]
2675 async fn test_update_task_complexity_and_priority() {
2676 let ctx = TestContext::new().await;
2677 let manager = TaskManager::new(ctx.pool());
2678
2679 let task = manager
2680 .add_task("Test task".to_string(), None, None, None, None, None)
2681 .await
2682 .unwrap();
2683 let updated = manager
2684 .update_task(
2685 task.id,
2686 TaskUpdate {
2687 complexity: Some(8),
2688 priority: Some(10),
2689 ..Default::default()
2690 },
2691 )
2692 .await
2693 .unwrap();
2694
2695 assert_eq!(updated.complexity, Some(8));
2696 assert_eq!(updated.priority, Some(10));
2697 }
2698
2699 #[tokio::test]
2700 async fn test_spawn_subtask() {
2701 let ctx = TestContext::new().await;
2702 let manager = TaskManager::new(ctx.pool());
2703
2704 let parent = manager
2706 .add_task("Parent task".to_string(), None, None, None, None, None)
2707 .await
2708 .unwrap();
2709 manager.start_task(parent.id, false).await.unwrap();
2710
2711 let response = manager
2713 .spawn_subtask("Child task", Some("Details"))
2714 .await
2715 .unwrap();
2716
2717 assert_eq!(response.subtask.parent_id, parent.id);
2718 assert_eq!(response.subtask.name, "Child task");
2719 assert_eq!(response.subtask.status, "doing");
2720 assert_eq!(response.parent_task.id, parent.id);
2721 assert_eq!(response.parent_task.name, "Parent task");
2722
2723 let session_id = crate::workspace::resolve_session_id(None);
2725 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2726 "SELECT current_task_id FROM sessions WHERE session_id = ?",
2727 )
2728 .bind(&session_id)
2729 .fetch_optional(ctx.pool())
2730 .await
2731 .unwrap()
2732 .flatten();
2733
2734 assert_eq!(current, Some(response.subtask.id));
2735
2736 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
2738 assert_eq!(retrieved.status, "doing");
2739 }
2740
2741 #[tokio::test]
2742 async fn test_spawn_subtask_no_current_task() {
2743 let ctx = TestContext::new().await;
2744 let manager = TaskManager::new(ctx.pool());
2745
2746 let result = manager.spawn_subtask("Child", None).await;
2748 assert!(result.is_err());
2749 }
2750
2751 #[tokio::test]
2752 async fn test_pick_next_tasks_basic() {
2753 let ctx = TestContext::new().await;
2754 let manager = TaskManager::new(ctx.pool());
2755
2756 for i in 1..=10 {
2758 manager
2759 .add_task(format!("Task {}", i), None, None, None, None, None)
2760 .await
2761 .unwrap();
2762 }
2763
2764 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
2766
2767 assert_eq!(picked.len(), 5);
2768 for task in &picked {
2769 assert_eq!(task.status, "doing");
2770 assert!(task.first_doing_at.is_some());
2771 }
2772
2773 let doing_count: i64 =
2775 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2776 .fetch_one(ctx.pool())
2777 .await
2778 .unwrap();
2779
2780 assert_eq!(doing_count, 5);
2781 }
2782
2783 #[tokio::test]
2784 async fn test_pick_next_tasks_with_existing_doing() {
2785 let ctx = TestContext::new().await;
2786 let manager = TaskManager::new(ctx.pool());
2787
2788 for i in 1..=10 {
2790 manager
2791 .add_task(format!("Task {}", i), None, None, None, None, None)
2792 .await
2793 .unwrap();
2794 }
2795
2796 let result = manager
2798 .find_tasks(Some("todo".to_string()), None, None, None, None)
2799 .await
2800 .unwrap();
2801 manager.start_task(result.tasks[0].id, false).await.unwrap();
2802 manager.start_task(result.tasks[1].id, false).await.unwrap();
2803
2804 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2806
2807 assert_eq!(picked.len(), 3);
2809
2810 let doing_count: i64 =
2812 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2813 .fetch_one(ctx.pool())
2814 .await
2815 .unwrap();
2816
2817 assert_eq!(doing_count, 5);
2818 }
2819
2820 #[tokio::test]
2821 async fn test_pick_next_tasks_at_capacity() {
2822 let ctx = TestContext::new().await;
2823 let manager = TaskManager::new(ctx.pool());
2824
2825 for i in 1..=10 {
2827 manager
2828 .add_task(format!("Task {}", i), None, None, None, None, None)
2829 .await
2830 .unwrap();
2831 }
2832
2833 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2835 assert_eq!(first_batch.len(), 5);
2836
2837 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2839 assert_eq!(second_batch.len(), 0);
2840 }
2841
2842 #[tokio::test]
2843 async fn test_pick_next_tasks_priority_ordering() {
2844 let ctx = TestContext::new().await;
2845 let manager = TaskManager::new(ctx.pool());
2846
2847 let low = manager
2849 .add_task("Low priority".to_string(), None, None, None, None, None)
2850 .await
2851 .unwrap();
2852 manager
2853 .update_task(
2854 low.id,
2855 TaskUpdate {
2856 priority: Some(1),
2857 ..Default::default()
2858 },
2859 )
2860 .await
2861 .unwrap();
2862
2863 let high = manager
2864 .add_task("High priority".to_string(), None, None, None, None, None)
2865 .await
2866 .unwrap();
2867 manager
2868 .update_task(
2869 high.id,
2870 TaskUpdate {
2871 priority: Some(10),
2872 ..Default::default()
2873 },
2874 )
2875 .await
2876 .unwrap();
2877
2878 let medium = manager
2879 .add_task("Medium priority".to_string(), None, None, None, None, None)
2880 .await
2881 .unwrap();
2882 manager
2883 .update_task(
2884 medium.id,
2885 TaskUpdate {
2886 priority: Some(5),
2887 ..Default::default()
2888 },
2889 )
2890 .await
2891 .unwrap();
2892
2893 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2895
2896 assert_eq!(picked.len(), 3);
2898 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
2902
2903 #[tokio::test]
2904 async fn test_pick_next_tasks_complexity_ordering() {
2905 let ctx = TestContext::new().await;
2906 let manager = TaskManager::new(ctx.pool());
2907
2908 let complex = manager
2910 .add_task("Complex".to_string(), None, None, None, None, None)
2911 .await
2912 .unwrap();
2913 manager
2914 .update_task(
2915 complex.id,
2916 TaskUpdate {
2917 complexity: Some(9),
2918 priority: Some(5),
2919 ..Default::default()
2920 },
2921 )
2922 .await
2923 .unwrap();
2924
2925 let simple = manager
2926 .add_task("Simple".to_string(), None, None, None, None, None)
2927 .await
2928 .unwrap();
2929 manager
2930 .update_task(
2931 simple.id,
2932 TaskUpdate {
2933 complexity: Some(1),
2934 priority: Some(5),
2935 ..Default::default()
2936 },
2937 )
2938 .await
2939 .unwrap();
2940
2941 let medium = manager
2942 .add_task("Medium".to_string(), None, None, None, None, None)
2943 .await
2944 .unwrap();
2945 manager
2946 .update_task(
2947 medium.id,
2948 TaskUpdate {
2949 complexity: Some(5),
2950 priority: Some(5),
2951 ..Default::default()
2952 },
2953 )
2954 .await
2955 .unwrap();
2956
2957 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2959
2960 assert_eq!(picked.len(), 3);
2962 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
2966
2967 #[tokio::test]
2968 async fn test_done_task_sibling_tasks_remain() {
2969 let ctx = TestContext::new().await;
2970 let manager = TaskManager::new(ctx.pool());
2971
2972 let parent = manager
2974 .add_task("Parent Task".to_string(), None, None, None, None, None)
2975 .await
2976 .unwrap();
2977 let child1 = manager
2978 .add_task(
2979 "Child 1".to_string(),
2980 None,
2981 Some(parent.id),
2982 None,
2983 None,
2984 None,
2985 )
2986 .await
2987 .unwrap();
2988 let child2 = manager
2989 .add_task(
2990 "Child 2".to_string(),
2991 None,
2992 Some(parent.id),
2993 None,
2994 None,
2995 None,
2996 )
2997 .await
2998 .unwrap();
2999 let _child3 = manager
3000 .add_task(
3001 "Child 3".to_string(),
3002 None,
3003 Some(parent.id),
3004 None,
3005 None,
3006 None,
3007 )
3008 .await
3009 .unwrap();
3010
3011 manager.start_task(child1.id, false).await.unwrap();
3013 let response = manager.done_task(false).await.unwrap();
3014
3015 match response.next_step_suggestion {
3017 NextStepSuggestion::SiblingTasksRemain {
3018 parent_task_id,
3019 remaining_siblings_count,
3020 ..
3021 } => {
3022 assert_eq!(parent_task_id, parent.id);
3023 assert_eq!(remaining_siblings_count, 2); },
3025 _ => panic!("Expected SiblingTasksRemain suggestion"),
3026 }
3027
3028 manager.start_task(child2.id, false).await.unwrap();
3030 let response2 = manager.done_task(false).await.unwrap();
3031
3032 match response2.next_step_suggestion {
3034 NextStepSuggestion::SiblingTasksRemain {
3035 remaining_siblings_count,
3036 ..
3037 } => {
3038 assert_eq!(remaining_siblings_count, 1); },
3040 _ => panic!("Expected SiblingTasksRemain suggestion"),
3041 }
3042 }
3043
3044 #[tokio::test]
3045 async fn test_done_task_top_level_with_children() {
3046 let ctx = TestContext::new().await;
3047 let manager = TaskManager::new(ctx.pool());
3048
3049 let parent = manager
3051 .add_task("Epic Task".to_string(), None, None, None, None, None)
3052 .await
3053 .unwrap();
3054 let child = manager
3055 .add_task(
3056 "Sub Task".to_string(),
3057 None,
3058 Some(parent.id),
3059 None,
3060 None,
3061 None,
3062 )
3063 .await
3064 .unwrap();
3065
3066 manager.start_task(child.id, false).await.unwrap();
3068 manager.done_task(false).await.unwrap();
3069
3070 manager.start_task(parent.id, false).await.unwrap();
3072 let response = manager.done_task(false).await.unwrap();
3073
3074 match response.next_step_suggestion {
3076 NextStepSuggestion::TopLevelTaskCompleted {
3077 completed_task_id,
3078 completed_task_name,
3079 ..
3080 } => {
3081 assert_eq!(completed_task_id, parent.id);
3082 assert_eq!(completed_task_name, "Epic Task");
3083 },
3084 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
3085 }
3086 }
3087
3088 #[tokio::test]
3089 async fn test_done_task_no_parent_context() {
3090 let ctx = TestContext::new().await;
3091 let manager = TaskManager::new(ctx.pool());
3092
3093 let task1 = manager
3095 .add_task(
3096 "Standalone Task 1".to_string(),
3097 None,
3098 None,
3099 None,
3100 None,
3101 None,
3102 )
3103 .await
3104 .unwrap();
3105 let _task2 = manager
3106 .add_task(
3107 "Standalone Task 2".to_string(),
3108 None,
3109 None,
3110 None,
3111 None,
3112 None,
3113 )
3114 .await
3115 .unwrap();
3116
3117 manager.start_task(task1.id, false).await.unwrap();
3119 let response = manager.done_task(false).await.unwrap();
3120
3121 match response.next_step_suggestion {
3123 NextStepSuggestion::NoParentContext {
3124 completed_task_id,
3125 completed_task_name,
3126 ..
3127 } => {
3128 assert_eq!(completed_task_id, task1.id);
3129 assert_eq!(completed_task_name, "Standalone Task 1");
3130 },
3131 _ => panic!("Expected NoParentContext suggestion"),
3132 }
3133 }
3134
3135 #[tokio::test]
3140 async fn test_done_task_by_id_non_focused_task_preserves_focus() {
3141 let ctx = TestContext::new().await;
3142 let manager = TaskManager::new(ctx.pool());
3143
3144 let task_a = manager
3146 .add_task("Task A".to_string(), None, None, None, None, None)
3147 .await
3148 .unwrap();
3149 let task_b = manager
3150 .add_task("Task B".to_string(), None, None, None, None, None)
3151 .await
3152 .unwrap();
3153 manager.start_task(task_a.id, false).await.unwrap();
3154
3155 let response = manager.done_task_by_id(task_b.id, false).await.unwrap();
3157
3158 assert_eq!(response.completed_task.status, "done");
3160 assert_eq!(response.completed_task.id, task_b.id);
3161
3162 assert_eq!(response.workspace_status.current_task_id, Some(task_a.id));
3164
3165 let session_id = crate::workspace::resolve_session_id(None);
3167 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
3168 "SELECT current_task_id FROM sessions WHERE session_id = ?",
3169 )
3170 .bind(&session_id)
3171 .fetch_optional(ctx.pool())
3172 .await
3173 .unwrap()
3174 .flatten();
3175 assert_eq!(current, Some(task_a.id));
3176 }
3177
3178 #[tokio::test]
3179 async fn test_done_task_by_id_focused_task_clears_focus() {
3180 let ctx = TestContext::new().await;
3181 let manager = TaskManager::new(ctx.pool());
3182
3183 let task = manager
3184 .add_task("Focused task".to_string(), None, None, None, None, None)
3185 .await
3186 .unwrap();
3187 manager.start_task(task.id, false).await.unwrap();
3188
3189 let response = manager.done_task_by_id(task.id, false).await.unwrap();
3191
3192 assert_eq!(response.completed_task.status, "done");
3193 assert_eq!(response.workspace_status.current_task_id, None);
3194
3195 let session_id = crate::workspace::resolve_session_id(None);
3197 let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
3198 "SELECT current_task_id FROM sessions WHERE session_id = ?",
3199 )
3200 .bind(&session_id)
3201 .fetch_optional(ctx.pool())
3202 .await
3203 .unwrap()
3204 .flatten();
3205 assert!(current.is_none());
3206 }
3207
3208 #[tokio::test]
3209 async fn test_done_task_by_id_human_task_rejected_for_ai_caller() {
3210 let ctx = TestContext::new().await;
3211 let manager = TaskManager::new(ctx.pool());
3212
3213 let task = manager
3215 .add_task(
3216 "Human task".to_string(),
3217 None,
3218 None,
3219 Some("human".to_string()),
3220 None,
3221 None,
3222 )
3223 .await
3224 .unwrap();
3225 manager
3226 .update_task(
3227 task.id,
3228 TaskUpdate {
3229 status: Some("doing"),
3230 ..Default::default()
3231 },
3232 )
3233 .await
3234 .unwrap();
3235
3236 let result = manager.done_task_by_id(task.id, true).await;
3238 assert!(matches!(
3239 result,
3240 Err(IntentError::HumanTaskCannotBeCompletedByAI { .. })
3241 ));
3242
3243 let response = manager.done_task_by_id(task.id, false).await.unwrap();
3245 assert_eq!(response.completed_task.status, "done");
3246 }
3247
3248 #[tokio::test]
3249 async fn test_done_task_by_id_with_uncompleted_children() {
3250 let ctx = TestContext::new().await;
3251 let manager = TaskManager::new(ctx.pool());
3252
3253 let parent = manager
3254 .add_task("Parent".to_string(), None, None, None, None, None)
3255 .await
3256 .unwrap();
3257 manager
3258 .add_task(
3259 "Incomplete child".to_string(),
3260 None,
3261 Some(parent.id),
3262 None,
3263 None,
3264 None,
3265 )
3266 .await
3267 .unwrap();
3268
3269 let result = manager.done_task_by_id(parent.id, false).await;
3270 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
3271 }
3272
3273 #[tokio::test]
3274 async fn test_done_task_by_id_nonexistent_task() {
3275 let ctx = TestContext::new().await;
3276 let manager = TaskManager::new(ctx.pool());
3277
3278 let result = manager.done_task_by_id(99999, false).await;
3279 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
3280 }
3281
3282 #[tokio::test]
3283 async fn test_done_task_synthesis_graceful_when_llm_unconfigured() {
3284 let ctx = TestContext::new().await;
3286 let manager = TaskManager::new(ctx.pool());
3287 let event_mgr = EventManager::new(ctx.pool());
3288
3289 let task = manager
3291 .add_task(
3292 "Test Task".to_string(),
3293 Some("Original spec".to_string()),
3294 None,
3295 Some("ai".to_string()),
3296 None,
3297 None,
3298 )
3299 .await
3300 .unwrap();
3301
3302 event_mgr
3304 .add_event(task.id, "decision".to_string(), "Test decision".to_string())
3305 .await
3306 .unwrap();
3307
3308 manager.start_task(task.id, false).await.unwrap();
3309
3310 let result = manager.done_task_by_id(task.id, false).await;
3312 assert!(result.is_ok(), "Task completion should succeed without LLM");
3313
3314 let completed_task = manager.get_task(task.id).await.unwrap();
3316 assert_eq!(completed_task.status, "done");
3317
3318 assert_eq!(completed_task.spec, Some("Original spec".to_string()));
3320 }
3321
3322 #[tokio::test]
3323 async fn test_done_task_synthesis_respects_owner_field() {
3324 let ctx = TestContext::new().await;
3326 let manager = TaskManager::new(ctx.pool());
3327
3328 let ai_task = manager
3330 .add_task(
3331 "AI Task".to_string(),
3332 Some("AI spec".to_string()),
3333 None,
3334 Some("ai".to_string()),
3335 None,
3336 None,
3337 )
3338 .await
3339 .unwrap();
3340 assert_eq!(ai_task.owner, "ai");
3341
3342 let human_task = manager
3344 .add_task(
3345 "Human Task".to_string(),
3346 Some("Human spec".to_string()),
3347 None,
3348 Some("human".to_string()),
3349 None,
3350 None,
3351 )
3352 .await
3353 .unwrap();
3354 assert_eq!(human_task.owner, "human");
3355
3356 manager.start_task(ai_task.id, false).await.unwrap();
3358 let result = manager.done_task_by_id(ai_task.id, false).await;
3359 assert!(result.is_ok());
3360
3361 manager.start_task(human_task.id, false).await.unwrap();
3362 let result = manager.done_task_by_id(human_task.id, false).await;
3363 assert!(result.is_ok());
3364 }
3365
3366 #[tokio::test]
3367 async fn test_try_synthesize_task_description_basic() {
3368 let ctx = TestContext::new().await;
3369 let manager = TaskManager::new(ctx.pool());
3370
3371 let task = manager
3372 .add_task(
3373 "Synthesis Test".to_string(),
3374 Some("Original".to_string()),
3375 None,
3376 None,
3377 None,
3378 None,
3379 )
3380 .await
3381 .unwrap();
3382
3383 let result = manager
3385 .try_synthesize_task_description(task.id, &task.name)
3386 .await;
3387
3388 assert!(result.is_ok(), "Should not error when LLM unconfigured");
3389 assert_eq!(
3390 result.unwrap(),
3391 None,
3392 "Should return None when LLM unconfigured"
3393 );
3394 }
3395
3396 #[tokio::test]
3397 async fn test_pick_next_focused_subtask() {
3398 let ctx = TestContext::new().await;
3399 let manager = TaskManager::new(ctx.pool());
3400
3401 let parent = manager
3403 .add_task("Parent task".to_string(), None, None, None, None, None)
3404 .await
3405 .unwrap();
3406 manager.start_task(parent.id, false).await.unwrap();
3407
3408 let subtask1 = manager
3410 .add_task(
3411 "Subtask 1".to_string(),
3412 None,
3413 Some(parent.id),
3414 None,
3415 None,
3416 None,
3417 )
3418 .await
3419 .unwrap();
3420 let subtask2 = manager
3421 .add_task(
3422 "Subtask 2".to_string(),
3423 None,
3424 Some(parent.id),
3425 None,
3426 None,
3427 None,
3428 )
3429 .await
3430 .unwrap();
3431
3432 manager
3434 .update_task(
3435 subtask1.id,
3436 TaskUpdate {
3437 priority: Some(2),
3438 ..Default::default()
3439 },
3440 )
3441 .await
3442 .unwrap();
3443 manager
3444 .update_task(
3445 subtask2.id,
3446 TaskUpdate {
3447 priority: Some(1),
3448 ..Default::default()
3449 },
3450 )
3451 .await
3452 .unwrap();
3453
3454 let response = manager.pick_next().await.unwrap();
3456
3457 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
3458 assert!(response.task.is_some());
3459 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
3460 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
3461 }
3462
3463 #[tokio::test]
3464 async fn test_pick_next_top_level_task() {
3465 let ctx = TestContext::new().await;
3466 let manager = TaskManager::new(ctx.pool());
3467
3468 let task1 = manager
3470 .add_task("Task 1".to_string(), None, None, None, None, None)
3471 .await
3472 .unwrap();
3473 let task2 = manager
3474 .add_task("Task 2".to_string(), None, None, None, None, None)
3475 .await
3476 .unwrap();
3477
3478 manager
3480 .update_task(
3481 task1.id,
3482 TaskUpdate {
3483 priority: Some(5),
3484 ..Default::default()
3485 },
3486 )
3487 .await
3488 .unwrap();
3489 manager
3490 .update_task(
3491 task2.id,
3492 TaskUpdate {
3493 priority: Some(3),
3494 ..Default::default()
3495 },
3496 )
3497 .await
3498 .unwrap();
3499
3500 let response = manager.pick_next().await.unwrap();
3502
3503 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3504 assert!(response.task.is_some());
3505 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
3506 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
3507 }
3508
3509 #[tokio::test]
3510 async fn test_pick_next_no_tasks() {
3511 let ctx = TestContext::new().await;
3512 let manager = TaskManager::new(ctx.pool());
3513
3514 let response = manager.pick_next().await.unwrap();
3516
3517 assert_eq!(response.suggestion_type, "NONE");
3518 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
3519 assert!(response.message.is_some());
3520 }
3521
3522 #[tokio::test]
3523 async fn test_pick_next_all_completed() {
3524 let ctx = TestContext::new().await;
3525 let manager = TaskManager::new(ctx.pool());
3526
3527 let task = manager
3529 .add_task("Task 1".to_string(), None, None, None, None, None)
3530 .await
3531 .unwrap();
3532 manager.start_task(task.id, false).await.unwrap();
3533 manager.done_task(false).await.unwrap();
3534
3535 let response = manager.pick_next().await.unwrap();
3537
3538 assert_eq!(response.suggestion_type, "NONE");
3539 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
3540 assert!(response.message.is_some());
3541 }
3542
3543 #[tokio::test]
3544 async fn test_pick_next_no_available_todos() {
3545 let ctx = TestContext::new().await;
3546 let manager = TaskManager::new(ctx.pool());
3547
3548 let parent = manager
3550 .add_task("Parent task".to_string(), None, None, None, None, None)
3551 .await
3552 .unwrap();
3553 manager.start_task(parent.id, false).await.unwrap();
3554
3555 let subtask = manager
3557 .add_task(
3558 "Subtask".to_string(),
3559 None,
3560 Some(parent.id),
3561 None,
3562 None,
3563 None,
3564 )
3565 .await
3566 .unwrap();
3567 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
3569 .bind(subtask.id)
3570 .execute(ctx.pool())
3571 .await
3572 .unwrap();
3573
3574 let session_id = crate::workspace::resolve_session_id(None);
3576 sqlx::query(
3577 r#"
3578 INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
3579 VALUES (?, ?, datetime('now'), datetime('now'))
3580 ON CONFLICT(session_id) DO UPDATE SET
3581 current_task_id = excluded.current_task_id,
3582 last_active_at = datetime('now')
3583 "#,
3584 )
3585 .bind(&session_id)
3586 .bind(subtask.id)
3587 .execute(ctx.pool())
3588 .await
3589 .unwrap();
3590
3591 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
3593 .bind(parent.id)
3594 .execute(ctx.pool())
3595 .await
3596 .unwrap();
3597
3598 let response = manager.pick_next().await.unwrap();
3601
3602 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3603 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
3604 assert_eq!(response.task.as_ref().unwrap().status, "doing");
3605 }
3606
3607 #[tokio::test]
3608 async fn test_pick_next_priority_ordering() {
3609 let ctx = TestContext::new().await;
3610 let manager = TaskManager::new(ctx.pool());
3611
3612 let parent = manager
3614 .add_task("Parent".to_string(), None, None, None, None, None)
3615 .await
3616 .unwrap();
3617 manager.start_task(parent.id, false).await.unwrap();
3618
3619 let sub1 = manager
3621 .add_task(
3622 "Priority 10".to_string(),
3623 None,
3624 Some(parent.id),
3625 None,
3626 None,
3627 None,
3628 )
3629 .await
3630 .unwrap();
3631 manager
3632 .update_task(
3633 sub1.id,
3634 TaskUpdate {
3635 priority: Some(10),
3636 ..Default::default()
3637 },
3638 )
3639 .await
3640 .unwrap();
3641
3642 let sub2 = manager
3643 .add_task(
3644 "Priority 1".to_string(),
3645 None,
3646 Some(parent.id),
3647 None,
3648 None,
3649 None,
3650 )
3651 .await
3652 .unwrap();
3653 manager
3654 .update_task(
3655 sub2.id,
3656 TaskUpdate {
3657 priority: Some(1),
3658 ..Default::default()
3659 },
3660 )
3661 .await
3662 .unwrap();
3663
3664 let sub3 = manager
3665 .add_task(
3666 "Priority 5".to_string(),
3667 None,
3668 Some(parent.id),
3669 None,
3670 None,
3671 None,
3672 )
3673 .await
3674 .unwrap();
3675 manager
3676 .update_task(
3677 sub3.id,
3678 TaskUpdate {
3679 priority: Some(5),
3680 ..Default::default()
3681 },
3682 )
3683 .await
3684 .unwrap();
3685
3686 let response = manager.pick_next().await.unwrap();
3688
3689 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
3690 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
3691 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
3692 }
3693
3694 #[tokio::test]
3695 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
3696 let ctx = TestContext::new().await;
3697 let manager = TaskManager::new(ctx.pool());
3698
3699 let parent = manager
3701 .add_task("Parent".to_string(), None, None, None, None, None)
3702 .await
3703 .unwrap();
3704 manager.start_task(parent.id, false).await.unwrap();
3705
3706 let top_level = manager
3708 .add_task("Top level task".to_string(), None, None, None, None, None)
3709 .await
3710 .unwrap();
3711
3712 let response = manager.pick_next().await.unwrap();
3714
3715 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3716 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
3717 }
3718
3719 #[tokio::test]
3722 async fn test_get_task_with_events() {
3723 let ctx = TestContext::new().await;
3724 let task_mgr = TaskManager::new(ctx.pool());
3725 let event_mgr = EventManager::new(ctx.pool());
3726
3727 let task = task_mgr
3728 .add_task("Test".to_string(), None, None, None, None, None)
3729 .await
3730 .unwrap();
3731
3732 event_mgr
3734 .add_event(task.id, "progress".to_string(), "Event 1".to_string())
3735 .await
3736 .unwrap();
3737 event_mgr
3738 .add_event(task.id, "decision".to_string(), "Event 2".to_string())
3739 .await
3740 .unwrap();
3741
3742 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3743
3744 assert_eq!(result.task.id, task.id);
3745 assert!(result.events_summary.is_some());
3746
3747 let summary = result.events_summary.unwrap();
3748 assert_eq!(summary.total_count, 2);
3749 assert_eq!(summary.recent_events.len(), 2);
3750 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
3752 }
3753
3754 #[tokio::test]
3755 async fn test_get_task_with_events_nonexistent() {
3756 let ctx = TestContext::new().await;
3757 let task_mgr = TaskManager::new(ctx.pool());
3758
3759 let result = task_mgr.get_task_with_events(999).await;
3760 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
3761 }
3762
3763 #[tokio::test]
3764 async fn test_get_task_with_many_events() {
3765 let ctx = TestContext::new().await;
3766 let task_mgr = TaskManager::new(ctx.pool());
3767 let event_mgr = EventManager::new(ctx.pool());
3768
3769 let task = task_mgr
3770 .add_task("Test".to_string(), None, None, None, None, None)
3771 .await
3772 .unwrap();
3773
3774 for i in 0..20 {
3776 event_mgr
3777 .add_event(task.id, "test".to_string(), format!("Event {}", i))
3778 .await
3779 .unwrap();
3780 }
3781
3782 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3783 let summary = result.events_summary.unwrap();
3784
3785 assert_eq!(summary.total_count, 20);
3786 assert_eq!(summary.recent_events.len(), 10); }
3788
3789 #[tokio::test]
3790 async fn test_get_task_with_no_events() {
3791 let ctx = TestContext::new().await;
3792 let task_mgr = TaskManager::new(ctx.pool());
3793
3794 let task = task_mgr
3795 .add_task("Test".to_string(), None, None, None, None, None)
3796 .await
3797 .unwrap();
3798
3799 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3800 let summary = result.events_summary.unwrap();
3801
3802 assert_eq!(summary.total_count, 0);
3803 assert_eq!(summary.recent_events.len(), 0);
3804 }
3805
3806 #[tokio::test]
3807 async fn test_pick_next_tasks_zero_capacity() {
3808 let ctx = TestContext::new().await;
3809 let task_mgr = TaskManager::new(ctx.pool());
3810
3811 task_mgr
3812 .add_task("Task 1".to_string(), None, None, None, None, None)
3813 .await
3814 .unwrap();
3815
3816 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
3818 assert_eq!(results.len(), 0);
3819 }
3820
3821 #[tokio::test]
3822 async fn test_pick_next_tasks_capacity_exceeds_available() {
3823 let ctx = TestContext::new().await;
3824 let task_mgr = TaskManager::new(ctx.pool());
3825
3826 task_mgr
3827 .add_task("Task 1".to_string(), None, None, None, None, None)
3828 .await
3829 .unwrap();
3830 task_mgr
3831 .add_task("Task 2".to_string(), None, None, None, None, None)
3832 .await
3833 .unwrap();
3834
3835 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
3837 assert_eq!(results.len(), 2); }
3839
3840 #[tokio::test]
3843 async fn test_get_task_context_root_task_no_relations() {
3844 let ctx = TestContext::new().await;
3845 let task_mgr = TaskManager::new(ctx.pool());
3846
3847 let task = task_mgr
3849 .add_task("Root task".to_string(), None, None, None, None, None)
3850 .await
3851 .unwrap();
3852
3853 let context = task_mgr.get_task_context(task.id).await.unwrap();
3854
3855 assert_eq!(context.task.id, task.id);
3857 assert_eq!(context.task.name, "Root task");
3858
3859 assert_eq!(context.ancestors.len(), 0);
3861
3862 assert_eq!(context.siblings.len(), 0);
3864
3865 assert_eq!(context.children.len(), 0);
3867 }
3868
3869 #[tokio::test]
3870 async fn test_get_task_context_with_siblings() {
3871 let ctx = TestContext::new().await;
3872 let task_mgr = TaskManager::new(ctx.pool());
3873
3874 let task1 = task_mgr
3876 .add_task("Task 1".to_string(), None, None, None, None, None)
3877 .await
3878 .unwrap();
3879 let task2 = task_mgr
3880 .add_task("Task 2".to_string(), None, None, None, None, None)
3881 .await
3882 .unwrap();
3883 let task3 = task_mgr
3884 .add_task("Task 3".to_string(), None, None, None, None, None)
3885 .await
3886 .unwrap();
3887
3888 let context = task_mgr.get_task_context(task2.id).await.unwrap();
3889
3890 assert_eq!(context.task.id, task2.id);
3892
3893 assert_eq!(context.ancestors.len(), 0);
3895
3896 assert_eq!(context.siblings.len(), 2);
3898 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
3899 assert!(sibling_ids.contains(&task1.id));
3900 assert!(sibling_ids.contains(&task3.id));
3901 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
3905 }
3906
3907 #[tokio::test]
3908 async fn test_get_task_context_with_parent() {
3909 let ctx = TestContext::new().await;
3910 let task_mgr = TaskManager::new(ctx.pool());
3911
3912 let parent = task_mgr
3914 .add_task("Parent task".to_string(), None, None, None, None, None)
3915 .await
3916 .unwrap();
3917 let child = task_mgr
3918 .add_task(
3919 "Child task".to_string(),
3920 None,
3921 Some(parent.id),
3922 None,
3923 None,
3924 None,
3925 )
3926 .await
3927 .unwrap();
3928
3929 let context = task_mgr.get_task_context(child.id).await.unwrap();
3930
3931 assert_eq!(context.task.id, child.id);
3933 assert_eq!(context.task.parent_id, Some(parent.id));
3934
3935 assert_eq!(context.ancestors.len(), 1);
3937 assert_eq!(context.ancestors[0].id, parent.id);
3938 assert_eq!(context.ancestors[0].name, "Parent task");
3939
3940 assert_eq!(context.siblings.len(), 0);
3942
3943 assert_eq!(context.children.len(), 0);
3945 }
3946
3947 #[tokio::test]
3948 async fn test_get_task_context_with_children() {
3949 let ctx = TestContext::new().await;
3950 let task_mgr = TaskManager::new(ctx.pool());
3951
3952 let parent = task_mgr
3954 .add_task("Parent task".to_string(), None, None, None, None, None)
3955 .await
3956 .unwrap();
3957 let child1 = task_mgr
3958 .add_task(
3959 "Child 1".to_string(),
3960 None,
3961 Some(parent.id),
3962 None,
3963 None,
3964 None,
3965 )
3966 .await
3967 .unwrap();
3968 let child2 = task_mgr
3969 .add_task(
3970 "Child 2".to_string(),
3971 None,
3972 Some(parent.id),
3973 None,
3974 None,
3975 None,
3976 )
3977 .await
3978 .unwrap();
3979 let child3 = task_mgr
3980 .add_task(
3981 "Child 3".to_string(),
3982 None,
3983 Some(parent.id),
3984 None,
3985 None,
3986 None,
3987 )
3988 .await
3989 .unwrap();
3990
3991 let context = task_mgr.get_task_context(parent.id).await.unwrap();
3992
3993 assert_eq!(context.task.id, parent.id);
3995
3996 assert_eq!(context.ancestors.len(), 0);
3998
3999 assert_eq!(context.siblings.len(), 0);
4001
4002 assert_eq!(context.children.len(), 3);
4004 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
4005 assert!(child_ids.contains(&child1.id));
4006 assert!(child_ids.contains(&child2.id));
4007 assert!(child_ids.contains(&child3.id));
4008 }
4009
4010 #[tokio::test]
4011 async fn test_get_task_context_multi_level_hierarchy() {
4012 let ctx = TestContext::new().await;
4013 let task_mgr = TaskManager::new(ctx.pool());
4014
4015 let grandparent = task_mgr
4017 .add_task("Grandparent".to_string(), None, None, None, None, None)
4018 .await
4019 .unwrap();
4020 let parent = task_mgr
4021 .add_task(
4022 "Parent".to_string(),
4023 None,
4024 Some(grandparent.id),
4025 None,
4026 None,
4027 None,
4028 )
4029 .await
4030 .unwrap();
4031 let child = task_mgr
4032 .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
4033 .await
4034 .unwrap();
4035
4036 let context = task_mgr.get_task_context(child.id).await.unwrap();
4037
4038 assert_eq!(context.task.id, child.id);
4040
4041 assert_eq!(context.ancestors.len(), 2);
4043 assert_eq!(context.ancestors[0].id, parent.id);
4044 assert_eq!(context.ancestors[0].name, "Parent");
4045 assert_eq!(context.ancestors[1].id, grandparent.id);
4046 assert_eq!(context.ancestors[1].name, "Grandparent");
4047
4048 assert_eq!(context.siblings.len(), 0);
4050
4051 assert_eq!(context.children.len(), 0);
4053 }
4054
4055 #[tokio::test]
4056 async fn test_get_task_context_complex_family_tree() {
4057 let ctx = TestContext::new().await;
4058 let task_mgr = TaskManager::new(ctx.pool());
4059
4060 let root = task_mgr
4068 .add_task("Root".to_string(), None, None, None, None, None)
4069 .await
4070 .unwrap();
4071 let child1 = task_mgr
4072 .add_task("Child1".to_string(), None, Some(root.id), None, None, None)
4073 .await
4074 .unwrap();
4075 let child2 = task_mgr
4076 .add_task("Child2".to_string(), None, Some(root.id), None, None, None)
4077 .await
4078 .unwrap();
4079 let grandchild1 = task_mgr
4080 .add_task(
4081 "Grandchild1".to_string(),
4082 None,
4083 Some(child1.id),
4084 None,
4085 None,
4086 None,
4087 )
4088 .await
4089 .unwrap();
4090 let grandchild2 = task_mgr
4091 .add_task(
4092 "Grandchild2".to_string(),
4093 None,
4094 Some(child1.id),
4095 None,
4096 None,
4097 None,
4098 )
4099 .await
4100 .unwrap();
4101
4102 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
4104
4105 assert_eq!(context.task.id, grandchild2.id);
4107
4108 assert_eq!(context.ancestors.len(), 2);
4110 assert_eq!(context.ancestors[0].id, child1.id);
4111 assert_eq!(context.ancestors[1].id, root.id);
4112
4113 assert_eq!(context.siblings.len(), 1);
4115 assert_eq!(context.siblings[0].id, grandchild1.id);
4116
4117 assert_eq!(context.children.len(), 0);
4119
4120 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
4122 assert_eq!(context_child1.ancestors.len(), 1);
4123 assert_eq!(context_child1.ancestors[0].id, root.id);
4124 assert_eq!(context_child1.siblings.len(), 1);
4125 assert_eq!(context_child1.siblings[0].id, child2.id);
4126 assert_eq!(context_child1.children.len(), 2);
4127 }
4128
4129 #[tokio::test]
4130 async fn test_get_task_context_respects_priority_ordering() {
4131 let ctx = TestContext::new().await;
4132 let task_mgr = TaskManager::new(ctx.pool());
4133
4134 let parent = task_mgr
4136 .add_task("Parent".to_string(), None, None, None, None, None)
4137 .await
4138 .unwrap();
4139
4140 let child_low = task_mgr
4142 .add_task(
4143 "Low priority".to_string(),
4144 None,
4145 Some(parent.id),
4146 None,
4147 None,
4148 None,
4149 )
4150 .await
4151 .unwrap();
4152 let _ = task_mgr
4153 .update_task(
4154 child_low.id,
4155 TaskUpdate {
4156 priority: Some(10),
4157 ..Default::default()
4158 },
4159 )
4160 .await
4161 .unwrap();
4162
4163 let child_high = task_mgr
4164 .add_task(
4165 "High priority".to_string(),
4166 None,
4167 Some(parent.id),
4168 None,
4169 None,
4170 None,
4171 )
4172 .await
4173 .unwrap();
4174 let _ = task_mgr
4175 .update_task(
4176 child_high.id,
4177 TaskUpdate {
4178 priority: Some(1),
4179 ..Default::default()
4180 },
4181 )
4182 .await
4183 .unwrap();
4184
4185 let child_medium = task_mgr
4186 .add_task(
4187 "Medium priority".to_string(),
4188 None,
4189 Some(parent.id),
4190 None,
4191 None,
4192 None,
4193 )
4194 .await
4195 .unwrap();
4196 let _ = task_mgr
4197 .update_task(
4198 child_medium.id,
4199 TaskUpdate {
4200 priority: Some(5),
4201 ..Default::default()
4202 },
4203 )
4204 .await
4205 .unwrap();
4206
4207 let context = task_mgr.get_task_context(parent.id).await.unwrap();
4208
4209 assert_eq!(context.children.len(), 3);
4211 assert_eq!(context.children[0].priority, Some(1));
4212 assert_eq!(context.children[1].priority, Some(5));
4213 assert_eq!(context.children[2].priority, Some(10));
4214 }
4215
4216 #[tokio::test]
4217 async fn test_get_task_context_nonexistent_task() {
4218 let ctx = TestContext::new().await;
4219 let task_mgr = TaskManager::new(ctx.pool());
4220
4221 let result = task_mgr.get_task_context(99999).await;
4222 assert!(result.is_err());
4223 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
4224 }
4225
4226 #[tokio::test]
4227 async fn test_get_task_context_handles_null_priority() {
4228 let ctx = TestContext::new().await;
4229 let task_mgr = TaskManager::new(ctx.pool());
4230
4231 let task1 = task_mgr
4233 .add_task("Task 1".to_string(), None, None, None, None, None)
4234 .await
4235 .unwrap();
4236 let _ = task_mgr
4237 .update_task(
4238 task1.id,
4239 TaskUpdate {
4240 priority: Some(1),
4241 ..Default::default()
4242 },
4243 )
4244 .await
4245 .unwrap();
4246
4247 let task2 = task_mgr
4248 .add_task("Task 2".to_string(), None, None, None, None, None)
4249 .await
4250 .unwrap();
4251 let task3 = task_mgr
4254 .add_task("Task 3".to_string(), None, None, None, None, None)
4255 .await
4256 .unwrap();
4257 let _ = task_mgr
4258 .update_task(
4259 task3.id,
4260 TaskUpdate {
4261 priority: Some(5),
4262 ..Default::default()
4263 },
4264 )
4265 .await
4266 .unwrap();
4267
4268 let context = task_mgr.get_task_context(task2.id).await.unwrap();
4269
4270 assert_eq!(context.siblings.len(), 2);
4272 assert_eq!(context.siblings[0].id, task1.id);
4274 assert_eq!(context.siblings[0].priority, Some(1));
4275 assert_eq!(context.siblings[1].id, task3.id);
4277 assert_eq!(context.siblings[1].priority, Some(5));
4278 }
4279
4280 #[tokio::test]
4281 async fn test_pick_next_tasks_priority_order() {
4282 let ctx = TestContext::new().await;
4283 let task_mgr = TaskManager::new(ctx.pool());
4284
4285 let critical = task_mgr
4287 .add_task("Critical Task".to_string(), None, None, None, None, None)
4288 .await
4289 .unwrap();
4290 task_mgr
4291 .update_task(
4292 critical.id,
4293 TaskUpdate {
4294 priority: Some(1),
4295 ..Default::default()
4296 },
4297 )
4298 .await
4299 .unwrap();
4300
4301 let low = task_mgr
4302 .add_task("Low Task".to_string(), None, None, None, None, None)
4303 .await
4304 .unwrap();
4305 task_mgr
4306 .update_task(
4307 low.id,
4308 TaskUpdate {
4309 priority: Some(4),
4310 ..Default::default()
4311 },
4312 )
4313 .await
4314 .unwrap();
4315
4316 let high = task_mgr
4317 .add_task("High Task".to_string(), None, None, None, None, None)
4318 .await
4319 .unwrap();
4320 task_mgr
4321 .update_task(
4322 high.id,
4323 TaskUpdate {
4324 priority: Some(2),
4325 ..Default::default()
4326 },
4327 )
4328 .await
4329 .unwrap();
4330
4331 let medium = task_mgr
4332 .add_task("Medium Task".to_string(), None, None, None, None, None)
4333 .await
4334 .unwrap();
4335 task_mgr
4336 .update_task(
4337 medium.id,
4338 TaskUpdate {
4339 priority: Some(3),
4340 ..Default::default()
4341 },
4342 )
4343 .await
4344 .unwrap();
4345
4346 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
4348
4349 assert_eq!(tasks.len(), 4);
4350 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); }
4355
4356 #[tokio::test]
4357 async fn test_pick_next_prefers_doing_over_todo() {
4358 let ctx = TestContext::new().await;
4359 let task_mgr = TaskManager::new(ctx.pool());
4360 let workspace_mgr = WorkspaceManager::new(ctx.pool());
4361
4362 let parent = task_mgr
4364 .add_task("Parent".to_string(), None, None, None, None, None)
4365 .await
4366 .unwrap();
4367 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
4368 workspace_mgr
4369 .set_current_task(parent_started.task.id, None)
4370 .await
4371 .unwrap();
4372
4373 let doing_subtask = task_mgr
4375 .add_task(
4376 "Doing Subtask".to_string(),
4377 None,
4378 Some(parent.id),
4379 None,
4380 None,
4381 None,
4382 )
4383 .await
4384 .unwrap();
4385 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
4386 workspace_mgr
4388 .set_current_task(parent.id, None)
4389 .await
4390 .unwrap();
4391
4392 let _todo_subtask = task_mgr
4393 .add_task(
4394 "Todo Subtask".to_string(),
4395 None,
4396 Some(parent.id),
4397 None,
4398 None,
4399 None,
4400 )
4401 .await
4402 .unwrap();
4403
4404 let result = task_mgr.pick_next().await.unwrap();
4406
4407 if let Some(task) = result.task {
4408 assert_eq!(
4409 task.id, doing_subtask.id,
4410 "Should recommend doing subtask over todo subtask"
4411 );
4412 assert_eq!(task.status, "doing");
4413 } else {
4414 panic!("Expected a task recommendation");
4415 }
4416 }
4417
4418 #[tokio::test]
4419 async fn test_multiple_doing_tasks_allowed() {
4420 let ctx = TestContext::new().await;
4421 let task_mgr = TaskManager::new(ctx.pool());
4422 let workspace_mgr = WorkspaceManager::new(ctx.pool());
4423
4424 let task_a = task_mgr
4426 .add_task("Task A".to_string(), None, None, None, None, None)
4427 .await
4428 .unwrap();
4429 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
4430 assert_eq!(task_a_started.task.status, "doing");
4431
4432 let current = workspace_mgr.get_current_task(None).await.unwrap();
4434 assert_eq!(current.current_task_id, Some(task_a.id));
4435
4436 let task_b = task_mgr
4438 .add_task("Task B".to_string(), None, None, None, None, None)
4439 .await
4440 .unwrap();
4441 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
4442 assert_eq!(task_b_started.task.status, "doing");
4443
4444 let current = workspace_mgr.get_current_task(None).await.unwrap();
4446 assert_eq!(current.current_task_id, Some(task_b.id));
4447
4448 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
4450 assert_eq!(
4451 task_a_after.status, "doing",
4452 "Task A should remain doing even though it is not current"
4453 );
4454
4455 let doing_tasks: Vec<Task> = sqlx::query_as(
4457 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
4458 FROM tasks WHERE status = 'doing' AND deleted_at IS NULL ORDER BY id"#
4459 )
4460 .fetch_all(ctx.pool())
4461 .await
4462 .unwrap();
4463
4464 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
4465 assert_eq!(doing_tasks[0].id, task_a.id);
4466 assert_eq!(doing_tasks[1].id, task_b.id);
4467 }
4468 #[tokio::test]
4469 async fn test_find_tasks_pagination() {
4470 let ctx = TestContext::new().await;
4471 let task_mgr = TaskManager::new(ctx.pool());
4472
4473 for i in 0..15 {
4475 task_mgr
4476 .add_task(format!("Task {}", i), None, None, None, None, None)
4477 .await
4478 .unwrap();
4479 }
4480
4481 let page1 = task_mgr
4483 .find_tasks(None, None, None, Some(10), Some(0))
4484 .await
4485 .unwrap();
4486 assert_eq!(page1.tasks.len(), 10);
4487 assert_eq!(page1.total_count, 15);
4488 assert!(page1.has_more);
4489 assert_eq!(page1.offset, 0);
4490
4491 let page2 = task_mgr
4493 .find_tasks(None, None, None, Some(10), Some(10))
4494 .await
4495 .unwrap();
4496 assert_eq!(page2.tasks.len(), 5);
4497 assert_eq!(page2.total_count, 15);
4498 assert!(!page2.has_more);
4499 assert_eq!(page2.offset, 10);
4500 }
4501}
4502
4503