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 project_path: Option<String>,
16}
17
18impl<'a> TaskManager<'a> {
19 pub fn new(pool: &'a SqlitePool) -> Self {
20 Self {
21 pool,
22 notifier: crate::notifications::NotificationSender::new(None, None),
23 project_path: None,
24 }
25 }
26
27 pub fn with_mcp_notifier(
29 pool: &'a SqlitePool,
30 project_path: String,
31 mcp_notifier: tokio::sync::mpsc::UnboundedSender<String>,
32 ) -> Self {
33 Self {
34 pool,
35 notifier: crate::notifications::NotificationSender::new(None, Some(mcp_notifier)),
36 project_path: Some(project_path),
37 }
38 }
39
40 pub fn with_websocket(
42 pool: &'a SqlitePool,
43 ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
44 project_path: String,
45 ) -> Self {
46 Self {
47 pool,
48 notifier: crate::notifications::NotificationSender::new(Some(ws_state), None),
49 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 let Some(project_path) = &self.project_path else {
58 return;
59 };
60
61 let task_json = match serde_json::to_value(task) {
62 Ok(json) => json,
63 Err(e) => {
64 tracing::warn!("Failed to serialize task for notification: {}", e);
65 return;
66 },
67 };
68
69 let payload =
70 DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
71 self.notifier.send(payload).await;
72 }
73
74 async fn notify_task_updated(&self, task: &Task) {
76 use crate::dashboard::websocket::DatabaseOperationPayload;
77
78 let Some(project_path) = &self.project_path else {
79 return;
80 };
81
82 let task_json = match serde_json::to_value(task) {
83 Ok(json) => json,
84 Err(e) => {
85 tracing::warn!("Failed to serialize task for notification: {}", e);
86 return;
87 },
88 };
89
90 let payload =
91 DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
92 self.notifier.send(payload).await;
93 }
94
95 async fn notify_task_deleted(&self, task_id: i64) {
97 use crate::dashboard::websocket::DatabaseOperationPayload;
98
99 let Some(project_path) = &self.project_path else {
100 return;
101 };
102
103 let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
104 self.notifier.send(payload).await;
105 }
106
107 pub async fn add_task(
109 &self,
110 name: &str,
111 spec: Option<&str>,
112 parent_id: Option<i64>,
113 ) -> Result<Task> {
114 if let Some(pid) = parent_id {
116 self.check_task_exists(pid).await?;
117 }
118
119 let now = Utc::now();
120
121 let result = sqlx::query(
122 r#"
123 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
124 VALUES (?, ?, ?, 'todo', ?)
125 "#,
126 )
127 .bind(name)
128 .bind(spec)
129 .bind(parent_id)
130 .bind(now)
131 .execute(self.pool)
132 .await?;
133
134 let id = result.last_insert_rowid();
135 let task = self.get_task(id).await?;
136
137 self.notify_task_created(&task).await;
139
140 Ok(task)
141 }
142
143 pub async fn get_task(&self, id: i64) -> Result<Task> {
145 let task = sqlx::query_as::<_, Task>(
146 r#"
147 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
148 FROM tasks
149 WHERE id = ?
150 "#,
151 )
152 .bind(id)
153 .fetch_optional(self.pool)
154 .await?
155 .ok_or(IntentError::TaskNotFound(id))?;
156
157 Ok(task)
158 }
159
160 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
162 let task = self.get_task(id).await?;
163 let events_summary = self.get_events_summary(id).await?;
164
165 Ok(TaskWithEvents {
166 task,
167 events_summary: Some(events_summary),
168 })
169 }
170
171 pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
180 let mut chain = Vec::new();
181 let mut current_id = Some(task_id);
182
183 while let Some(id) = current_id {
184 let task = self.get_task(id).await?;
185 current_id = task.parent_id;
186 chain.push(task);
187 }
188
189 Ok(chain)
190 }
191
192 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
200 let task = self.get_task(id).await?;
202
203 let mut ancestors = Vec::new();
205 let mut current_parent_id = task.parent_id;
206
207 while let Some(parent_id) = current_parent_id {
208 let parent = self.get_task(parent_id).await?;
209 current_parent_id = parent.parent_id;
210 ancestors.push(parent);
211 }
212
213 let siblings = if let Some(parent_id) = task.parent_id {
215 sqlx::query_as::<_, Task>(
216 r#"
217 SELECT id, parent_id, name, spec, status, complexity, priority,
218 first_todo_at, first_doing_at, first_done_at, active_form
219 FROM tasks
220 WHERE parent_id = ? AND id != ?
221 ORDER BY priority ASC NULLS LAST, id ASC
222 "#,
223 )
224 .bind(parent_id)
225 .bind(id)
226 .fetch_all(self.pool)
227 .await?
228 } else {
229 sqlx::query_as::<_, Task>(
231 r#"
232 SELECT id, parent_id, name, spec, status, complexity, priority,
233 first_todo_at, first_doing_at, first_done_at, active_form
234 FROM tasks
235 WHERE parent_id IS NULL AND id != ?
236 ORDER BY priority ASC NULLS LAST, id ASC
237 "#,
238 )
239 .bind(id)
240 .fetch_all(self.pool)
241 .await?
242 };
243
244 let children = sqlx::query_as::<_, Task>(
246 r#"
247 SELECT id, parent_id, name, spec, status, complexity, priority,
248 first_todo_at, first_doing_at, first_done_at, active_form
249 FROM tasks
250 WHERE parent_id = ?
251 ORDER BY priority ASC NULLS LAST, id ASC
252 "#,
253 )
254 .bind(id)
255 .fetch_all(self.pool)
256 .await?;
257
258 let blocking_tasks = sqlx::query_as::<_, Task>(
260 r#"
261 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
262 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form
263 FROM tasks t
264 JOIN dependencies d ON t.id = d.blocking_task_id
265 WHERE d.blocked_task_id = ?
266 ORDER BY t.priority ASC NULLS LAST, t.id ASC
267 "#,
268 )
269 .bind(id)
270 .fetch_all(self.pool)
271 .await?;
272
273 let blocked_by_tasks = sqlx::query_as::<_, Task>(
275 r#"
276 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
277 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form
278 FROM tasks t
279 JOIN dependencies d ON t.id = d.blocked_task_id
280 WHERE d.blocking_task_id = ?
281 ORDER BY t.priority ASC NULLS LAST, t.id ASC
282 "#,
283 )
284 .bind(id)
285 .fetch_all(self.pool)
286 .await?;
287
288 Ok(TaskContext {
289 task,
290 ancestors,
291 siblings,
292 children,
293 dependencies: crate::db::models::TaskDependencies {
294 blocking_tasks,
295 blocked_by_tasks,
296 },
297 })
298 }
299
300 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
302 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
303 .bind(task_id)
304 .fetch_one(self.pool)
305 .await?;
306
307 let recent_events = sqlx::query_as::<_, Event>(
308 r#"
309 SELECT id, task_id, timestamp, log_type, discussion_data
310 FROM events
311 WHERE task_id = ?
312 ORDER BY timestamp DESC
313 LIMIT 10
314 "#,
315 )
316 .bind(task_id)
317 .fetch_all(self.pool)
318 .await?;
319
320 Ok(EventsSummary {
321 total_count,
322 recent_events,
323 })
324 }
325
326 #[allow(clippy::too_many_arguments)]
328 pub async fn update_task(
329 &self,
330 id: i64,
331 name: Option<&str>,
332 spec: Option<&str>,
333 parent_id: Option<Option<i64>>,
334 status: Option<&str>,
335 complexity: Option<i32>,
336 priority: Option<i32>,
337 ) -> Result<Task> {
338 let task = self.get_task(id).await?;
340
341 if let Some(s) = status {
343 if !["todo", "doing", "done"].contains(&s) {
344 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
345 }
346 }
347
348 if let Some(Some(pid)) = parent_id {
350 if pid == id {
351 return Err(IntentError::CircularDependency {
352 blocking_task_id: pid,
353 blocked_task_id: id,
354 });
355 }
356 self.check_task_exists(pid).await?;
357 self.check_circular_dependency(id, pid).await?;
358 }
359
360 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
362 sqlx::QueryBuilder::new("UPDATE tasks SET ");
363 let mut has_updates = false;
364
365 if let Some(n) = name {
366 if has_updates {
367 builder.push(", ");
368 }
369 builder.push("name = ").push_bind(n);
370 has_updates = true;
371 }
372
373 if let Some(s) = spec {
374 if has_updates {
375 builder.push(", ");
376 }
377 builder.push("spec = ").push_bind(s);
378 has_updates = true;
379 }
380
381 if let Some(pid) = parent_id {
382 if has_updates {
383 builder.push(", ");
384 }
385 match pid {
386 Some(p) => {
387 builder.push("parent_id = ").push_bind(p);
388 },
389 None => {
390 builder.push("parent_id = NULL");
391 },
392 }
393 has_updates = true;
394 }
395
396 if let Some(c) = complexity {
397 if has_updates {
398 builder.push(", ");
399 }
400 builder.push("complexity = ").push_bind(c);
401 has_updates = true;
402 }
403
404 if let Some(p) = priority {
405 if has_updates {
406 builder.push(", ");
407 }
408 builder.push("priority = ").push_bind(p);
409 has_updates = true;
410 }
411
412 if let Some(s) = status {
413 if has_updates {
414 builder.push(", ");
415 }
416 builder.push("status = ").push_bind(s);
417 has_updates = true;
418
419 let now = Utc::now();
421 let timestamp = now.to_rfc3339();
422 match s {
423 "todo" if task.first_todo_at.is_none() => {
424 builder.push(", first_todo_at = ").push_bind(timestamp);
425 },
426 "doing" if task.first_doing_at.is_none() => {
427 builder.push(", first_doing_at = ").push_bind(timestamp);
428 },
429 "done" if task.first_done_at.is_none() => {
430 builder.push(", first_done_at = ").push_bind(timestamp);
431 },
432 _ => {},
433 }
434 }
435
436 if !has_updates {
437 return Ok(task);
438 }
439
440 builder.push(" WHERE id = ").push_bind(id);
441
442 builder.build().execute(self.pool).await?;
443
444 let task = self.get_task(id).await?;
445
446 self.notify_task_updated(&task).await;
448
449 Ok(task)
450 }
451
452 pub async fn delete_task(&self, id: i64) -> Result<()> {
454 self.check_task_exists(id).await?;
455
456 sqlx::query("DELETE FROM tasks WHERE id = ?")
457 .bind(id)
458 .execute(self.pool)
459 .await?;
460
461 self.notify_task_deleted(id).await;
463
464 Ok(())
465 }
466
467 pub async fn find_tasks(
469 &self,
470 status: Option<&str>,
471 parent_id: Option<Option<i64>>,
472 sort_by: Option<TaskSortBy>,
473 limit: Option<i64>,
474 offset: Option<i64>,
475 ) -> Result<PaginatedTasks> {
476 let sort_by = sort_by.unwrap_or_default(); let limit = limit.unwrap_or(100);
479 let offset = offset.unwrap_or(0);
480
481 let mut where_clause = String::from("WHERE 1=1");
483 let mut conditions = Vec::new();
484
485 if let Some(s) = status {
486 where_clause.push_str(" AND status = ?");
487 conditions.push(s.to_string());
488 }
489
490 if let Some(pid) = parent_id {
491 if let Some(p) = pid {
492 where_clause.push_str(" AND parent_id = ?");
493 conditions.push(p.to_string());
494 } else {
495 where_clause.push_str(" AND parent_id IS NULL");
496 }
497 }
498
499 let order_clause = match sort_by {
501 TaskSortBy::Id => {
502 "ORDER BY id ASC".to_string()
504 },
505 TaskSortBy::Priority => {
506 "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
508 .to_string()
509 },
510 TaskSortBy::Time => {
511 r#"ORDER BY
513 CASE status
514 WHEN 'doing' THEN first_doing_at
515 WHEN 'todo' THEN first_todo_at
516 WHEN 'done' THEN first_done_at
517 END ASC NULLS LAST,
518 id ASC"#
519 .to_string()
520 },
521 TaskSortBy::FocusAware => {
522 r#"ORDER BY
524 CASE
525 WHEN t.id = (SELECT value FROM workspace_state WHERE key = 'current_task_id') THEN 0
526 WHEN t.status = 'doing' THEN 1
527 WHEN t.status = 'todo' THEN 2
528 ELSE 3
529 END ASC,
530 COALESCE(t.priority, 999) ASC,
531 t.id ASC"#
532 .to_string()
533 },
534 };
535
536 let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
538 let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
539 for cond in &conditions {
540 count_q = count_q.bind(cond);
541 }
542 let total_count = count_q.fetch_one(self.pool).await?;
543
544 let main_query = format!(
546 "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form FROM tasks t {} {} LIMIT ? OFFSET ?",
547 where_clause, order_clause
548 );
549
550 let mut q = sqlx::query_as::<_, Task>(&main_query);
551 for cond in conditions {
552 q = q.bind(cond);
553 }
554 q = q.bind(limit);
555 q = q.bind(offset);
556
557 let tasks = q.fetch_all(self.pool).await?;
558
559 let has_more = offset + (tasks.len() as i64) < total_count;
561
562 Ok(PaginatedTasks {
563 tasks,
564 total_count,
565 has_more,
566 limit,
567 offset,
568 })
569 }
570
571 pub async fn get_stats(&self) -> Result<WorkspaceStats> {
576 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
577 r#"SELECT
578 COUNT(*) as total,
579 COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
580 COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
581 COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
582 FROM tasks"#,
583 )
584 .fetch_one(self.pool)
585 .await?;
586
587 Ok(WorkspaceStats {
588 total_tasks: row.0,
589 todo: row.1,
590 doing: row.2,
591 done: row.3,
592 })
593 }
594
595 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
597 use crate::dependencies::get_incomplete_blocking_tasks;
599 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
600 return Err(IntentError::TaskBlocked {
601 task_id: id,
602 blocking_task_ids: blocking_tasks,
603 });
604 }
605
606 let mut tx = self.pool.begin().await?;
607
608 let now = Utc::now();
609
610 sqlx::query(
612 r#"
613 UPDATE tasks
614 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
615 WHERE id = ?
616 "#,
617 )
618 .bind(now)
619 .bind(id)
620 .execute(&mut *tx)
621 .await?;
622
623 sqlx::query(
625 r#"
626 INSERT OR REPLACE INTO workspace_state (key, value)
627 VALUES ('current_task_id', ?)
628 "#,
629 )
630 .bind(id.to_string())
631 .execute(&mut *tx)
632 .await?;
633
634 tx.commit().await?;
635
636 if with_events {
637 let result = self.get_task_with_events(id).await?;
638 self.notify_task_updated(&result.task).await;
639 Ok(result)
640 } else {
641 let task = self.get_task(id).await?;
642 self.notify_task_updated(&task).await;
643 Ok(TaskWithEvents {
644 task,
645 events_summary: None,
646 })
647 }
648 }
649
650 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
654 let mut tx = self.pool.begin().await?;
655
656 let current_task_id: Option<String> =
658 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
659 .fetch_optional(&mut *tx)
660 .await?;
661
662 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
663 IntentError::InvalidInput(
664 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
665 ),
666 )?;
667
668 let task_info: (String, Option<i64>) =
670 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
671 .bind(id)
672 .fetch_one(&mut *tx)
673 .await?;
674 let (task_name, parent_id) = task_info;
675
676 let uncompleted_children: i64 = sqlx::query_scalar(
678 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
679 )
680 .bind(id)
681 .fetch_one(&mut *tx)
682 .await?;
683
684 if uncompleted_children > 0 {
685 return Err(IntentError::UncompletedChildren);
686 }
687
688 let now = Utc::now();
689
690 sqlx::query(
692 r#"
693 UPDATE tasks
694 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
695 WHERE id = ?
696 "#,
697 )
698 .bind(now)
699 .bind(id)
700 .execute(&mut *tx)
701 .await?;
702
703 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
705 .execute(&mut *tx)
706 .await?;
707
708 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
710 let remaining_siblings: i64 = sqlx::query_scalar(
712 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
713 )
714 .bind(parent_task_id)
715 .bind(id)
716 .fetch_one(&mut *tx)
717 .await?;
718
719 if remaining_siblings == 0 {
720 let parent_name: String =
722 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
723 .bind(parent_task_id)
724 .fetch_one(&mut *tx)
725 .await?;
726
727 NextStepSuggestion::ParentIsReady {
728 message: format!(
729 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
730 parent_task_id, parent_name
731 ),
732 parent_task_id,
733 parent_task_name: parent_name,
734 }
735 } else {
736 let parent_name: String =
738 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
739 .bind(parent_task_id)
740 .fetch_one(&mut *tx)
741 .await?;
742
743 NextStepSuggestion::SiblingTasksRemain {
744 message: format!(
745 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
746 id, parent_task_id, parent_name
747 ),
748 parent_task_id,
749 parent_task_name: parent_name,
750 remaining_siblings_count: remaining_siblings,
751 }
752 }
753 } else {
754 let child_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_CHILDREN_TOTAL)
756 .bind(id)
757 .fetch_one(&mut *tx)
758 .await?;
759
760 if child_count > 0 {
761 NextStepSuggestion::TopLevelTaskCompleted {
763 message: format!(
764 "Top-level task #{} '{}' has been completed. Well done!",
765 id, task_name
766 ),
767 completed_task_id: id,
768 completed_task_name: task_name.clone(),
769 }
770 } else {
771 let remaining_tasks: i64 = sqlx::query_scalar(
773 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
774 )
775 .bind(id)
776 .fetch_one(&mut *tx)
777 .await?;
778
779 if remaining_tasks == 0 {
780 NextStepSuggestion::WorkspaceIsClear {
781 message: format!(
782 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
783 id
784 ),
785 completed_task_id: id,
786 }
787 } else {
788 NextStepSuggestion::NoParentContext {
789 message: format!("Task #{} '{}' has been completed.", id, task_name),
790 completed_task_id: id,
791 completed_task_name: task_name.clone(),
792 }
793 }
794 }
795 };
796
797 tx.commit().await?;
798
799 let completed_task = self.get_task(id).await?;
801 self.notify_task_updated(&completed_task).await;
802
803 Ok(DoneTaskResponse {
804 completed_task,
805 workspace_status: WorkspaceStatus {
806 current_task_id: None,
807 },
808 next_step_suggestion,
809 })
810 }
811
812 async fn check_task_exists(&self, id: i64) -> Result<()> {
814 let exists: bool = sqlx::query_scalar(crate::sql_constants::CHECK_TASK_EXISTS)
815 .bind(id)
816 .fetch_one(self.pool)
817 .await?;
818
819 if !exists {
820 return Err(IntentError::TaskNotFound(id));
821 }
822
823 Ok(())
824 }
825
826 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
828 let mut current_id = new_parent_id;
829
830 loop {
831 if current_id == task_id {
832 return Err(IntentError::CircularDependency {
833 blocking_task_id: new_parent_id,
834 blocked_task_id: task_id,
835 });
836 }
837
838 let parent: Option<i64> =
839 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_PARENT_ID)
840 .bind(current_id)
841 .fetch_optional(self.pool)
842 .await?;
843
844 match parent {
845 Some(pid) => current_id = pid,
846 None => break,
847 }
848 }
849
850 Ok(())
851 }
852 pub async fn spawn_subtask(
856 &self,
857 name: &str,
858 spec: Option<&str>,
859 ) -> Result<SpawnSubtaskResponse> {
860 let current_task_id: Option<String> =
862 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
863 .fetch_optional(self.pool)
864 .await?;
865
866 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
867 IntentError::InvalidInput("No current task to create subtask under".to_string()),
868 )?;
869
870 let parent_name: String = sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
872 .bind(parent_id)
873 .fetch_one(self.pool)
874 .await?;
875
876 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
878
879 self.start_task(subtask.id, false).await?;
882
883 Ok(SpawnSubtaskResponse {
884 subtask: SubtaskInfo {
885 id: subtask.id,
886 name: subtask.name,
887 parent_id,
888 status: "doing".to_string(),
889 },
890 parent_task: ParentTaskInfo {
891 id: parent_id,
892 name: parent_name,
893 },
894 })
895 }
896
897 pub async fn pick_next_tasks(
910 &self,
911 max_count: usize,
912 capacity_limit: usize,
913 ) -> Result<Vec<Task>> {
914 let mut tx = self.pool.begin().await?;
915
916 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
918 .fetch_one(&mut *tx)
919 .await?;
920
921 let available = capacity_limit.saturating_sub(doing_count as usize);
923 if available == 0 {
924 return Ok(vec![]);
925 }
926
927 let limit = std::cmp::min(max_count, available);
928
929 let todo_tasks = sqlx::query_as::<_, Task>(
931 r#"
932 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
933 FROM tasks
934 WHERE status = 'todo'
935 ORDER BY
936 COALESCE(priority, 0) ASC,
937 COALESCE(complexity, 5) ASC,
938 id ASC
939 LIMIT ?
940 "#,
941 )
942 .bind(limit as i64)
943 .fetch_all(&mut *tx)
944 .await?;
945
946 if todo_tasks.is_empty() {
947 return Ok(vec![]);
948 }
949
950 let now = Utc::now();
951
952 for task in &todo_tasks {
954 sqlx::query(
955 r#"
956 UPDATE tasks
957 SET status = 'doing',
958 first_doing_at = COALESCE(first_doing_at, ?)
959 WHERE id = ?
960 "#,
961 )
962 .bind(now)
963 .bind(task.id)
964 .execute(&mut *tx)
965 .await?;
966 }
967
968 tx.commit().await?;
969
970 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
972 let placeholders = vec!["?"; task_ids.len()].join(",");
973 let query = format!(
974 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
975 FROM tasks WHERE id IN ({})
976 ORDER BY
977 COALESCE(priority, 0) ASC,
978 COALESCE(complexity, 5) ASC,
979 id ASC",
980 placeholders
981 );
982
983 let mut q = sqlx::query_as::<_, Task>(&query);
984 for id in task_ids {
985 q = q.bind(id);
986 }
987
988 let updated_tasks = q.fetch_all(self.pool).await?;
989 Ok(updated_tasks)
990 }
991
992 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1001 let current_task_id: Option<String> =
1003 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1004 .fetch_optional(self.pool)
1005 .await?;
1006
1007 if let Some(current_id_str) = current_task_id.as_ref() {
1008 if let Ok(current_id) = current_id_str.parse::<i64>() {
1009 let doing_subtasks = sqlx::query_as::<_, Task>(
1012 r#"
1013 SELECT id, parent_id, name, spec, status, complexity, priority,
1014 first_todo_at, first_doing_at, first_done_at, active_form
1015 FROM tasks
1016 WHERE parent_id = ? AND status = 'doing'
1017 AND NOT EXISTS (
1018 SELECT 1 FROM dependencies d
1019 JOIN tasks bt ON d.blocking_task_id = bt.id
1020 WHERE d.blocked_task_id = tasks.id
1021 AND bt.status != 'done'
1022 )
1023 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1024 LIMIT 1
1025 "#,
1026 )
1027 .bind(current_id)
1028 .fetch_optional(self.pool)
1029 .await?;
1030
1031 if let Some(task) = doing_subtasks {
1032 return Ok(PickNextResponse::focused_subtask(task));
1033 }
1034
1035 let todo_subtasks = sqlx::query_as::<_, Task>(
1037 r#"
1038 SELECT id, parent_id, name, spec, status, complexity, priority,
1039 first_todo_at, first_doing_at, first_done_at, active_form
1040 FROM tasks
1041 WHERE parent_id = ? AND status = 'todo'
1042 AND NOT EXISTS (
1043 SELECT 1 FROM dependencies d
1044 JOIN tasks bt ON d.blocking_task_id = bt.id
1045 WHERE d.blocked_task_id = tasks.id
1046 AND bt.status != 'done'
1047 )
1048 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1049 LIMIT 1
1050 "#,
1051 )
1052 .bind(current_id)
1053 .fetch_optional(self.pool)
1054 .await?;
1055
1056 if let Some(task) = todo_subtasks {
1057 return Ok(PickNextResponse::focused_subtask(task));
1058 }
1059 }
1060 }
1061
1062 let doing_top_level = if let Some(current_id_str) = current_task_id.as_ref() {
1065 if let Ok(current_id) = current_id_str.parse::<i64>() {
1066 sqlx::query_as::<_, Task>(
1067 r#"
1068 SELECT id, parent_id, name, spec, status, complexity, priority,
1069 first_todo_at, first_doing_at, first_done_at, active_form
1070 FROM tasks
1071 WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1072 AND NOT EXISTS (
1073 SELECT 1 FROM dependencies d
1074 JOIN tasks bt ON d.blocking_task_id = bt.id
1075 WHERE d.blocked_task_id = tasks.id
1076 AND bt.status != 'done'
1077 )
1078 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1079 LIMIT 1
1080 "#,
1081 )
1082 .bind(current_id)
1083 .fetch_optional(self.pool)
1084 .await?
1085 } else {
1086 None
1087 }
1088 } else {
1089 sqlx::query_as::<_, Task>(
1090 r#"
1091 SELECT id, parent_id, name, spec, status, complexity, priority,
1092 first_todo_at, first_doing_at, first_done_at, active_form
1093 FROM tasks
1094 WHERE parent_id IS NULL AND status = 'doing'
1095 AND NOT EXISTS (
1096 SELECT 1 FROM dependencies d
1097 JOIN tasks bt ON d.blocking_task_id = bt.id
1098 WHERE d.blocked_task_id = tasks.id
1099 AND bt.status != 'done'
1100 )
1101 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1102 LIMIT 1
1103 "#,
1104 )
1105 .fetch_optional(self.pool)
1106 .await?
1107 };
1108
1109 if let Some(task) = doing_top_level {
1110 return Ok(PickNextResponse::top_level_task(task));
1111 }
1112
1113 let todo_top_level = sqlx::query_as::<_, Task>(
1116 r#"
1117 SELECT id, parent_id, name, spec, status, complexity, priority,
1118 first_todo_at, first_doing_at, first_done_at, active_form
1119 FROM tasks
1120 WHERE parent_id IS NULL AND status = 'todo'
1121 AND NOT EXISTS (
1122 SELECT 1 FROM dependencies d
1123 JOIN tasks bt ON d.blocking_task_id = bt.id
1124 WHERE d.blocked_task_id = tasks.id
1125 AND bt.status != 'done'
1126 )
1127 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1128 LIMIT 1
1129 "#,
1130 )
1131 .fetch_optional(self.pool)
1132 .await?;
1133
1134 if let Some(task) = todo_top_level {
1135 return Ok(PickNextResponse::top_level_task(task));
1136 }
1137
1138 let total_tasks: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_TOTAL)
1141 .fetch_one(self.pool)
1142 .await?;
1143
1144 if total_tasks == 0 {
1145 return Ok(PickNextResponse::no_tasks_in_project());
1146 }
1147
1148 let todo_or_doing_count: i64 =
1150 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1151 .fetch_one(self.pool)
1152 .await?;
1153
1154 if todo_or_doing_count == 0 {
1155 return Ok(PickNextResponse::all_tasks_completed());
1156 }
1157
1158 Ok(PickNextResponse::no_available_todos())
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use crate::events::EventManager;
1167 use crate::test_utils::test_helpers::TestContext;
1168 use crate::workspace::WorkspaceManager;
1169
1170 #[tokio::test]
1171 async fn test_get_stats_empty() {
1172 let ctx = TestContext::new().await;
1173 let manager = TaskManager::new(ctx.pool());
1174
1175 let stats = manager.get_stats().await.unwrap();
1176
1177 assert_eq!(stats.total_tasks, 0);
1178 assert_eq!(stats.todo, 0);
1179 assert_eq!(stats.doing, 0);
1180 assert_eq!(stats.done, 0);
1181 }
1182
1183 #[tokio::test]
1184 async fn test_get_stats_with_tasks() {
1185 let ctx = TestContext::new().await;
1186 let manager = TaskManager::new(ctx.pool());
1187
1188 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1190 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1191 let _task3 = manager.add_task("Task 3", None, None).await.unwrap();
1192
1193 manager
1195 .update_task(task1.id, None, None, None, Some("doing"), None, None)
1196 .await
1197 .unwrap();
1198 manager
1199 .update_task(task2.id, None, None, None, Some("done"), None, None)
1200 .await
1201 .unwrap();
1202 let stats = manager.get_stats().await.unwrap();
1205
1206 assert_eq!(stats.total_tasks, 3);
1207 assert_eq!(stats.todo, 1);
1208 assert_eq!(stats.doing, 1);
1209 assert_eq!(stats.done, 1);
1210 }
1211
1212 #[tokio::test]
1213 async fn test_add_task() {
1214 let ctx = TestContext::new().await;
1215 let manager = TaskManager::new(ctx.pool());
1216
1217 let task = manager.add_task("Test task", None, None).await.unwrap();
1218
1219 assert_eq!(task.name, "Test task");
1220 assert_eq!(task.status, "todo");
1221 assert!(task.first_todo_at.is_some());
1222 assert!(task.first_doing_at.is_none());
1223 assert!(task.first_done_at.is_none());
1224 }
1225
1226 #[tokio::test]
1227 async fn test_add_task_with_spec() {
1228 let ctx = TestContext::new().await;
1229 let manager = TaskManager::new(ctx.pool());
1230
1231 let spec = "This is a task specification";
1232 let task = manager
1233 .add_task("Test task", Some(spec), None)
1234 .await
1235 .unwrap();
1236
1237 assert_eq!(task.name, "Test task");
1238 assert_eq!(task.spec.as_deref(), Some(spec));
1239 }
1240
1241 #[tokio::test]
1242 async fn test_add_task_with_parent() {
1243 let ctx = TestContext::new().await;
1244 let manager = TaskManager::new(ctx.pool());
1245
1246 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1247 let child = manager
1248 .add_task("Child task", None, Some(parent.id))
1249 .await
1250 .unwrap();
1251
1252 assert_eq!(child.parent_id, Some(parent.id));
1253 }
1254
1255 #[tokio::test]
1256 async fn test_get_task() {
1257 let ctx = TestContext::new().await;
1258 let manager = TaskManager::new(ctx.pool());
1259
1260 let created = manager.add_task("Test task", None, None).await.unwrap();
1261 let retrieved = manager.get_task(created.id).await.unwrap();
1262
1263 assert_eq!(created.id, retrieved.id);
1264 assert_eq!(created.name, retrieved.name);
1265 }
1266
1267 #[tokio::test]
1268 async fn test_get_task_not_found() {
1269 let ctx = TestContext::new().await;
1270 let manager = TaskManager::new(ctx.pool());
1271
1272 let result = manager.get_task(999).await;
1273 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1274 }
1275
1276 #[tokio::test]
1277 async fn test_update_task_name() {
1278 let ctx = TestContext::new().await;
1279 let manager = TaskManager::new(ctx.pool());
1280
1281 let task = manager.add_task("Original name", None, None).await.unwrap();
1282 let updated = manager
1283 .update_task(task.id, Some("New name"), None, None, None, None, None)
1284 .await
1285 .unwrap();
1286
1287 assert_eq!(updated.name, "New name");
1288 }
1289
1290 #[tokio::test]
1291 async fn test_update_task_status() {
1292 let ctx = TestContext::new().await;
1293 let manager = TaskManager::new(ctx.pool());
1294
1295 let task = manager.add_task("Test task", None, None).await.unwrap();
1296 let updated = manager
1297 .update_task(task.id, None, None, None, Some("doing"), None, None)
1298 .await
1299 .unwrap();
1300
1301 assert_eq!(updated.status, "doing");
1302 assert!(updated.first_doing_at.is_some());
1303 }
1304
1305 #[tokio::test]
1306 async fn test_delete_task() {
1307 let ctx = TestContext::new().await;
1308 let manager = TaskManager::new(ctx.pool());
1309
1310 let task = manager.add_task("Test task", None, None).await.unwrap();
1311 manager.delete_task(task.id).await.unwrap();
1312
1313 let result = manager.get_task(task.id).await;
1314 assert!(result.is_err());
1315 }
1316
1317 #[tokio::test]
1318 async fn test_find_tasks_by_status() {
1319 let ctx = TestContext::new().await;
1320 let manager = TaskManager::new(ctx.pool());
1321
1322 manager.add_task("Todo task", None, None).await.unwrap();
1323 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1324 manager
1325 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1326 .await
1327 .unwrap();
1328
1329 let todo_result = manager
1330 .find_tasks(Some("todo"), None, None, None, None)
1331 .await
1332 .unwrap();
1333 let doing_result = manager
1334 .find_tasks(Some("doing"), None, None, None, None)
1335 .await
1336 .unwrap();
1337
1338 assert_eq!(todo_result.tasks.len(), 1);
1339 assert_eq!(doing_result.tasks.len(), 1);
1340 assert_eq!(doing_result.tasks[0].status, "doing");
1341 }
1342
1343 #[tokio::test]
1344 async fn test_find_tasks_by_parent() {
1345 let ctx = TestContext::new().await;
1346 let manager = TaskManager::new(ctx.pool());
1347
1348 let parent = manager.add_task("Parent", None, None).await.unwrap();
1349 manager
1350 .add_task("Child 1", None, Some(parent.id))
1351 .await
1352 .unwrap();
1353 manager
1354 .add_task("Child 2", None, Some(parent.id))
1355 .await
1356 .unwrap();
1357
1358 let result = manager
1359 .find_tasks(None, Some(Some(parent.id)), None, None, None)
1360 .await
1361 .unwrap();
1362
1363 assert_eq!(result.tasks.len(), 2);
1364 }
1365
1366 #[tokio::test]
1367 async fn test_start_task() {
1368 let ctx = TestContext::new().await;
1369 let manager = TaskManager::new(ctx.pool());
1370
1371 let task = manager.add_task("Test task", None, None).await.unwrap();
1372 let started = manager.start_task(task.id, false).await.unwrap();
1373
1374 assert_eq!(started.task.status, "doing");
1375 assert!(started.task.first_doing_at.is_some());
1376
1377 let current: Option<String> =
1379 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1380 .fetch_optional(ctx.pool())
1381 .await
1382 .unwrap();
1383
1384 assert_eq!(current, Some(task.id.to_string()));
1385 }
1386
1387 #[tokio::test]
1388 async fn test_start_task_with_events() {
1389 let ctx = TestContext::new().await;
1390 let manager = TaskManager::new(ctx.pool());
1391
1392 let task = manager.add_task("Test task", None, None).await.unwrap();
1393
1394 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1396 .bind(task.id)
1397 .bind("test")
1398 .bind("test event")
1399 .execute(ctx.pool())
1400 .await
1401 .unwrap();
1402
1403 let started = manager.start_task(task.id, true).await.unwrap();
1404
1405 assert!(started.events_summary.is_some());
1406 let summary = started.events_summary.unwrap();
1407 assert_eq!(summary.total_count, 1);
1408 }
1409
1410 #[tokio::test]
1411 async fn test_done_task() {
1412 let ctx = TestContext::new().await;
1413 let manager = TaskManager::new(ctx.pool());
1414
1415 let task = manager.add_task("Test task", None, None).await.unwrap();
1416 manager.start_task(task.id, false).await.unwrap();
1417 let response = manager.done_task().await.unwrap();
1418
1419 assert_eq!(response.completed_task.status, "done");
1420 assert!(response.completed_task.first_done_at.is_some());
1421 assert_eq!(response.workspace_status.current_task_id, None);
1422
1423 match response.next_step_suggestion {
1425 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1426 _ => panic!("Expected WorkspaceIsClear suggestion"),
1427 }
1428
1429 let current: Option<String> =
1431 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1432 .fetch_optional(ctx.pool())
1433 .await
1434 .unwrap();
1435
1436 assert!(current.is_none());
1437 }
1438
1439 #[tokio::test]
1440 async fn test_done_task_with_uncompleted_children() {
1441 let ctx = TestContext::new().await;
1442 let manager = TaskManager::new(ctx.pool());
1443
1444 let parent = manager.add_task("Parent", None, None).await.unwrap();
1445 manager
1446 .add_task("Child", None, Some(parent.id))
1447 .await
1448 .unwrap();
1449
1450 manager.start_task(parent.id, false).await.unwrap();
1452
1453 let result = manager.done_task().await;
1454 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1455 }
1456
1457 #[tokio::test]
1458 async fn test_done_task_with_completed_children() {
1459 let ctx = TestContext::new().await;
1460 let manager = TaskManager::new(ctx.pool());
1461
1462 let parent = manager.add_task("Parent", None, None).await.unwrap();
1463 let child = manager
1464 .add_task("Child", None, Some(parent.id))
1465 .await
1466 .unwrap();
1467
1468 manager.start_task(child.id, false).await.unwrap();
1470 let child_response = manager.done_task().await.unwrap();
1471
1472 match child_response.next_step_suggestion {
1474 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1475 assert_eq!(parent_task_id, parent.id);
1476 },
1477 _ => panic!("Expected ParentIsReady suggestion"),
1478 }
1479
1480 manager.start_task(parent.id, false).await.unwrap();
1482 let parent_response = manager.done_task().await.unwrap();
1483 assert_eq!(parent_response.completed_task.status, "done");
1484
1485 match parent_response.next_step_suggestion {
1487 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1488 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1489 }
1490 }
1491
1492 #[tokio::test]
1493 async fn test_circular_dependency() {
1494 let ctx = TestContext::new().await;
1495 let manager = TaskManager::new(ctx.pool());
1496
1497 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1498 let task2 = manager
1499 .add_task("Task 2", None, Some(task1.id))
1500 .await
1501 .unwrap();
1502
1503 let result = manager
1505 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1506 .await;
1507
1508 assert!(matches!(
1509 result,
1510 Err(IntentError::CircularDependency { .. })
1511 ));
1512 }
1513
1514 #[tokio::test]
1515 async fn test_invalid_parent_id() {
1516 let ctx = TestContext::new().await;
1517 let manager = TaskManager::new(ctx.pool());
1518
1519 let result = manager.add_task("Test", None, Some(999)).await;
1520 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1521 }
1522
1523 #[tokio::test]
1524 async fn test_update_task_complexity_and_priority() {
1525 let ctx = TestContext::new().await;
1526 let manager = TaskManager::new(ctx.pool());
1527
1528 let task = manager.add_task("Test task", None, None).await.unwrap();
1529 let updated = manager
1530 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1531 .await
1532 .unwrap();
1533
1534 assert_eq!(updated.complexity, Some(8));
1535 assert_eq!(updated.priority, Some(10));
1536 }
1537
1538 #[tokio::test]
1539 async fn test_spawn_subtask() {
1540 let ctx = TestContext::new().await;
1541 let manager = TaskManager::new(ctx.pool());
1542
1543 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1545 manager.start_task(parent.id, false).await.unwrap();
1546
1547 let response = manager
1549 .spawn_subtask("Child task", Some("Details"))
1550 .await
1551 .unwrap();
1552
1553 assert_eq!(response.subtask.parent_id, parent.id);
1554 assert_eq!(response.subtask.name, "Child task");
1555 assert_eq!(response.subtask.status, "doing");
1556 assert_eq!(response.parent_task.id, parent.id);
1557 assert_eq!(response.parent_task.name, "Parent task");
1558
1559 let current: Option<String> =
1561 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1562 .fetch_optional(ctx.pool())
1563 .await
1564 .unwrap();
1565
1566 assert_eq!(current, Some(response.subtask.id.to_string()));
1567
1568 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1570 assert_eq!(retrieved.status, "doing");
1571 }
1572
1573 #[tokio::test]
1574 async fn test_spawn_subtask_no_current_task() {
1575 let ctx = TestContext::new().await;
1576 let manager = TaskManager::new(ctx.pool());
1577
1578 let result = manager.spawn_subtask("Child", None).await;
1580 assert!(result.is_err());
1581 }
1582
1583 #[tokio::test]
1584 async fn test_pick_next_tasks_basic() {
1585 let ctx = TestContext::new().await;
1586 let manager = TaskManager::new(ctx.pool());
1587
1588 for i in 1..=10 {
1590 manager
1591 .add_task(&format!("Task {}", i), None, None)
1592 .await
1593 .unwrap();
1594 }
1595
1596 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1598
1599 assert_eq!(picked.len(), 5);
1600 for task in &picked {
1601 assert_eq!(task.status, "doing");
1602 assert!(task.first_doing_at.is_some());
1603 }
1604
1605 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1607 .fetch_one(ctx.pool())
1608 .await
1609 .unwrap();
1610
1611 assert_eq!(doing_count, 5);
1612 }
1613
1614 #[tokio::test]
1615 async fn test_pick_next_tasks_with_existing_doing() {
1616 let ctx = TestContext::new().await;
1617 let manager = TaskManager::new(ctx.pool());
1618
1619 for i in 1..=10 {
1621 manager
1622 .add_task(&format!("Task {}", i), None, None)
1623 .await
1624 .unwrap();
1625 }
1626
1627 let result = manager
1629 .find_tasks(Some("todo"), None, None, None, None)
1630 .await
1631 .unwrap();
1632 manager.start_task(result.tasks[0].id, false).await.unwrap();
1633 manager.start_task(result.tasks[1].id, false).await.unwrap();
1634
1635 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1637
1638 assert_eq!(picked.len(), 3);
1640
1641 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1643 .fetch_one(ctx.pool())
1644 .await
1645 .unwrap();
1646
1647 assert_eq!(doing_count, 5);
1648 }
1649
1650 #[tokio::test]
1651 async fn test_pick_next_tasks_at_capacity() {
1652 let ctx = TestContext::new().await;
1653 let manager = TaskManager::new(ctx.pool());
1654
1655 for i in 1..=10 {
1657 manager
1658 .add_task(&format!("Task {}", i), None, None)
1659 .await
1660 .unwrap();
1661 }
1662
1663 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1665 assert_eq!(first_batch.len(), 5);
1666
1667 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1669 assert_eq!(second_batch.len(), 0);
1670 }
1671
1672 #[tokio::test]
1673 async fn test_pick_next_tasks_priority_ordering() {
1674 let ctx = TestContext::new().await;
1675 let manager = TaskManager::new(ctx.pool());
1676
1677 let low = manager.add_task("Low priority", None, None).await.unwrap();
1679 manager
1680 .update_task(low.id, None, None, None, None, None, Some(1))
1681 .await
1682 .unwrap();
1683
1684 let high = manager.add_task("High priority", None, None).await.unwrap();
1685 manager
1686 .update_task(high.id, None, None, None, None, None, Some(10))
1687 .await
1688 .unwrap();
1689
1690 let medium = manager
1691 .add_task("Medium priority", None, None)
1692 .await
1693 .unwrap();
1694 manager
1695 .update_task(medium.id, None, None, None, None, None, Some(5))
1696 .await
1697 .unwrap();
1698
1699 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1701
1702 assert_eq!(picked.len(), 3);
1704 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
1708
1709 #[tokio::test]
1710 async fn test_pick_next_tasks_complexity_ordering() {
1711 let ctx = TestContext::new().await;
1712 let manager = TaskManager::new(ctx.pool());
1713
1714 let complex = manager.add_task("Complex", None, None).await.unwrap();
1716 manager
1717 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1718 .await
1719 .unwrap();
1720
1721 let simple = manager.add_task("Simple", None, None).await.unwrap();
1722 manager
1723 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1724 .await
1725 .unwrap();
1726
1727 let medium = manager.add_task("Medium", None, None).await.unwrap();
1728 manager
1729 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1730 .await
1731 .unwrap();
1732
1733 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1735
1736 assert_eq!(picked.len(), 3);
1738 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1742
1743 #[tokio::test]
1744 async fn test_done_task_sibling_tasks_remain() {
1745 let ctx = TestContext::new().await;
1746 let manager = TaskManager::new(ctx.pool());
1747
1748 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1750 let child1 = manager
1751 .add_task("Child 1", None, Some(parent.id))
1752 .await
1753 .unwrap();
1754 let child2 = manager
1755 .add_task("Child 2", None, Some(parent.id))
1756 .await
1757 .unwrap();
1758 let _child3 = manager
1759 .add_task("Child 3", None, Some(parent.id))
1760 .await
1761 .unwrap();
1762
1763 manager.start_task(child1.id, false).await.unwrap();
1765 let response = manager.done_task().await.unwrap();
1766
1767 match response.next_step_suggestion {
1769 NextStepSuggestion::SiblingTasksRemain {
1770 parent_task_id,
1771 remaining_siblings_count,
1772 ..
1773 } => {
1774 assert_eq!(parent_task_id, parent.id);
1775 assert_eq!(remaining_siblings_count, 2); },
1777 _ => panic!("Expected SiblingTasksRemain suggestion"),
1778 }
1779
1780 manager.start_task(child2.id, false).await.unwrap();
1782 let response2 = manager.done_task().await.unwrap();
1783
1784 match response2.next_step_suggestion {
1786 NextStepSuggestion::SiblingTasksRemain {
1787 remaining_siblings_count,
1788 ..
1789 } => {
1790 assert_eq!(remaining_siblings_count, 1); },
1792 _ => panic!("Expected SiblingTasksRemain suggestion"),
1793 }
1794 }
1795
1796 #[tokio::test]
1797 async fn test_done_task_top_level_with_children() {
1798 let ctx = TestContext::new().await;
1799 let manager = TaskManager::new(ctx.pool());
1800
1801 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1803 let child = manager
1804 .add_task("Sub Task", None, Some(parent.id))
1805 .await
1806 .unwrap();
1807
1808 manager.start_task(child.id, false).await.unwrap();
1810 manager.done_task().await.unwrap();
1811
1812 manager.start_task(parent.id, false).await.unwrap();
1814 let response = manager.done_task().await.unwrap();
1815
1816 match response.next_step_suggestion {
1818 NextStepSuggestion::TopLevelTaskCompleted {
1819 completed_task_id,
1820 completed_task_name,
1821 ..
1822 } => {
1823 assert_eq!(completed_task_id, parent.id);
1824 assert_eq!(completed_task_name, "Epic Task");
1825 },
1826 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1827 }
1828 }
1829
1830 #[tokio::test]
1831 async fn test_done_task_no_parent_context() {
1832 let ctx = TestContext::new().await;
1833 let manager = TaskManager::new(ctx.pool());
1834
1835 let task1 = manager
1837 .add_task("Standalone Task 1", None, None)
1838 .await
1839 .unwrap();
1840 let _task2 = manager
1841 .add_task("Standalone Task 2", None, None)
1842 .await
1843 .unwrap();
1844
1845 manager.start_task(task1.id, false).await.unwrap();
1847 let response = manager.done_task().await.unwrap();
1848
1849 match response.next_step_suggestion {
1851 NextStepSuggestion::NoParentContext {
1852 completed_task_id,
1853 completed_task_name,
1854 ..
1855 } => {
1856 assert_eq!(completed_task_id, task1.id);
1857 assert_eq!(completed_task_name, "Standalone Task 1");
1858 },
1859 _ => panic!("Expected NoParentContext suggestion"),
1860 }
1861 }
1862
1863 #[tokio::test]
1864 async fn test_pick_next_focused_subtask() {
1865 let ctx = TestContext::new().await;
1866 let manager = TaskManager::new(ctx.pool());
1867
1868 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1870 manager.start_task(parent.id, false).await.unwrap();
1871
1872 let subtask1 = manager
1874 .add_task("Subtask 1", None, Some(parent.id))
1875 .await
1876 .unwrap();
1877 let subtask2 = manager
1878 .add_task("Subtask 2", None, Some(parent.id))
1879 .await
1880 .unwrap();
1881
1882 manager
1884 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1885 .await
1886 .unwrap();
1887 manager
1888 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1889 .await
1890 .unwrap();
1891
1892 let response = manager.pick_next().await.unwrap();
1894
1895 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1896 assert!(response.task.is_some());
1897 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1898 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1899 }
1900
1901 #[tokio::test]
1902 async fn test_pick_next_top_level_task() {
1903 let ctx = TestContext::new().await;
1904 let manager = TaskManager::new(ctx.pool());
1905
1906 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1908 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1909
1910 manager
1912 .update_task(task1.id, None, None, None, None, None, Some(5))
1913 .await
1914 .unwrap();
1915 manager
1916 .update_task(task2.id, None, None, None, None, None, Some(3))
1917 .await
1918 .unwrap();
1919
1920 let response = manager.pick_next().await.unwrap();
1922
1923 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1924 assert!(response.task.is_some());
1925 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1926 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1927 }
1928
1929 #[tokio::test]
1930 async fn test_pick_next_no_tasks() {
1931 let ctx = TestContext::new().await;
1932 let manager = TaskManager::new(ctx.pool());
1933
1934 let response = manager.pick_next().await.unwrap();
1936
1937 assert_eq!(response.suggestion_type, "NONE");
1938 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1939 assert!(response.message.is_some());
1940 }
1941
1942 #[tokio::test]
1943 async fn test_pick_next_all_completed() {
1944 let ctx = TestContext::new().await;
1945 let manager = TaskManager::new(ctx.pool());
1946
1947 let task = manager.add_task("Task 1", None, None).await.unwrap();
1949 manager.start_task(task.id, false).await.unwrap();
1950 manager.done_task().await.unwrap();
1951
1952 let response = manager.pick_next().await.unwrap();
1954
1955 assert_eq!(response.suggestion_type, "NONE");
1956 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1957 assert!(response.message.is_some());
1958 }
1959
1960 #[tokio::test]
1961 async fn test_pick_next_no_available_todos() {
1962 let ctx = TestContext::new().await;
1963 let manager = TaskManager::new(ctx.pool());
1964
1965 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1967 manager.start_task(parent.id, false).await.unwrap();
1968
1969 let subtask = manager
1971 .add_task("Subtask", None, Some(parent.id))
1972 .await
1973 .unwrap();
1974 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1976 .bind(subtask.id)
1977 .execute(ctx.pool())
1978 .await
1979 .unwrap();
1980
1981 sqlx::query(
1983 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
1984 )
1985 .bind(subtask.id.to_string())
1986 .execute(ctx.pool())
1987 .await
1988 .unwrap();
1989
1990 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1992 .bind(parent.id)
1993 .execute(ctx.pool())
1994 .await
1995 .unwrap();
1996
1997 let response = manager.pick_next().await.unwrap();
2000
2001 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2002 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2003 assert_eq!(response.task.as_ref().unwrap().status, "doing");
2004 }
2005
2006 #[tokio::test]
2007 async fn test_pick_next_priority_ordering() {
2008 let ctx = TestContext::new().await;
2009 let manager = TaskManager::new(ctx.pool());
2010
2011 let parent = manager.add_task("Parent", None, None).await.unwrap();
2013 manager.start_task(parent.id, false).await.unwrap();
2014
2015 let sub1 = manager
2017 .add_task("Priority 10", None, Some(parent.id))
2018 .await
2019 .unwrap();
2020 manager
2021 .update_task(sub1.id, None, None, None, None, None, Some(10))
2022 .await
2023 .unwrap();
2024
2025 let sub2 = manager
2026 .add_task("Priority 1", None, Some(parent.id))
2027 .await
2028 .unwrap();
2029 manager
2030 .update_task(sub2.id, None, None, None, None, None, Some(1))
2031 .await
2032 .unwrap();
2033
2034 let sub3 = manager
2035 .add_task("Priority 5", None, Some(parent.id))
2036 .await
2037 .unwrap();
2038 manager
2039 .update_task(sub3.id, None, None, None, None, None, Some(5))
2040 .await
2041 .unwrap();
2042
2043 let response = manager.pick_next().await.unwrap();
2045
2046 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2047 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2048 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2049 }
2050
2051 #[tokio::test]
2052 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2053 let ctx = TestContext::new().await;
2054 let manager = TaskManager::new(ctx.pool());
2055
2056 let parent = manager.add_task("Parent", None, None).await.unwrap();
2058 manager.start_task(parent.id, false).await.unwrap();
2059
2060 let top_level = manager
2062 .add_task("Top level task", None, None)
2063 .await
2064 .unwrap();
2065
2066 let response = manager.pick_next().await.unwrap();
2068
2069 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2070 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2071 }
2072
2073 #[tokio::test]
2076 async fn test_get_task_with_events() {
2077 let ctx = TestContext::new().await;
2078 let task_mgr = TaskManager::new(ctx.pool());
2079 let event_mgr = EventManager::new(ctx.pool());
2080
2081 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2082
2083 event_mgr
2085 .add_event(task.id, "progress", "Event 1")
2086 .await
2087 .unwrap();
2088 event_mgr
2089 .add_event(task.id, "decision", "Event 2")
2090 .await
2091 .unwrap();
2092
2093 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2094
2095 assert_eq!(result.task.id, task.id);
2096 assert!(result.events_summary.is_some());
2097
2098 let summary = result.events_summary.unwrap();
2099 assert_eq!(summary.total_count, 2);
2100 assert_eq!(summary.recent_events.len(), 2);
2101 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2103 }
2104
2105 #[tokio::test]
2106 async fn test_get_task_with_events_nonexistent() {
2107 let ctx = TestContext::new().await;
2108 let task_mgr = TaskManager::new(ctx.pool());
2109
2110 let result = task_mgr.get_task_with_events(999).await;
2111 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2112 }
2113
2114 #[tokio::test]
2115 async fn test_get_task_with_many_events() {
2116 let ctx = TestContext::new().await;
2117 let task_mgr = TaskManager::new(ctx.pool());
2118 let event_mgr = EventManager::new(ctx.pool());
2119
2120 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2121
2122 for i in 0..20 {
2124 event_mgr
2125 .add_event(task.id, "test", &format!("Event {}", i))
2126 .await
2127 .unwrap();
2128 }
2129
2130 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2131 let summary = result.events_summary.unwrap();
2132
2133 assert_eq!(summary.total_count, 20);
2134 assert_eq!(summary.recent_events.len(), 10); }
2136
2137 #[tokio::test]
2138 async fn test_get_task_with_no_events() {
2139 let ctx = TestContext::new().await;
2140 let task_mgr = TaskManager::new(ctx.pool());
2141
2142 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2143
2144 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2145 let summary = result.events_summary.unwrap();
2146
2147 assert_eq!(summary.total_count, 0);
2148 assert_eq!(summary.recent_events.len(), 0);
2149 }
2150
2151 #[tokio::test]
2152 async fn test_pick_next_tasks_zero_capacity() {
2153 let ctx = TestContext::new().await;
2154 let task_mgr = TaskManager::new(ctx.pool());
2155
2156 task_mgr.add_task("Task 1", None, None).await.unwrap();
2157
2158 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2160 assert_eq!(results.len(), 0);
2161 }
2162
2163 #[tokio::test]
2164 async fn test_pick_next_tasks_capacity_exceeds_available() {
2165 let ctx = TestContext::new().await;
2166 let task_mgr = TaskManager::new(ctx.pool());
2167
2168 task_mgr.add_task("Task 1", None, None).await.unwrap();
2169 task_mgr.add_task("Task 2", None, None).await.unwrap();
2170
2171 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2173 assert_eq!(results.len(), 2); }
2175
2176 #[tokio::test]
2179 async fn test_get_task_context_root_task_no_relations() {
2180 let ctx = TestContext::new().await;
2181 let task_mgr = TaskManager::new(ctx.pool());
2182
2183 let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2185
2186 let context = task_mgr.get_task_context(task.id).await.unwrap();
2187
2188 assert_eq!(context.task.id, task.id);
2190 assert_eq!(context.task.name, "Root task");
2191
2192 assert_eq!(context.ancestors.len(), 0);
2194
2195 assert_eq!(context.siblings.len(), 0);
2197
2198 assert_eq!(context.children.len(), 0);
2200 }
2201
2202 #[tokio::test]
2203 async fn test_get_task_context_with_siblings() {
2204 let ctx = TestContext::new().await;
2205 let task_mgr = TaskManager::new(ctx.pool());
2206
2207 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2209 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2210 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2211
2212 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2213
2214 assert_eq!(context.task.id, task2.id);
2216
2217 assert_eq!(context.ancestors.len(), 0);
2219
2220 assert_eq!(context.siblings.len(), 2);
2222 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2223 assert!(sibling_ids.contains(&task1.id));
2224 assert!(sibling_ids.contains(&task3.id));
2225 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2229 }
2230
2231 #[tokio::test]
2232 async fn test_get_task_context_with_parent() {
2233 let ctx = TestContext::new().await;
2234 let task_mgr = TaskManager::new(ctx.pool());
2235
2236 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2238 let child = task_mgr
2239 .add_task("Child task", None, Some(parent.id))
2240 .await
2241 .unwrap();
2242
2243 let context = task_mgr.get_task_context(child.id).await.unwrap();
2244
2245 assert_eq!(context.task.id, child.id);
2247 assert_eq!(context.task.parent_id, Some(parent.id));
2248
2249 assert_eq!(context.ancestors.len(), 1);
2251 assert_eq!(context.ancestors[0].id, parent.id);
2252 assert_eq!(context.ancestors[0].name, "Parent task");
2253
2254 assert_eq!(context.siblings.len(), 0);
2256
2257 assert_eq!(context.children.len(), 0);
2259 }
2260
2261 #[tokio::test]
2262 async fn test_get_task_context_with_children() {
2263 let ctx = TestContext::new().await;
2264 let task_mgr = TaskManager::new(ctx.pool());
2265
2266 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2268 let child1 = task_mgr
2269 .add_task("Child 1", None, Some(parent.id))
2270 .await
2271 .unwrap();
2272 let child2 = task_mgr
2273 .add_task("Child 2", None, Some(parent.id))
2274 .await
2275 .unwrap();
2276 let child3 = task_mgr
2277 .add_task("Child 3", None, Some(parent.id))
2278 .await
2279 .unwrap();
2280
2281 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2282
2283 assert_eq!(context.task.id, parent.id);
2285
2286 assert_eq!(context.ancestors.len(), 0);
2288
2289 assert_eq!(context.siblings.len(), 0);
2291
2292 assert_eq!(context.children.len(), 3);
2294 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2295 assert!(child_ids.contains(&child1.id));
2296 assert!(child_ids.contains(&child2.id));
2297 assert!(child_ids.contains(&child3.id));
2298 }
2299
2300 #[tokio::test]
2301 async fn test_get_task_context_multi_level_hierarchy() {
2302 let ctx = TestContext::new().await;
2303 let task_mgr = TaskManager::new(ctx.pool());
2304
2305 let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2307 let parent = task_mgr
2308 .add_task("Parent", None, Some(grandparent.id))
2309 .await
2310 .unwrap();
2311 let child = task_mgr
2312 .add_task("Child", None, Some(parent.id))
2313 .await
2314 .unwrap();
2315
2316 let context = task_mgr.get_task_context(child.id).await.unwrap();
2317
2318 assert_eq!(context.task.id, child.id);
2320
2321 assert_eq!(context.ancestors.len(), 2);
2323 assert_eq!(context.ancestors[0].id, parent.id);
2324 assert_eq!(context.ancestors[0].name, "Parent");
2325 assert_eq!(context.ancestors[1].id, grandparent.id);
2326 assert_eq!(context.ancestors[1].name, "Grandparent");
2327
2328 assert_eq!(context.siblings.len(), 0);
2330
2331 assert_eq!(context.children.len(), 0);
2333 }
2334
2335 #[tokio::test]
2336 async fn test_get_task_context_complex_family_tree() {
2337 let ctx = TestContext::new().await;
2338 let task_mgr = TaskManager::new(ctx.pool());
2339
2340 let root = task_mgr.add_task("Root", None, None).await.unwrap();
2348 let child1 = task_mgr
2349 .add_task("Child1", None, Some(root.id))
2350 .await
2351 .unwrap();
2352 let child2 = task_mgr
2353 .add_task("Child2", None, Some(root.id))
2354 .await
2355 .unwrap();
2356 let grandchild1 = task_mgr
2357 .add_task("Grandchild1", None, Some(child1.id))
2358 .await
2359 .unwrap();
2360 let grandchild2 = task_mgr
2361 .add_task("Grandchild2", None, Some(child1.id))
2362 .await
2363 .unwrap();
2364
2365 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2367
2368 assert_eq!(context.task.id, grandchild2.id);
2370
2371 assert_eq!(context.ancestors.len(), 2);
2373 assert_eq!(context.ancestors[0].id, child1.id);
2374 assert_eq!(context.ancestors[1].id, root.id);
2375
2376 assert_eq!(context.siblings.len(), 1);
2378 assert_eq!(context.siblings[0].id, grandchild1.id);
2379
2380 assert_eq!(context.children.len(), 0);
2382
2383 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2385 assert_eq!(context_child1.ancestors.len(), 1);
2386 assert_eq!(context_child1.ancestors[0].id, root.id);
2387 assert_eq!(context_child1.siblings.len(), 1);
2388 assert_eq!(context_child1.siblings[0].id, child2.id);
2389 assert_eq!(context_child1.children.len(), 2);
2390 }
2391
2392 #[tokio::test]
2393 async fn test_get_task_context_respects_priority_ordering() {
2394 let ctx = TestContext::new().await;
2395 let task_mgr = TaskManager::new(ctx.pool());
2396
2397 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2399
2400 let child_low = task_mgr
2402 .add_task("Low priority", None, Some(parent.id))
2403 .await
2404 .unwrap();
2405 let _ = task_mgr
2406 .update_task(child_low.id, None, None, None, None, None, Some(10))
2407 .await
2408 .unwrap();
2409
2410 let child_high = task_mgr
2411 .add_task("High priority", None, Some(parent.id))
2412 .await
2413 .unwrap();
2414 let _ = task_mgr
2415 .update_task(child_high.id, None, None, None, None, None, Some(1))
2416 .await
2417 .unwrap();
2418
2419 let child_medium = task_mgr
2420 .add_task("Medium priority", None, Some(parent.id))
2421 .await
2422 .unwrap();
2423 let _ = task_mgr
2424 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2425 .await
2426 .unwrap();
2427
2428 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2429
2430 assert_eq!(context.children.len(), 3);
2432 assert_eq!(context.children[0].priority, Some(1));
2433 assert_eq!(context.children[1].priority, Some(5));
2434 assert_eq!(context.children[2].priority, Some(10));
2435 }
2436
2437 #[tokio::test]
2438 async fn test_get_task_context_nonexistent_task() {
2439 let ctx = TestContext::new().await;
2440 let task_mgr = TaskManager::new(ctx.pool());
2441
2442 let result = task_mgr.get_task_context(99999).await;
2443 assert!(result.is_err());
2444 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2445 }
2446
2447 #[tokio::test]
2448 async fn test_get_task_context_handles_null_priority() {
2449 let ctx = TestContext::new().await;
2450 let task_mgr = TaskManager::new(ctx.pool());
2451
2452 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2454 let _ = task_mgr
2455 .update_task(task1.id, None, None, None, None, None, Some(1))
2456 .await
2457 .unwrap();
2458
2459 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2460 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2463 let _ = task_mgr
2464 .update_task(task3.id, None, None, None, None, None, Some(5))
2465 .await
2466 .unwrap();
2467
2468 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2469
2470 assert_eq!(context.siblings.len(), 2);
2472 assert_eq!(context.siblings[0].id, task1.id);
2474 assert_eq!(context.siblings[0].priority, Some(1));
2475 assert_eq!(context.siblings[1].id, task3.id);
2477 assert_eq!(context.siblings[1].priority, Some(5));
2478 }
2479
2480 #[tokio::test]
2481 async fn test_pick_next_tasks_priority_order() {
2482 let ctx = TestContext::new().await;
2483 let task_mgr = TaskManager::new(ctx.pool());
2484
2485 let critical = task_mgr
2487 .add_task("Critical Task", None, None)
2488 .await
2489 .unwrap();
2490 task_mgr
2491 .update_task(critical.id, None, None, None, None, None, Some(1))
2492 .await
2493 .unwrap();
2494
2495 let low = task_mgr.add_task("Low Task", None, None).await.unwrap();
2496 task_mgr
2497 .update_task(low.id, None, None, None, None, None, Some(4))
2498 .await
2499 .unwrap();
2500
2501 let high = task_mgr.add_task("High Task", None, None).await.unwrap();
2502 task_mgr
2503 .update_task(high.id, None, None, None, None, None, Some(2))
2504 .await
2505 .unwrap();
2506
2507 let medium = task_mgr.add_task("Medium Task", None, None).await.unwrap();
2508 task_mgr
2509 .update_task(medium.id, None, None, None, None, None, Some(3))
2510 .await
2511 .unwrap();
2512
2513 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2515
2516 assert_eq!(tasks.len(), 4);
2517 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); }
2522
2523 #[tokio::test]
2524 async fn test_pick_next_prefers_doing_over_todo() {
2525 let ctx = TestContext::new().await;
2526 let task_mgr = TaskManager::new(ctx.pool());
2527 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2528
2529 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2531 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
2532 workspace_mgr
2533 .set_current_task(parent_started.task.id)
2534 .await
2535 .unwrap();
2536
2537 let doing_subtask = task_mgr
2539 .add_task("Doing Subtask", None, Some(parent.id))
2540 .await
2541 .unwrap();
2542 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
2543 workspace_mgr.set_current_task(parent.id).await.unwrap();
2545
2546 let _todo_subtask = task_mgr
2547 .add_task("Todo Subtask", None, Some(parent.id))
2548 .await
2549 .unwrap();
2550
2551 let result = task_mgr.pick_next().await.unwrap();
2553
2554 if let Some(task) = result.task {
2555 assert_eq!(
2556 task.id, doing_subtask.id,
2557 "Should recommend doing subtask over todo subtask"
2558 );
2559 assert_eq!(task.status, "doing");
2560 } else {
2561 panic!("Expected a task recommendation");
2562 }
2563 }
2564
2565 #[tokio::test]
2566 async fn test_multiple_doing_tasks_allowed() {
2567 let ctx = TestContext::new().await;
2568 let task_mgr = TaskManager::new(ctx.pool());
2569 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2570
2571 let task_a = task_mgr.add_task("Task A", None, None).await.unwrap();
2573 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
2574 assert_eq!(task_a_started.task.status, "doing");
2575
2576 let current = workspace_mgr.get_current_task().await.unwrap();
2578 assert_eq!(current.current_task_id, Some(task_a.id));
2579
2580 let task_b = task_mgr.add_task("Task B", None, None).await.unwrap();
2582 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
2583 assert_eq!(task_b_started.task.status, "doing");
2584
2585 let current = workspace_mgr.get_current_task().await.unwrap();
2587 assert_eq!(current.current_task_id, Some(task_b.id));
2588
2589 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
2591 assert_eq!(
2592 task_a_after.status, "doing",
2593 "Task A should remain doing even though it is not current"
2594 );
2595
2596 let doing_tasks: Vec<Task> = sqlx::query_as(
2598 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
2599 FROM tasks WHERE status = 'doing' ORDER BY id"#
2600 )
2601 .fetch_all(ctx.pool())
2602 .await
2603 .unwrap();
2604
2605 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
2606 assert_eq!(doing_tasks[0].id, task_a.id);
2607 assert_eq!(doing_tasks[1].id, task_b.id);
2608 }
2609 #[tokio::test]
2610 async fn test_find_tasks_pagination() {
2611 let ctx = TestContext::new().await;
2612 let task_mgr = TaskManager::new(ctx.pool());
2613
2614 for i in 0..15 {
2616 task_mgr
2617 .add_task(&format!("Task {}", i), None, None)
2618 .await
2619 .unwrap();
2620 }
2621
2622 let page1 = task_mgr
2624 .find_tasks(None, None, None, Some(10), Some(0))
2625 .await
2626 .unwrap();
2627 assert_eq!(page1.tasks.len(), 10);
2628 assert_eq!(page1.total_count, 15);
2629 assert!(page1.has_more);
2630 assert_eq!(page1.offset, 0);
2631
2632 let page2 = task_mgr
2634 .find_tasks(None, None, None, Some(10), Some(10))
2635 .await
2636 .unwrap();
2637 assert_eq!(page2.tasks.len(), 5);
2638 assert_eq!(page2.total_count, 15);
2639 assert!(!page2.has_more);
2640 assert_eq!(page2.offset, 10);
2641 }
2642}
2643
2644