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