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