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(
110 &self,
111 name: &str,
112 spec: Option<&str>,
113 parent_id: Option<i64>,
114 owner: Option<&str>,
115 ) -> Result<Task> {
116 if let Some(pid) = parent_id {
118 self.check_task_exists(pid).await?;
119 }
120
121 let now = Utc::now();
122 let owner = owner.unwrap_or("human");
123
124 let result = sqlx::query(
125 r#"
126 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner)
127 VALUES (?, ?, ?, 'todo', ?, ?)
128 "#,
129 )
130 .bind(name)
131 .bind(spec)
132 .bind(parent_id)
133 .bind(now)
134 .bind(owner)
135 .execute(self.pool)
136 .await?;
137
138 let id = result.last_insert_rowid();
139 let task = self.get_task(id).await?;
140
141 self.notify_task_created(&task).await;
143
144 Ok(task)
145 }
146
147 pub async fn get_task(&self, id: i64) -> Result<Task> {
149 let task = sqlx::query_as::<_, Task>(
150 r#"
151 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
152 FROM tasks
153 WHERE id = ?
154 "#,
155 )
156 .bind(id)
157 .fetch_optional(self.pool)
158 .await?
159 .ok_or(IntentError::TaskNotFound(id))?;
160
161 Ok(task)
162 }
163
164 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
166 let task = self.get_task(id).await?;
167 let events_summary = self.get_events_summary(id).await?;
168
169 Ok(TaskWithEvents {
170 task,
171 events_summary: Some(events_summary),
172 })
173 }
174
175 pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
184 let mut chain = Vec::new();
185 let mut current_id = Some(task_id);
186
187 while let Some(id) = current_id {
188 let task = self.get_task(id).await?;
189 current_id = task.parent_id;
190 chain.push(task);
191 }
192
193 Ok(chain)
194 }
195
196 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
204 let task = self.get_task(id).await?;
206
207 let mut ancestors = Vec::new();
209 let mut current_parent_id = task.parent_id;
210
211 while let Some(parent_id) = current_parent_id {
212 let parent = self.get_task(parent_id).await?;
213 current_parent_id = parent.parent_id;
214 ancestors.push(parent);
215 }
216
217 let siblings = if let Some(parent_id) = task.parent_id {
219 sqlx::query_as::<_, Task>(
220 r#"
221 SELECT id, parent_id, name, spec, status, complexity, priority,
222 first_todo_at, first_doing_at, first_done_at, active_form, owner
223 FROM tasks
224 WHERE parent_id = ? AND id != ?
225 ORDER BY priority ASC NULLS LAST, id ASC
226 "#,
227 )
228 .bind(parent_id)
229 .bind(id)
230 .fetch_all(self.pool)
231 .await?
232 } else {
233 sqlx::query_as::<_, Task>(
235 r#"
236 SELECT id, parent_id, name, spec, status, complexity, priority,
237 first_todo_at, first_doing_at, first_done_at, active_form, owner
238 FROM tasks
239 WHERE parent_id IS NULL AND id != ?
240 ORDER BY priority ASC NULLS LAST, id ASC
241 "#,
242 )
243 .bind(id)
244 .fetch_all(self.pool)
245 .await?
246 };
247
248 let children = sqlx::query_as::<_, Task>(
250 r#"
251 SELECT id, parent_id, name, spec, status, complexity, priority,
252 first_todo_at, first_doing_at, first_done_at, active_form, owner
253 FROM tasks
254 WHERE parent_id = ?
255 ORDER BY priority ASC NULLS LAST, id ASC
256 "#,
257 )
258 .bind(id)
259 .fetch_all(self.pool)
260 .await?;
261
262 let blocking_tasks = sqlx::query_as::<_, Task>(
264 r#"
265 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
266 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
267 FROM tasks t
268 JOIN dependencies d ON t.id = d.blocking_task_id
269 WHERE d.blocked_task_id = ?
270 ORDER BY t.priority ASC NULLS LAST, t.id ASC
271 "#,
272 )
273 .bind(id)
274 .fetch_all(self.pool)
275 .await?;
276
277 let blocked_by_tasks = sqlx::query_as::<_, Task>(
279 r#"
280 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
281 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
282 FROM tasks t
283 JOIN dependencies d ON t.id = d.blocked_task_id
284 WHERE d.blocking_task_id = ?
285 ORDER BY t.priority ASC NULLS LAST, t.id ASC
286 "#,
287 )
288 .bind(id)
289 .fetch_all(self.pool)
290 .await?;
291
292 Ok(TaskContext {
293 task,
294 ancestors,
295 siblings,
296 children,
297 dependencies: crate::db::models::TaskDependencies {
298 blocking_tasks,
299 blocked_by_tasks,
300 },
301 })
302 }
303
304 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
306 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
307 .bind(task_id)
308 .fetch_one(self.pool)
309 .await?;
310
311 let recent_events = sqlx::query_as::<_, Event>(
312 r#"
313 SELECT id, task_id, timestamp, log_type, discussion_data
314 FROM events
315 WHERE task_id = ?
316 ORDER BY timestamp DESC
317 LIMIT 10
318 "#,
319 )
320 .bind(task_id)
321 .fetch_all(self.pool)
322 .await?;
323
324 Ok(EventsSummary {
325 total_count,
326 recent_events,
327 })
328 }
329
330 #[allow(clippy::too_many_arguments)]
332 pub async fn update_task(
333 &self,
334 id: i64,
335 name: Option<&str>,
336 spec: Option<&str>,
337 parent_id: Option<Option<i64>>,
338 status: Option<&str>,
339 complexity: Option<i32>,
340 priority: Option<i32>,
341 ) -> Result<Task> {
342 let task = self.get_task(id).await?;
344
345 if let Some(s) = status {
347 if !["todo", "doing", "done"].contains(&s) {
348 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
349 }
350 }
351
352 if let Some(Some(pid)) = parent_id {
354 if pid == id {
355 return Err(IntentError::CircularDependency {
356 blocking_task_id: pid,
357 blocked_task_id: id,
358 });
359 }
360 self.check_task_exists(pid).await?;
361 self.check_circular_dependency(id, pid).await?;
362 }
363
364 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
366 sqlx::QueryBuilder::new("UPDATE tasks SET ");
367 let mut has_updates = false;
368
369 if let Some(n) = name {
370 if has_updates {
371 builder.push(", ");
372 }
373 builder.push("name = ").push_bind(n);
374 has_updates = true;
375 }
376
377 if let Some(s) = spec {
378 if has_updates {
379 builder.push(", ");
380 }
381 builder.push("spec = ").push_bind(s);
382 has_updates = true;
383 }
384
385 if let Some(pid) = parent_id {
386 if has_updates {
387 builder.push(", ");
388 }
389 match pid {
390 Some(p) => {
391 builder.push("parent_id = ").push_bind(p);
392 },
393 None => {
394 builder.push("parent_id = NULL");
395 },
396 }
397 has_updates = true;
398 }
399
400 if let Some(c) = complexity {
401 if has_updates {
402 builder.push(", ");
403 }
404 builder.push("complexity = ").push_bind(c);
405 has_updates = true;
406 }
407
408 if let Some(p) = priority {
409 if has_updates {
410 builder.push(", ");
411 }
412 builder.push("priority = ").push_bind(p);
413 has_updates = true;
414 }
415
416 if let Some(s) = status {
417 if has_updates {
418 builder.push(", ");
419 }
420 builder.push("status = ").push_bind(s);
421 has_updates = true;
422
423 let now = Utc::now();
425 let timestamp = now.to_rfc3339();
426 match s {
427 "todo" if task.first_todo_at.is_none() => {
428 builder.push(", first_todo_at = ").push_bind(timestamp);
429 },
430 "doing" if task.first_doing_at.is_none() => {
431 builder.push(", first_doing_at = ").push_bind(timestamp);
432 },
433 "done" if task.first_done_at.is_none() => {
434 builder.push(", first_done_at = ").push_bind(timestamp);
435 },
436 _ => {},
437 }
438 }
439
440 if !has_updates {
441 return Ok(task);
442 }
443
444 builder.push(" WHERE id = ").push_bind(id);
445
446 builder.build().execute(self.pool).await?;
447
448 let task = self.get_task(id).await?;
449
450 self.notify_task_updated(&task).await;
452
453 Ok(task)
454 }
455
456 pub async fn delete_task(&self, id: i64) -> Result<()> {
458 self.check_task_exists(id).await?;
459
460 sqlx::query("DELETE FROM tasks WHERE id = ?")
461 .bind(id)
462 .execute(self.pool)
463 .await?;
464
465 self.notify_task_deleted(id).await;
467
468 Ok(())
469 }
470
471 pub async fn find_tasks(
473 &self,
474 status: Option<&str>,
475 parent_id: Option<Option<i64>>,
476 sort_by: Option<TaskSortBy>,
477 limit: Option<i64>,
478 offset: Option<i64>,
479 ) -> Result<PaginatedTasks> {
480 let sort_by = sort_by.unwrap_or_default(); let limit = limit.unwrap_or(100);
483 let offset = offset.unwrap_or(0);
484
485 let mut where_clause = String::from("WHERE 1=1");
487 let mut conditions = Vec::new();
488
489 if let Some(s) = status {
490 where_clause.push_str(" AND status = ?");
491 conditions.push(s.to_string());
492 }
493
494 if let Some(pid) = parent_id {
495 if let Some(p) = pid {
496 where_clause.push_str(" AND parent_id = ?");
497 conditions.push(p.to_string());
498 } else {
499 where_clause.push_str(" AND parent_id IS NULL");
500 }
501 }
502
503 let order_clause = match sort_by {
505 TaskSortBy::Id => {
506 "ORDER BY id ASC".to_string()
508 },
509 TaskSortBy::Priority => {
510 "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
512 .to_string()
513 },
514 TaskSortBy::Time => {
515 r#"ORDER BY
517 CASE status
518 WHEN 'doing' THEN first_doing_at
519 WHEN 'todo' THEN first_todo_at
520 WHEN 'done' THEN first_done_at
521 END ASC NULLS LAST,
522 id ASC"#
523 .to_string()
524 },
525 TaskSortBy::FocusAware => {
526 r#"ORDER BY
528 CASE
529 WHEN t.id = (SELECT value FROM workspace_state WHERE key = 'current_task_id') THEN 0
530 WHEN t.status = 'doing' THEN 1
531 WHEN t.status = 'todo' THEN 2
532 ELSE 3
533 END ASC,
534 COALESCE(t.priority, 999) ASC,
535 t.id ASC"#
536 .to_string()
537 },
538 };
539
540 let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
542 let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
543 for cond in &conditions {
544 count_q = count_q.bind(cond);
545 }
546 let total_count = count_q.fetch_one(self.pool).await?;
547
548 let main_query = format!(
550 "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 ?",
551 where_clause, order_clause
552 );
553
554 let mut q = sqlx::query_as::<_, Task>(&main_query);
555 for cond in conditions {
556 q = q.bind(cond);
557 }
558 q = q.bind(limit);
559 q = q.bind(offset);
560
561 let tasks = q.fetch_all(self.pool).await?;
562
563 let has_more = offset + (tasks.len() as i64) < total_count;
565
566 Ok(PaginatedTasks {
567 tasks,
568 total_count,
569 has_more,
570 limit,
571 offset,
572 })
573 }
574
575 pub async fn get_stats(&self) -> Result<WorkspaceStats> {
580 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
581 r#"SELECT
582 COUNT(*) as total,
583 COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
584 COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
585 COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
586 FROM tasks"#,
587 )
588 .fetch_one(self.pool)
589 .await?;
590
591 Ok(WorkspaceStats {
592 total_tasks: row.0,
593 todo: row.1,
594 doing: row.2,
595 done: row.3,
596 })
597 }
598
599 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
601 use crate::dependencies::get_incomplete_blocking_tasks;
603 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
604 return Err(IntentError::TaskBlocked {
605 task_id: id,
606 blocking_task_ids: blocking_tasks,
607 });
608 }
609
610 let mut tx = self.pool.begin().await?;
611
612 let now = Utc::now();
613
614 sqlx::query(
616 r#"
617 UPDATE tasks
618 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
619 WHERE id = ?
620 "#,
621 )
622 .bind(now)
623 .bind(id)
624 .execute(&mut *tx)
625 .await?;
626
627 sqlx::query(
629 r#"
630 INSERT OR REPLACE INTO workspace_state (key, value)
631 VALUES ('current_task_id', ?)
632 "#,
633 )
634 .bind(id.to_string())
635 .execute(&mut *tx)
636 .await?;
637
638 tx.commit().await?;
639
640 if with_events {
641 let result = self.get_task_with_events(id).await?;
642 self.notify_task_updated(&result.task).await;
643 Ok(result)
644 } else {
645 let task = self.get_task(id).await?;
646 self.notify_task_updated(&task).await;
647 Ok(TaskWithEvents {
648 task,
649 events_summary: None,
650 })
651 }
652 }
653
654 pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
663 let mut tx = self.pool.begin().await?;
664
665 let current_task_id: Option<String> =
667 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
668 .fetch_optional(&mut *tx)
669 .await?;
670
671 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
672 IntentError::InvalidInput(
673 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
674 ),
675 )?;
676
677 let task_info: (String, Option<i64>, String) =
679 sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
680 .bind(id)
681 .fetch_one(&mut *tx)
682 .await?;
683 let (task_name, parent_id, owner) = task_info;
684
685 if owner == "human" && is_ai_caller {
688 return Err(IntentError::HumanTaskCannotBeCompletedByAI {
689 task_id: id,
690 task_name: task_name.clone(),
691 });
692 }
693
694 let uncompleted_children: i64 = sqlx::query_scalar(
696 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
697 )
698 .bind(id)
699 .fetch_one(&mut *tx)
700 .await?;
701
702 if uncompleted_children > 0 {
703 return Err(IntentError::UncompletedChildren);
704 }
705
706 let now = Utc::now();
707
708 sqlx::query(
710 r#"
711 UPDATE tasks
712 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
713 WHERE id = ?
714 "#,
715 )
716 .bind(now)
717 .bind(id)
718 .execute(&mut *tx)
719 .await?;
720
721 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
723 .execute(&mut *tx)
724 .await?;
725
726 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
728 let remaining_siblings: i64 = sqlx::query_scalar(
730 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
731 )
732 .bind(parent_task_id)
733 .bind(id)
734 .fetch_one(&mut *tx)
735 .await?;
736
737 if remaining_siblings == 0 {
738 let parent_name: String =
740 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
741 .bind(parent_task_id)
742 .fetch_one(&mut *tx)
743 .await?;
744
745 NextStepSuggestion::ParentIsReady {
746 message: format!(
747 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
748 parent_task_id, parent_name
749 ),
750 parent_task_id,
751 parent_task_name: parent_name,
752 }
753 } else {
754 let parent_name: String =
756 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
757 .bind(parent_task_id)
758 .fetch_one(&mut *tx)
759 .await?;
760
761 NextStepSuggestion::SiblingTasksRemain {
762 message: format!(
763 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
764 id, parent_task_id, parent_name
765 ),
766 parent_task_id,
767 parent_task_name: parent_name,
768 remaining_siblings_count: remaining_siblings,
769 }
770 }
771 } else {
772 let child_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_CHILDREN_TOTAL)
774 .bind(id)
775 .fetch_one(&mut *tx)
776 .await?;
777
778 if child_count > 0 {
779 NextStepSuggestion::TopLevelTaskCompleted {
781 message: format!(
782 "Top-level task #{} '{}' has been completed. Well done!",
783 id, task_name
784 ),
785 completed_task_id: id,
786 completed_task_name: task_name.clone(),
787 }
788 } else {
789 let remaining_tasks: i64 = sqlx::query_scalar(
791 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
792 )
793 .bind(id)
794 .fetch_one(&mut *tx)
795 .await?;
796
797 if remaining_tasks == 0 {
798 NextStepSuggestion::WorkspaceIsClear {
799 message: format!(
800 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
801 id
802 ),
803 completed_task_id: id,
804 }
805 } else {
806 NextStepSuggestion::NoParentContext {
807 message: format!("Task #{} '{}' has been completed.", id, task_name),
808 completed_task_id: id,
809 completed_task_name: task_name.clone(),
810 }
811 }
812 }
813 };
814
815 tx.commit().await?;
816
817 let completed_task = self.get_task(id).await?;
819 self.notify_task_updated(&completed_task).await;
820
821 Ok(DoneTaskResponse {
822 completed_task,
823 workspace_status: WorkspaceStatus {
824 current_task_id: None,
825 },
826 next_step_suggestion,
827 })
828 }
829
830 async fn check_task_exists(&self, id: i64) -> Result<()> {
832 let exists: bool = sqlx::query_scalar(crate::sql_constants::CHECK_TASK_EXISTS)
833 .bind(id)
834 .fetch_one(self.pool)
835 .await?;
836
837 if !exists {
838 return Err(IntentError::TaskNotFound(id));
839 }
840
841 Ok(())
842 }
843
844 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
846 let mut current_id = new_parent_id;
847
848 loop {
849 if current_id == task_id {
850 return Err(IntentError::CircularDependency {
851 blocking_task_id: new_parent_id,
852 blocked_task_id: task_id,
853 });
854 }
855
856 let parent: Option<i64> =
857 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_PARENT_ID)
858 .bind(current_id)
859 .fetch_optional(self.pool)
860 .await?;
861
862 match parent {
863 Some(pid) => current_id = pid,
864 None => break,
865 }
866 }
867
868 Ok(())
869 }
870 pub async fn spawn_subtask(
874 &self,
875 name: &str,
876 spec: Option<&str>,
877 ) -> Result<SpawnSubtaskResponse> {
878 let current_task_id: Option<String> =
880 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
881 .fetch_optional(self.pool)
882 .await?;
883
884 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
885 IntentError::InvalidInput("No current task to create subtask under".to_string()),
886 )?;
887
888 let parent_name: String = sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
890 .bind(parent_id)
891 .fetch_one(self.pool)
892 .await?;
893
894 let subtask = self.add_task(name, spec, Some(parent_id), None).await?;
896
897 self.start_task(subtask.id, false).await?;
900
901 Ok(SpawnSubtaskResponse {
902 subtask: SubtaskInfo {
903 id: subtask.id,
904 name: subtask.name,
905 parent_id,
906 status: "doing".to_string(),
907 },
908 parent_task: ParentTaskInfo {
909 id: parent_id,
910 name: parent_name,
911 },
912 })
913 }
914
915 pub async fn pick_next_tasks(
928 &self,
929 max_count: usize,
930 capacity_limit: usize,
931 ) -> Result<Vec<Task>> {
932 let mut tx = self.pool.begin().await?;
933
934 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
936 .fetch_one(&mut *tx)
937 .await?;
938
939 let available = capacity_limit.saturating_sub(doing_count as usize);
941 if available == 0 {
942 return Ok(vec![]);
943 }
944
945 let limit = std::cmp::min(max_count, available);
946
947 let todo_tasks = sqlx::query_as::<_, Task>(
949 r#"
950 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
951 FROM tasks
952 WHERE status = 'todo'
953 ORDER BY
954 COALESCE(priority, 0) ASC,
955 COALESCE(complexity, 5) ASC,
956 id ASC
957 LIMIT ?
958 "#,
959 )
960 .bind(limit as i64)
961 .fetch_all(&mut *tx)
962 .await?;
963
964 if todo_tasks.is_empty() {
965 return Ok(vec![]);
966 }
967
968 let now = Utc::now();
969
970 for task in &todo_tasks {
972 sqlx::query(
973 r#"
974 UPDATE tasks
975 SET status = 'doing',
976 first_doing_at = COALESCE(first_doing_at, ?)
977 WHERE id = ?
978 "#,
979 )
980 .bind(now)
981 .bind(task.id)
982 .execute(&mut *tx)
983 .await?;
984 }
985
986 tx.commit().await?;
987
988 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
990 let placeholders = vec!["?"; task_ids.len()].join(",");
991 let query = format!(
992 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
993 FROM tasks WHERE id IN ({})
994 ORDER BY
995 COALESCE(priority, 0) ASC,
996 COALESCE(complexity, 5) ASC,
997 id ASC",
998 placeholders
999 );
1000
1001 let mut q = sqlx::query_as::<_, Task>(&query);
1002 for id in task_ids {
1003 q = q.bind(id);
1004 }
1005
1006 let updated_tasks = q.fetch_all(self.pool).await?;
1007 Ok(updated_tasks)
1008 }
1009
1010 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1019 let current_task_id: Option<String> =
1021 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1022 .fetch_optional(self.pool)
1023 .await?;
1024
1025 if let Some(current_id_str) = current_task_id.as_ref() {
1026 if let Ok(current_id) = current_id_str.parse::<i64>() {
1027 let doing_subtasks = sqlx::query_as::<_, Task>(
1030 r#"
1031 SELECT id, parent_id, name, spec, status, complexity, priority,
1032 first_todo_at, first_doing_at, first_done_at, active_form, owner
1033 FROM tasks
1034 WHERE parent_id = ? AND status = 'doing'
1035 AND NOT EXISTS (
1036 SELECT 1 FROM dependencies d
1037 JOIN tasks bt ON d.blocking_task_id = bt.id
1038 WHERE d.blocked_task_id = tasks.id
1039 AND bt.status != 'done'
1040 )
1041 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1042 LIMIT 1
1043 "#,
1044 )
1045 .bind(current_id)
1046 .fetch_optional(self.pool)
1047 .await?;
1048
1049 if let Some(task) = doing_subtasks {
1050 return Ok(PickNextResponse::focused_subtask(task));
1051 }
1052
1053 let todo_subtasks = sqlx::query_as::<_, Task>(
1055 r#"
1056 SELECT id, parent_id, name, spec, status, complexity, priority,
1057 first_todo_at, first_doing_at, first_done_at, active_form, owner
1058 FROM tasks
1059 WHERE parent_id = ? AND status = 'todo'
1060 AND NOT EXISTS (
1061 SELECT 1 FROM dependencies d
1062 JOIN tasks bt ON d.blocking_task_id = bt.id
1063 WHERE d.blocked_task_id = tasks.id
1064 AND bt.status != 'done'
1065 )
1066 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1067 LIMIT 1
1068 "#,
1069 )
1070 .bind(current_id)
1071 .fetch_optional(self.pool)
1072 .await?;
1073
1074 if let Some(task) = todo_subtasks {
1075 return Ok(PickNextResponse::focused_subtask(task));
1076 }
1077 }
1078 }
1079
1080 let doing_top_level = if let Some(current_id_str) = current_task_id.as_ref() {
1083 if let Ok(current_id) = current_id_str.parse::<i64>() {
1084 sqlx::query_as::<_, Task>(
1085 r#"
1086 SELECT id, parent_id, name, spec, status, complexity, priority,
1087 first_todo_at, first_doing_at, first_done_at, active_form, owner
1088 FROM tasks
1089 WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1090 AND NOT EXISTS (
1091 SELECT 1 FROM dependencies d
1092 JOIN tasks bt ON d.blocking_task_id = bt.id
1093 WHERE d.blocked_task_id = tasks.id
1094 AND bt.status != 'done'
1095 )
1096 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1097 LIMIT 1
1098 "#,
1099 )
1100 .bind(current_id)
1101 .fetch_optional(self.pool)
1102 .await?
1103 } else {
1104 None
1105 }
1106 } else {
1107 sqlx::query_as::<_, Task>(
1108 r#"
1109 SELECT id, parent_id, name, spec, status, complexity, priority,
1110 first_todo_at, first_doing_at, first_done_at, active_form, owner
1111 FROM tasks
1112 WHERE parent_id IS NULL AND status = 'doing'
1113 AND NOT EXISTS (
1114 SELECT 1 FROM dependencies d
1115 JOIN tasks bt ON d.blocking_task_id = bt.id
1116 WHERE d.blocked_task_id = tasks.id
1117 AND bt.status != 'done'
1118 )
1119 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1120 LIMIT 1
1121 "#,
1122 )
1123 .fetch_optional(self.pool)
1124 .await?
1125 };
1126
1127 if let Some(task) = doing_top_level {
1128 return Ok(PickNextResponse::top_level_task(task));
1129 }
1130
1131 let todo_top_level = sqlx::query_as::<_, Task>(
1134 r#"
1135 SELECT id, parent_id, name, spec, status, complexity, priority,
1136 first_todo_at, first_doing_at, first_done_at, active_form, owner
1137 FROM tasks
1138 WHERE parent_id IS NULL AND status = 'todo'
1139 AND NOT EXISTS (
1140 SELECT 1 FROM dependencies d
1141 JOIN tasks bt ON d.blocking_task_id = bt.id
1142 WHERE d.blocked_task_id = tasks.id
1143 AND bt.status != 'done'
1144 )
1145 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1146 LIMIT 1
1147 "#,
1148 )
1149 .fetch_optional(self.pool)
1150 .await?;
1151
1152 if let Some(task) = todo_top_level {
1153 return Ok(PickNextResponse::top_level_task(task));
1154 }
1155
1156 let total_tasks: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_TOTAL)
1159 .fetch_one(self.pool)
1160 .await?;
1161
1162 if total_tasks == 0 {
1163 return Ok(PickNextResponse::no_tasks_in_project());
1164 }
1165
1166 let todo_or_doing_count: i64 =
1168 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1169 .fetch_one(self.pool)
1170 .await?;
1171
1172 if todo_or_doing_count == 0 {
1173 return Ok(PickNextResponse::all_tasks_completed());
1174 }
1175
1176 Ok(PickNextResponse::no_available_todos())
1178 }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use super::*;
1184 use crate::events::EventManager;
1185 use crate::test_utils::test_helpers::TestContext;
1186 use crate::workspace::WorkspaceManager;
1187
1188 #[tokio::test]
1189 async fn test_get_stats_empty() {
1190 let ctx = TestContext::new().await;
1191 let manager = TaskManager::new(ctx.pool());
1192
1193 let stats = manager.get_stats().await.unwrap();
1194
1195 assert_eq!(stats.total_tasks, 0);
1196 assert_eq!(stats.todo, 0);
1197 assert_eq!(stats.doing, 0);
1198 assert_eq!(stats.done, 0);
1199 }
1200
1201 #[tokio::test]
1202 async fn test_get_stats_with_tasks() {
1203 let ctx = TestContext::new().await;
1204 let manager = TaskManager::new(ctx.pool());
1205
1206 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1208 let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
1209 let _task3 = manager.add_task("Task 3", None, None, None).await.unwrap();
1210
1211 manager
1213 .update_task(task1.id, None, None, None, Some("doing"), None, None)
1214 .await
1215 .unwrap();
1216 manager
1217 .update_task(task2.id, None, None, None, Some("done"), None, None)
1218 .await
1219 .unwrap();
1220 let stats = manager.get_stats().await.unwrap();
1223
1224 assert_eq!(stats.total_tasks, 3);
1225 assert_eq!(stats.todo, 1);
1226 assert_eq!(stats.doing, 1);
1227 assert_eq!(stats.done, 1);
1228 }
1229
1230 #[tokio::test]
1231 async fn test_add_task() {
1232 let ctx = TestContext::new().await;
1233 let manager = TaskManager::new(ctx.pool());
1234
1235 let task = manager
1236 .add_task("Test task", None, None, None)
1237 .await
1238 .unwrap();
1239
1240 assert_eq!(task.name, "Test task");
1241 assert_eq!(task.status, "todo");
1242 assert!(task.first_todo_at.is_some());
1243 assert!(task.first_doing_at.is_none());
1244 assert!(task.first_done_at.is_none());
1245 }
1246
1247 #[tokio::test]
1248 async fn test_add_task_with_spec() {
1249 let ctx = TestContext::new().await;
1250 let manager = TaskManager::new(ctx.pool());
1251
1252 let spec = "This is a task specification";
1253 let task = manager
1254 .add_task("Test task", Some(spec), None, None)
1255 .await
1256 .unwrap();
1257
1258 assert_eq!(task.name, "Test task");
1259 assert_eq!(task.spec.as_deref(), Some(spec));
1260 }
1261
1262 #[tokio::test]
1263 async fn test_add_task_with_parent() {
1264 let ctx = TestContext::new().await;
1265 let manager = TaskManager::new(ctx.pool());
1266
1267 let parent = manager
1268 .add_task("Parent task", None, None, None)
1269 .await
1270 .unwrap();
1271 let child = manager
1272 .add_task("Child task", None, Some(parent.id), None)
1273 .await
1274 .unwrap();
1275
1276 assert_eq!(child.parent_id, Some(parent.id));
1277 }
1278
1279 #[tokio::test]
1280 async fn test_get_task() {
1281 let ctx = TestContext::new().await;
1282 let manager = TaskManager::new(ctx.pool());
1283
1284 let created = manager
1285 .add_task("Test task", None, None, None)
1286 .await
1287 .unwrap();
1288 let retrieved = manager.get_task(created.id).await.unwrap();
1289
1290 assert_eq!(created.id, retrieved.id);
1291 assert_eq!(created.name, retrieved.name);
1292 }
1293
1294 #[tokio::test]
1295 async fn test_get_task_not_found() {
1296 let ctx = TestContext::new().await;
1297 let manager = TaskManager::new(ctx.pool());
1298
1299 let result = manager.get_task(999).await;
1300 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1301 }
1302
1303 #[tokio::test]
1304 async fn test_update_task_name() {
1305 let ctx = TestContext::new().await;
1306 let manager = TaskManager::new(ctx.pool());
1307
1308 let task = manager
1309 .add_task("Original name", None, None, None)
1310 .await
1311 .unwrap();
1312 let updated = manager
1313 .update_task(task.id, Some("New name"), None, None, None, None, None)
1314 .await
1315 .unwrap();
1316
1317 assert_eq!(updated.name, "New name");
1318 }
1319
1320 #[tokio::test]
1321 async fn test_update_task_status() {
1322 let ctx = TestContext::new().await;
1323 let manager = TaskManager::new(ctx.pool());
1324
1325 let task = manager
1326 .add_task("Test task", None, None, None)
1327 .await
1328 .unwrap();
1329 let updated = manager
1330 .update_task(task.id, None, None, None, Some("doing"), None, None)
1331 .await
1332 .unwrap();
1333
1334 assert_eq!(updated.status, "doing");
1335 assert!(updated.first_doing_at.is_some());
1336 }
1337
1338 #[tokio::test]
1339 async fn test_delete_task() {
1340 let ctx = TestContext::new().await;
1341 let manager = TaskManager::new(ctx.pool());
1342
1343 let task = manager
1344 .add_task("Test task", None, None, None)
1345 .await
1346 .unwrap();
1347 manager.delete_task(task.id).await.unwrap();
1348
1349 let result = manager.get_task(task.id).await;
1350 assert!(result.is_err());
1351 }
1352
1353 #[tokio::test]
1354 async fn test_find_tasks_by_status() {
1355 let ctx = TestContext::new().await;
1356 let manager = TaskManager::new(ctx.pool());
1357
1358 manager
1359 .add_task("Todo task", None, None, None)
1360 .await
1361 .unwrap();
1362 let doing_task = manager
1363 .add_task("Doing task", None, None, None)
1364 .await
1365 .unwrap();
1366 manager
1367 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1368 .await
1369 .unwrap();
1370
1371 let todo_result = manager
1372 .find_tasks(Some("todo"), None, None, None, None)
1373 .await
1374 .unwrap();
1375 let doing_result = manager
1376 .find_tasks(Some("doing"), None, None, None, None)
1377 .await
1378 .unwrap();
1379
1380 assert_eq!(todo_result.tasks.len(), 1);
1381 assert_eq!(doing_result.tasks.len(), 1);
1382 assert_eq!(doing_result.tasks[0].status, "doing");
1383 }
1384
1385 #[tokio::test]
1386 async fn test_find_tasks_by_parent() {
1387 let ctx = TestContext::new().await;
1388 let manager = TaskManager::new(ctx.pool());
1389
1390 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1391 manager
1392 .add_task("Child 1", None, Some(parent.id), None)
1393 .await
1394 .unwrap();
1395 manager
1396 .add_task("Child 2", None, Some(parent.id), None)
1397 .await
1398 .unwrap();
1399
1400 let result = manager
1401 .find_tasks(None, Some(Some(parent.id)), None, None, None)
1402 .await
1403 .unwrap();
1404
1405 assert_eq!(result.tasks.len(), 2);
1406 }
1407
1408 #[tokio::test]
1409 async fn test_start_task() {
1410 let ctx = TestContext::new().await;
1411 let manager = TaskManager::new(ctx.pool());
1412
1413 let task = manager
1414 .add_task("Test task", None, None, None)
1415 .await
1416 .unwrap();
1417 let started = manager.start_task(task.id, false).await.unwrap();
1418
1419 assert_eq!(started.task.status, "doing");
1420 assert!(started.task.first_doing_at.is_some());
1421
1422 let current: Option<String> =
1424 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1425 .fetch_optional(ctx.pool())
1426 .await
1427 .unwrap();
1428
1429 assert_eq!(current, Some(task.id.to_string()));
1430 }
1431
1432 #[tokio::test]
1433 async fn test_start_task_with_events() {
1434 let ctx = TestContext::new().await;
1435 let manager = TaskManager::new(ctx.pool());
1436
1437 let task = manager
1438 .add_task("Test task", None, None, None)
1439 .await
1440 .unwrap();
1441
1442 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1444 .bind(task.id)
1445 .bind("test")
1446 .bind("test event")
1447 .execute(ctx.pool())
1448 .await
1449 .unwrap();
1450
1451 let started = manager.start_task(task.id, true).await.unwrap();
1452
1453 assert!(started.events_summary.is_some());
1454 let summary = started.events_summary.unwrap();
1455 assert_eq!(summary.total_count, 1);
1456 }
1457
1458 #[tokio::test]
1459 async fn test_done_task() {
1460 let ctx = TestContext::new().await;
1461 let manager = TaskManager::new(ctx.pool());
1462
1463 let task = manager
1464 .add_task("Test task", None, None, None)
1465 .await
1466 .unwrap();
1467 manager.start_task(task.id, false).await.unwrap();
1468 let response = manager.done_task(false).await.unwrap();
1469
1470 assert_eq!(response.completed_task.status, "done");
1471 assert!(response.completed_task.first_done_at.is_some());
1472 assert_eq!(response.workspace_status.current_task_id, None);
1473
1474 match response.next_step_suggestion {
1476 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1477 _ => panic!("Expected WorkspaceIsClear suggestion"),
1478 }
1479
1480 let current: Option<String> =
1482 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1483 .fetch_optional(ctx.pool())
1484 .await
1485 .unwrap();
1486
1487 assert!(current.is_none());
1488 }
1489
1490 #[tokio::test]
1491 async fn test_done_task_with_uncompleted_children() {
1492 let ctx = TestContext::new().await;
1493 let manager = TaskManager::new(ctx.pool());
1494
1495 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1496 manager
1497 .add_task("Child", None, Some(parent.id), None)
1498 .await
1499 .unwrap();
1500
1501 manager.start_task(parent.id, false).await.unwrap();
1503
1504 let result = manager.done_task(false).await;
1505 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1506 }
1507
1508 #[tokio::test]
1509 async fn test_done_task_with_completed_children() {
1510 let ctx = TestContext::new().await;
1511 let manager = TaskManager::new(ctx.pool());
1512
1513 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1514 let child = manager
1515 .add_task("Child", None, Some(parent.id), None)
1516 .await
1517 .unwrap();
1518
1519 manager.start_task(child.id, false).await.unwrap();
1521 let child_response = manager.done_task(false).await.unwrap();
1522
1523 match child_response.next_step_suggestion {
1525 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1526 assert_eq!(parent_task_id, parent.id);
1527 },
1528 _ => panic!("Expected ParentIsReady suggestion"),
1529 }
1530
1531 manager.start_task(parent.id, false).await.unwrap();
1533 let parent_response = manager.done_task(false).await.unwrap();
1534 assert_eq!(parent_response.completed_task.status, "done");
1535
1536 match parent_response.next_step_suggestion {
1538 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1539 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1540 }
1541 }
1542
1543 #[tokio::test]
1544 async fn test_circular_dependency() {
1545 let ctx = TestContext::new().await;
1546 let manager = TaskManager::new(ctx.pool());
1547
1548 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1549 let task2 = manager
1550 .add_task("Task 2", None, Some(task1.id), None)
1551 .await
1552 .unwrap();
1553
1554 let result = manager
1556 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1557 .await;
1558
1559 assert!(matches!(
1560 result,
1561 Err(IntentError::CircularDependency { .. })
1562 ));
1563 }
1564
1565 #[tokio::test]
1566 async fn test_invalid_parent_id() {
1567 let ctx = TestContext::new().await;
1568 let manager = TaskManager::new(ctx.pool());
1569
1570 let result = manager.add_task("Test", None, Some(999), None).await;
1571 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1572 }
1573
1574 #[tokio::test]
1575 async fn test_update_task_complexity_and_priority() {
1576 let ctx = TestContext::new().await;
1577 let manager = TaskManager::new(ctx.pool());
1578
1579 let task = manager
1580 .add_task("Test task", None, None, None)
1581 .await
1582 .unwrap();
1583 let updated = manager
1584 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1585 .await
1586 .unwrap();
1587
1588 assert_eq!(updated.complexity, Some(8));
1589 assert_eq!(updated.priority, Some(10));
1590 }
1591
1592 #[tokio::test]
1593 async fn test_spawn_subtask() {
1594 let ctx = TestContext::new().await;
1595 let manager = TaskManager::new(ctx.pool());
1596
1597 let parent = manager
1599 .add_task("Parent task", None, None, None)
1600 .await
1601 .unwrap();
1602 manager.start_task(parent.id, false).await.unwrap();
1603
1604 let response = manager
1606 .spawn_subtask("Child task", Some("Details"))
1607 .await
1608 .unwrap();
1609
1610 assert_eq!(response.subtask.parent_id, parent.id);
1611 assert_eq!(response.subtask.name, "Child task");
1612 assert_eq!(response.subtask.status, "doing");
1613 assert_eq!(response.parent_task.id, parent.id);
1614 assert_eq!(response.parent_task.name, "Parent task");
1615
1616 let current: Option<String> =
1618 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1619 .fetch_optional(ctx.pool())
1620 .await
1621 .unwrap();
1622
1623 assert_eq!(current, Some(response.subtask.id.to_string()));
1624
1625 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1627 assert_eq!(retrieved.status, "doing");
1628 }
1629
1630 #[tokio::test]
1631 async fn test_spawn_subtask_no_current_task() {
1632 let ctx = TestContext::new().await;
1633 let manager = TaskManager::new(ctx.pool());
1634
1635 let result = manager.spawn_subtask("Child", None).await;
1637 assert!(result.is_err());
1638 }
1639
1640 #[tokio::test]
1641 async fn test_pick_next_tasks_basic() {
1642 let ctx = TestContext::new().await;
1643 let manager = TaskManager::new(ctx.pool());
1644
1645 for i in 1..=10 {
1647 manager
1648 .add_task(&format!("Task {}", i), None, None, None)
1649 .await
1650 .unwrap();
1651 }
1652
1653 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1655
1656 assert_eq!(picked.len(), 5);
1657 for task in &picked {
1658 assert_eq!(task.status, "doing");
1659 assert!(task.first_doing_at.is_some());
1660 }
1661
1662 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1664 .fetch_one(ctx.pool())
1665 .await
1666 .unwrap();
1667
1668 assert_eq!(doing_count, 5);
1669 }
1670
1671 #[tokio::test]
1672 async fn test_pick_next_tasks_with_existing_doing() {
1673 let ctx = TestContext::new().await;
1674 let manager = TaskManager::new(ctx.pool());
1675
1676 for i in 1..=10 {
1678 manager
1679 .add_task(&format!("Task {}", i), None, None, None)
1680 .await
1681 .unwrap();
1682 }
1683
1684 let result = manager
1686 .find_tasks(Some("todo"), None, None, None, None)
1687 .await
1688 .unwrap();
1689 manager.start_task(result.tasks[0].id, false).await.unwrap();
1690 manager.start_task(result.tasks[1].id, false).await.unwrap();
1691
1692 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1694
1695 assert_eq!(picked.len(), 3);
1697
1698 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1700 .fetch_one(ctx.pool())
1701 .await
1702 .unwrap();
1703
1704 assert_eq!(doing_count, 5);
1705 }
1706
1707 #[tokio::test]
1708 async fn test_pick_next_tasks_at_capacity() {
1709 let ctx = TestContext::new().await;
1710 let manager = TaskManager::new(ctx.pool());
1711
1712 for i in 1..=10 {
1714 manager
1715 .add_task(&format!("Task {}", i), None, None, None)
1716 .await
1717 .unwrap();
1718 }
1719
1720 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1722 assert_eq!(first_batch.len(), 5);
1723
1724 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1726 assert_eq!(second_batch.len(), 0);
1727 }
1728
1729 #[tokio::test]
1730 async fn test_pick_next_tasks_priority_ordering() {
1731 let ctx = TestContext::new().await;
1732 let manager = TaskManager::new(ctx.pool());
1733
1734 let low = manager
1736 .add_task("Low priority", None, None, None)
1737 .await
1738 .unwrap();
1739 manager
1740 .update_task(low.id, None, None, None, None, None, Some(1))
1741 .await
1742 .unwrap();
1743
1744 let high = manager
1745 .add_task("High priority", None, None, None)
1746 .await
1747 .unwrap();
1748 manager
1749 .update_task(high.id, None, None, None, None, None, Some(10))
1750 .await
1751 .unwrap();
1752
1753 let medium = manager
1754 .add_task("Medium priority", None, None, None)
1755 .await
1756 .unwrap();
1757 manager
1758 .update_task(medium.id, None, None, None, None, None, Some(5))
1759 .await
1760 .unwrap();
1761
1762 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1764
1765 assert_eq!(picked.len(), 3);
1767 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
1771
1772 #[tokio::test]
1773 async fn test_pick_next_tasks_complexity_ordering() {
1774 let ctx = TestContext::new().await;
1775 let manager = TaskManager::new(ctx.pool());
1776
1777 let complex = manager.add_task("Complex", None, None, None).await.unwrap();
1779 manager
1780 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1781 .await
1782 .unwrap();
1783
1784 let simple = manager.add_task("Simple", None, None, None).await.unwrap();
1785 manager
1786 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1787 .await
1788 .unwrap();
1789
1790 let medium = manager.add_task("Medium", None, None, None).await.unwrap();
1791 manager
1792 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1793 .await
1794 .unwrap();
1795
1796 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1798
1799 assert_eq!(picked.len(), 3);
1801 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1805
1806 #[tokio::test]
1807 async fn test_done_task_sibling_tasks_remain() {
1808 let ctx = TestContext::new().await;
1809 let manager = TaskManager::new(ctx.pool());
1810
1811 let parent = manager
1813 .add_task("Parent Task", None, None, None)
1814 .await
1815 .unwrap();
1816 let child1 = manager
1817 .add_task("Child 1", None, Some(parent.id), None)
1818 .await
1819 .unwrap();
1820 let child2 = manager
1821 .add_task("Child 2", None, Some(parent.id), None)
1822 .await
1823 .unwrap();
1824 let _child3 = manager
1825 .add_task("Child 3", None, Some(parent.id), None)
1826 .await
1827 .unwrap();
1828
1829 manager.start_task(child1.id, false).await.unwrap();
1831 let response = manager.done_task(false).await.unwrap();
1832
1833 match response.next_step_suggestion {
1835 NextStepSuggestion::SiblingTasksRemain {
1836 parent_task_id,
1837 remaining_siblings_count,
1838 ..
1839 } => {
1840 assert_eq!(parent_task_id, parent.id);
1841 assert_eq!(remaining_siblings_count, 2); },
1843 _ => panic!("Expected SiblingTasksRemain suggestion"),
1844 }
1845
1846 manager.start_task(child2.id, false).await.unwrap();
1848 let response2 = manager.done_task(false).await.unwrap();
1849
1850 match response2.next_step_suggestion {
1852 NextStepSuggestion::SiblingTasksRemain {
1853 remaining_siblings_count,
1854 ..
1855 } => {
1856 assert_eq!(remaining_siblings_count, 1); },
1858 _ => panic!("Expected SiblingTasksRemain suggestion"),
1859 }
1860 }
1861
1862 #[tokio::test]
1863 async fn test_done_task_top_level_with_children() {
1864 let ctx = TestContext::new().await;
1865 let manager = TaskManager::new(ctx.pool());
1866
1867 let parent = manager
1869 .add_task("Epic Task", None, None, None)
1870 .await
1871 .unwrap();
1872 let child = manager
1873 .add_task("Sub Task", None, Some(parent.id), None)
1874 .await
1875 .unwrap();
1876
1877 manager.start_task(child.id, false).await.unwrap();
1879 manager.done_task(false).await.unwrap();
1880
1881 manager.start_task(parent.id, false).await.unwrap();
1883 let response = manager.done_task(false).await.unwrap();
1884
1885 match response.next_step_suggestion {
1887 NextStepSuggestion::TopLevelTaskCompleted {
1888 completed_task_id,
1889 completed_task_name,
1890 ..
1891 } => {
1892 assert_eq!(completed_task_id, parent.id);
1893 assert_eq!(completed_task_name, "Epic Task");
1894 },
1895 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1896 }
1897 }
1898
1899 #[tokio::test]
1900 async fn test_done_task_no_parent_context() {
1901 let ctx = TestContext::new().await;
1902 let manager = TaskManager::new(ctx.pool());
1903
1904 let task1 = manager
1906 .add_task("Standalone Task 1", None, None, None)
1907 .await
1908 .unwrap();
1909 let _task2 = manager
1910 .add_task("Standalone Task 2", None, None, None)
1911 .await
1912 .unwrap();
1913
1914 manager.start_task(task1.id, false).await.unwrap();
1916 let response = manager.done_task(false).await.unwrap();
1917
1918 match response.next_step_suggestion {
1920 NextStepSuggestion::NoParentContext {
1921 completed_task_id,
1922 completed_task_name,
1923 ..
1924 } => {
1925 assert_eq!(completed_task_id, task1.id);
1926 assert_eq!(completed_task_name, "Standalone Task 1");
1927 },
1928 _ => panic!("Expected NoParentContext suggestion"),
1929 }
1930 }
1931
1932 #[tokio::test]
1933 async fn test_pick_next_focused_subtask() {
1934 let ctx = TestContext::new().await;
1935 let manager = TaskManager::new(ctx.pool());
1936
1937 let parent = manager
1939 .add_task("Parent task", None, None, None)
1940 .await
1941 .unwrap();
1942 manager.start_task(parent.id, false).await.unwrap();
1943
1944 let subtask1 = manager
1946 .add_task("Subtask 1", None, Some(parent.id), None)
1947 .await
1948 .unwrap();
1949 let subtask2 = manager
1950 .add_task("Subtask 2", None, Some(parent.id), None)
1951 .await
1952 .unwrap();
1953
1954 manager
1956 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1957 .await
1958 .unwrap();
1959 manager
1960 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1961 .await
1962 .unwrap();
1963
1964 let response = manager.pick_next().await.unwrap();
1966
1967 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1968 assert!(response.task.is_some());
1969 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1970 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1971 }
1972
1973 #[tokio::test]
1974 async fn test_pick_next_top_level_task() {
1975 let ctx = TestContext::new().await;
1976 let manager = TaskManager::new(ctx.pool());
1977
1978 let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1980 let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
1981
1982 manager
1984 .update_task(task1.id, None, None, None, None, None, Some(5))
1985 .await
1986 .unwrap();
1987 manager
1988 .update_task(task2.id, None, None, None, None, None, Some(3))
1989 .await
1990 .unwrap();
1991
1992 let response = manager.pick_next().await.unwrap();
1994
1995 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1996 assert!(response.task.is_some());
1997 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1998 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1999 }
2000
2001 #[tokio::test]
2002 async fn test_pick_next_no_tasks() {
2003 let ctx = TestContext::new().await;
2004 let manager = TaskManager::new(ctx.pool());
2005
2006 let response = manager.pick_next().await.unwrap();
2008
2009 assert_eq!(response.suggestion_type, "NONE");
2010 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2011 assert!(response.message.is_some());
2012 }
2013
2014 #[tokio::test]
2015 async fn test_pick_next_all_completed() {
2016 let ctx = TestContext::new().await;
2017 let manager = TaskManager::new(ctx.pool());
2018
2019 let task = manager.add_task("Task 1", None, None, None).await.unwrap();
2021 manager.start_task(task.id, false).await.unwrap();
2022 manager.done_task(false).await.unwrap();
2023
2024 let response = manager.pick_next().await.unwrap();
2026
2027 assert_eq!(response.suggestion_type, "NONE");
2028 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2029 assert!(response.message.is_some());
2030 }
2031
2032 #[tokio::test]
2033 async fn test_pick_next_no_available_todos() {
2034 let ctx = TestContext::new().await;
2035 let manager = TaskManager::new(ctx.pool());
2036
2037 let parent = manager
2039 .add_task("Parent task", None, None, None)
2040 .await
2041 .unwrap();
2042 manager.start_task(parent.id, false).await.unwrap();
2043
2044 let subtask = manager
2046 .add_task("Subtask", None, Some(parent.id), None)
2047 .await
2048 .unwrap();
2049 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2051 .bind(subtask.id)
2052 .execute(ctx.pool())
2053 .await
2054 .unwrap();
2055
2056 sqlx::query(
2058 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
2059 )
2060 .bind(subtask.id.to_string())
2061 .execute(ctx.pool())
2062 .await
2063 .unwrap();
2064
2065 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2067 .bind(parent.id)
2068 .execute(ctx.pool())
2069 .await
2070 .unwrap();
2071
2072 let response = manager.pick_next().await.unwrap();
2075
2076 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2077 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2078 assert_eq!(response.task.as_ref().unwrap().status, "doing");
2079 }
2080
2081 #[tokio::test]
2082 async fn test_pick_next_priority_ordering() {
2083 let ctx = TestContext::new().await;
2084 let manager = TaskManager::new(ctx.pool());
2085
2086 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2088 manager.start_task(parent.id, false).await.unwrap();
2089
2090 let sub1 = manager
2092 .add_task("Priority 10", None, Some(parent.id), None)
2093 .await
2094 .unwrap();
2095 manager
2096 .update_task(sub1.id, None, None, None, None, None, Some(10))
2097 .await
2098 .unwrap();
2099
2100 let sub2 = manager
2101 .add_task("Priority 1", None, Some(parent.id), None)
2102 .await
2103 .unwrap();
2104 manager
2105 .update_task(sub2.id, None, None, None, None, None, Some(1))
2106 .await
2107 .unwrap();
2108
2109 let sub3 = manager
2110 .add_task("Priority 5", None, Some(parent.id), None)
2111 .await
2112 .unwrap();
2113 manager
2114 .update_task(sub3.id, None, None, None, None, None, Some(5))
2115 .await
2116 .unwrap();
2117
2118 let response = manager.pick_next().await.unwrap();
2120
2121 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2122 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2123 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2124 }
2125
2126 #[tokio::test]
2127 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2128 let ctx = TestContext::new().await;
2129 let manager = TaskManager::new(ctx.pool());
2130
2131 let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2133 manager.start_task(parent.id, false).await.unwrap();
2134
2135 let top_level = manager
2137 .add_task("Top level task", None, None, None)
2138 .await
2139 .unwrap();
2140
2141 let response = manager.pick_next().await.unwrap();
2143
2144 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2145 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2146 }
2147
2148 #[tokio::test]
2151 async fn test_get_task_with_events() {
2152 let ctx = TestContext::new().await;
2153 let task_mgr = TaskManager::new(ctx.pool());
2154 let event_mgr = EventManager::new(ctx.pool());
2155
2156 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2157
2158 event_mgr
2160 .add_event(task.id, "progress", "Event 1")
2161 .await
2162 .unwrap();
2163 event_mgr
2164 .add_event(task.id, "decision", "Event 2")
2165 .await
2166 .unwrap();
2167
2168 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2169
2170 assert_eq!(result.task.id, task.id);
2171 assert!(result.events_summary.is_some());
2172
2173 let summary = result.events_summary.unwrap();
2174 assert_eq!(summary.total_count, 2);
2175 assert_eq!(summary.recent_events.len(), 2);
2176 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2178 }
2179
2180 #[tokio::test]
2181 async fn test_get_task_with_events_nonexistent() {
2182 let ctx = TestContext::new().await;
2183 let task_mgr = TaskManager::new(ctx.pool());
2184
2185 let result = task_mgr.get_task_with_events(999).await;
2186 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2187 }
2188
2189 #[tokio::test]
2190 async fn test_get_task_with_many_events() {
2191 let ctx = TestContext::new().await;
2192 let task_mgr = TaskManager::new(ctx.pool());
2193 let event_mgr = EventManager::new(ctx.pool());
2194
2195 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2196
2197 for i in 0..20 {
2199 event_mgr
2200 .add_event(task.id, "test", &format!("Event {}", i))
2201 .await
2202 .unwrap();
2203 }
2204
2205 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2206 let summary = result.events_summary.unwrap();
2207
2208 assert_eq!(summary.total_count, 20);
2209 assert_eq!(summary.recent_events.len(), 10); }
2211
2212 #[tokio::test]
2213 async fn test_get_task_with_no_events() {
2214 let ctx = TestContext::new().await;
2215 let task_mgr = TaskManager::new(ctx.pool());
2216
2217 let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2218
2219 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2220 let summary = result.events_summary.unwrap();
2221
2222 assert_eq!(summary.total_count, 0);
2223 assert_eq!(summary.recent_events.len(), 0);
2224 }
2225
2226 #[tokio::test]
2227 async fn test_pick_next_tasks_zero_capacity() {
2228 let ctx = TestContext::new().await;
2229 let task_mgr = TaskManager::new(ctx.pool());
2230
2231 task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2232
2233 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2235 assert_eq!(results.len(), 0);
2236 }
2237
2238 #[tokio::test]
2239 async fn test_pick_next_tasks_capacity_exceeds_available() {
2240 let ctx = TestContext::new().await;
2241 let task_mgr = TaskManager::new(ctx.pool());
2242
2243 task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2244 task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2245
2246 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2248 assert_eq!(results.len(), 2); }
2250
2251 #[tokio::test]
2254 async fn test_get_task_context_root_task_no_relations() {
2255 let ctx = TestContext::new().await;
2256 let task_mgr = TaskManager::new(ctx.pool());
2257
2258 let task = task_mgr
2260 .add_task("Root task", None, None, None)
2261 .await
2262 .unwrap();
2263
2264 let context = task_mgr.get_task_context(task.id).await.unwrap();
2265
2266 assert_eq!(context.task.id, task.id);
2268 assert_eq!(context.task.name, "Root task");
2269
2270 assert_eq!(context.ancestors.len(), 0);
2272
2273 assert_eq!(context.siblings.len(), 0);
2275
2276 assert_eq!(context.children.len(), 0);
2278 }
2279
2280 #[tokio::test]
2281 async fn test_get_task_context_with_siblings() {
2282 let ctx = TestContext::new().await;
2283 let task_mgr = TaskManager::new(ctx.pool());
2284
2285 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2287 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2288 let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2289
2290 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2291
2292 assert_eq!(context.task.id, task2.id);
2294
2295 assert_eq!(context.ancestors.len(), 0);
2297
2298 assert_eq!(context.siblings.len(), 2);
2300 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2301 assert!(sibling_ids.contains(&task1.id));
2302 assert!(sibling_ids.contains(&task3.id));
2303 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2307 }
2308
2309 #[tokio::test]
2310 async fn test_get_task_context_with_parent() {
2311 let ctx = TestContext::new().await;
2312 let task_mgr = TaskManager::new(ctx.pool());
2313
2314 let parent = task_mgr
2316 .add_task("Parent task", None, None, None)
2317 .await
2318 .unwrap();
2319 let child = task_mgr
2320 .add_task("Child task", None, Some(parent.id), None)
2321 .await
2322 .unwrap();
2323
2324 let context = task_mgr.get_task_context(child.id).await.unwrap();
2325
2326 assert_eq!(context.task.id, child.id);
2328 assert_eq!(context.task.parent_id, Some(parent.id));
2329
2330 assert_eq!(context.ancestors.len(), 1);
2332 assert_eq!(context.ancestors[0].id, parent.id);
2333 assert_eq!(context.ancestors[0].name, "Parent task");
2334
2335 assert_eq!(context.siblings.len(), 0);
2337
2338 assert_eq!(context.children.len(), 0);
2340 }
2341
2342 #[tokio::test]
2343 async fn test_get_task_context_with_children() {
2344 let ctx = TestContext::new().await;
2345 let task_mgr = TaskManager::new(ctx.pool());
2346
2347 let parent = task_mgr
2349 .add_task("Parent task", None, None, None)
2350 .await
2351 .unwrap();
2352 let child1 = task_mgr
2353 .add_task("Child 1", None, Some(parent.id), None)
2354 .await
2355 .unwrap();
2356 let child2 = task_mgr
2357 .add_task("Child 2", None, Some(parent.id), None)
2358 .await
2359 .unwrap();
2360 let child3 = task_mgr
2361 .add_task("Child 3", None, Some(parent.id), None)
2362 .await
2363 .unwrap();
2364
2365 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2366
2367 assert_eq!(context.task.id, parent.id);
2369
2370 assert_eq!(context.ancestors.len(), 0);
2372
2373 assert_eq!(context.siblings.len(), 0);
2375
2376 assert_eq!(context.children.len(), 3);
2378 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2379 assert!(child_ids.contains(&child1.id));
2380 assert!(child_ids.contains(&child2.id));
2381 assert!(child_ids.contains(&child3.id));
2382 }
2383
2384 #[tokio::test]
2385 async fn test_get_task_context_multi_level_hierarchy() {
2386 let ctx = TestContext::new().await;
2387 let task_mgr = TaskManager::new(ctx.pool());
2388
2389 let grandparent = task_mgr
2391 .add_task("Grandparent", None, None, None)
2392 .await
2393 .unwrap();
2394 let parent = task_mgr
2395 .add_task("Parent", None, Some(grandparent.id), None)
2396 .await
2397 .unwrap();
2398 let child = task_mgr
2399 .add_task("Child", None, Some(parent.id), None)
2400 .await
2401 .unwrap();
2402
2403 let context = task_mgr.get_task_context(child.id).await.unwrap();
2404
2405 assert_eq!(context.task.id, child.id);
2407
2408 assert_eq!(context.ancestors.len(), 2);
2410 assert_eq!(context.ancestors[0].id, parent.id);
2411 assert_eq!(context.ancestors[0].name, "Parent");
2412 assert_eq!(context.ancestors[1].id, grandparent.id);
2413 assert_eq!(context.ancestors[1].name, "Grandparent");
2414
2415 assert_eq!(context.siblings.len(), 0);
2417
2418 assert_eq!(context.children.len(), 0);
2420 }
2421
2422 #[tokio::test]
2423 async fn test_get_task_context_complex_family_tree() {
2424 let ctx = TestContext::new().await;
2425 let task_mgr = TaskManager::new(ctx.pool());
2426
2427 let root = task_mgr.add_task("Root", None, None, None).await.unwrap();
2435 let child1 = task_mgr
2436 .add_task("Child1", None, Some(root.id), None)
2437 .await
2438 .unwrap();
2439 let child2 = task_mgr
2440 .add_task("Child2", None, Some(root.id), None)
2441 .await
2442 .unwrap();
2443 let grandchild1 = task_mgr
2444 .add_task("Grandchild1", None, Some(child1.id), None)
2445 .await
2446 .unwrap();
2447 let grandchild2 = task_mgr
2448 .add_task("Grandchild2", None, Some(child1.id), None)
2449 .await
2450 .unwrap();
2451
2452 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2454
2455 assert_eq!(context.task.id, grandchild2.id);
2457
2458 assert_eq!(context.ancestors.len(), 2);
2460 assert_eq!(context.ancestors[0].id, child1.id);
2461 assert_eq!(context.ancestors[1].id, root.id);
2462
2463 assert_eq!(context.siblings.len(), 1);
2465 assert_eq!(context.siblings[0].id, grandchild1.id);
2466
2467 assert_eq!(context.children.len(), 0);
2469
2470 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2472 assert_eq!(context_child1.ancestors.len(), 1);
2473 assert_eq!(context_child1.ancestors[0].id, root.id);
2474 assert_eq!(context_child1.siblings.len(), 1);
2475 assert_eq!(context_child1.siblings[0].id, child2.id);
2476 assert_eq!(context_child1.children.len(), 2);
2477 }
2478
2479 #[tokio::test]
2480 async fn test_get_task_context_respects_priority_ordering() {
2481 let ctx = TestContext::new().await;
2482 let task_mgr = TaskManager::new(ctx.pool());
2483
2484 let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2486
2487 let child_low = task_mgr
2489 .add_task("Low priority", None, Some(parent.id), None)
2490 .await
2491 .unwrap();
2492 let _ = task_mgr
2493 .update_task(child_low.id, None, None, None, None, None, Some(10))
2494 .await
2495 .unwrap();
2496
2497 let child_high = task_mgr
2498 .add_task("High priority", None, Some(parent.id), None)
2499 .await
2500 .unwrap();
2501 let _ = task_mgr
2502 .update_task(child_high.id, None, None, None, None, None, Some(1))
2503 .await
2504 .unwrap();
2505
2506 let child_medium = task_mgr
2507 .add_task("Medium priority", None, Some(parent.id), None)
2508 .await
2509 .unwrap();
2510 let _ = task_mgr
2511 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2512 .await
2513 .unwrap();
2514
2515 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2516
2517 assert_eq!(context.children.len(), 3);
2519 assert_eq!(context.children[0].priority, Some(1));
2520 assert_eq!(context.children[1].priority, Some(5));
2521 assert_eq!(context.children[2].priority, Some(10));
2522 }
2523
2524 #[tokio::test]
2525 async fn test_get_task_context_nonexistent_task() {
2526 let ctx = TestContext::new().await;
2527 let task_mgr = TaskManager::new(ctx.pool());
2528
2529 let result = task_mgr.get_task_context(99999).await;
2530 assert!(result.is_err());
2531 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2532 }
2533
2534 #[tokio::test]
2535 async fn test_get_task_context_handles_null_priority() {
2536 let ctx = TestContext::new().await;
2537 let task_mgr = TaskManager::new(ctx.pool());
2538
2539 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2541 let _ = task_mgr
2542 .update_task(task1.id, None, None, None, None, None, Some(1))
2543 .await
2544 .unwrap();
2545
2546 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2547 let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2550 let _ = task_mgr
2551 .update_task(task3.id, None, None, None, None, None, Some(5))
2552 .await
2553 .unwrap();
2554
2555 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2556
2557 assert_eq!(context.siblings.len(), 2);
2559 assert_eq!(context.siblings[0].id, task1.id);
2561 assert_eq!(context.siblings[0].priority, Some(1));
2562 assert_eq!(context.siblings[1].id, task3.id);
2564 assert_eq!(context.siblings[1].priority, Some(5));
2565 }
2566
2567 #[tokio::test]
2568 async fn test_pick_next_tasks_priority_order() {
2569 let ctx = TestContext::new().await;
2570 let task_mgr = TaskManager::new(ctx.pool());
2571
2572 let critical = task_mgr
2574 .add_task("Critical Task", None, None, None)
2575 .await
2576 .unwrap();
2577 task_mgr
2578 .update_task(critical.id, None, None, None, None, None, Some(1))
2579 .await
2580 .unwrap();
2581
2582 let low = task_mgr
2583 .add_task("Low Task", None, None, None)
2584 .await
2585 .unwrap();
2586 task_mgr
2587 .update_task(low.id, None, None, None, None, None, Some(4))
2588 .await
2589 .unwrap();
2590
2591 let high = task_mgr
2592 .add_task("High Task", None, None, None)
2593 .await
2594 .unwrap();
2595 task_mgr
2596 .update_task(high.id, None, None, None, None, None, Some(2))
2597 .await
2598 .unwrap();
2599
2600 let medium = task_mgr
2601 .add_task("Medium Task", None, None, None)
2602 .await
2603 .unwrap();
2604 task_mgr
2605 .update_task(medium.id, None, None, None, None, None, Some(3))
2606 .await
2607 .unwrap();
2608
2609 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2611
2612 assert_eq!(tasks.len(), 4);
2613 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); }
2618
2619 #[tokio::test]
2620 async fn test_pick_next_prefers_doing_over_todo() {
2621 let ctx = TestContext::new().await;
2622 let task_mgr = TaskManager::new(ctx.pool());
2623 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2624
2625 let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2627 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
2628 workspace_mgr
2629 .set_current_task(parent_started.task.id)
2630 .await
2631 .unwrap();
2632
2633 let doing_subtask = task_mgr
2635 .add_task("Doing Subtask", None, Some(parent.id), None)
2636 .await
2637 .unwrap();
2638 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
2639 workspace_mgr.set_current_task(parent.id).await.unwrap();
2641
2642 let _todo_subtask = task_mgr
2643 .add_task("Todo Subtask", None, Some(parent.id), None)
2644 .await
2645 .unwrap();
2646
2647 let result = task_mgr.pick_next().await.unwrap();
2649
2650 if let Some(task) = result.task {
2651 assert_eq!(
2652 task.id, doing_subtask.id,
2653 "Should recommend doing subtask over todo subtask"
2654 );
2655 assert_eq!(task.status, "doing");
2656 } else {
2657 panic!("Expected a task recommendation");
2658 }
2659 }
2660
2661 #[tokio::test]
2662 async fn test_multiple_doing_tasks_allowed() {
2663 let ctx = TestContext::new().await;
2664 let task_mgr = TaskManager::new(ctx.pool());
2665 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2666
2667 let task_a = task_mgr.add_task("Task A", None, None, None).await.unwrap();
2669 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
2670 assert_eq!(task_a_started.task.status, "doing");
2671
2672 let current = workspace_mgr.get_current_task().await.unwrap();
2674 assert_eq!(current.current_task_id, Some(task_a.id));
2675
2676 let task_b = task_mgr.add_task("Task B", None, None, None).await.unwrap();
2678 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
2679 assert_eq!(task_b_started.task.status, "doing");
2680
2681 let current = workspace_mgr.get_current_task().await.unwrap();
2683 assert_eq!(current.current_task_id, Some(task_b.id));
2684
2685 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
2687 assert_eq!(
2688 task_a_after.status, "doing",
2689 "Task A should remain doing even though it is not current"
2690 );
2691
2692 let doing_tasks: Vec<Task> = sqlx::query_as(
2694 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
2695 FROM tasks WHERE status = 'doing' ORDER BY id"#
2696 )
2697 .fetch_all(ctx.pool())
2698 .await
2699 .unwrap();
2700
2701 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
2702 assert_eq!(doing_tasks[0].id, task_a.id);
2703 assert_eq!(doing_tasks[1].id, task_b.id);
2704 }
2705 #[tokio::test]
2706 async fn test_find_tasks_pagination() {
2707 let ctx = TestContext::new().await;
2708 let task_mgr = TaskManager::new(ctx.pool());
2709
2710 for i in 0..15 {
2712 task_mgr
2713 .add_task(&format!("Task {}", i), None, None, None)
2714 .await
2715 .unwrap();
2716 }
2717
2718 let page1 = task_mgr
2720 .find_tasks(None, None, None, Some(10), Some(0))
2721 .await
2722 .unwrap();
2723 assert_eq!(page1.tasks.len(), 10);
2724 assert_eq!(page1.total_count, 15);
2725 assert!(page1.has_more);
2726 assert_eq!(page1.offset, 0);
2727
2728 let page2 = task_mgr
2730 .find_tasks(None, None, None, Some(10), Some(10))
2731 .await
2732 .unwrap();
2733 assert_eq!(page2.tasks.len(), 5);
2734 assert_eq!(page2.total_count, 15);
2735 assert!(!page2.has_more);
2736 assert_eq!(page2.offset, 10);
2737 }
2738}
2739
2740