1use crate::db::models::{
2 DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, ParentTaskInfo, PickNextResponse,
3 SpawnSubtaskResponse, SubtaskInfo, Task, TaskSearchResult, TaskWithEvents, WorkspaceStatus,
4};
5use crate::error::{IntentError, Result};
6use chrono::Utc;
7use sqlx::{Row, SqlitePool};
8use std::sync::Arc;
9
10pub use crate::db::models::TaskContext;
11pub struct TaskManager<'a> {
12 pool: &'a SqlitePool,
13 notifier: crate::notifications::NotificationSender,
14 project_path: Option<String>,
15}
16
17impl<'a> TaskManager<'a> {
18 pub fn new(pool: &'a SqlitePool) -> Self {
19 Self {
20 pool,
21 notifier: crate::notifications::NotificationSender::new(None, None),
22 project_path: None,
23 }
24 }
25
26 pub fn with_mcp_notifier(
28 pool: &'a SqlitePool,
29 project_path: String,
30 mcp_notifier: tokio::sync::mpsc::UnboundedSender<String>,
31 ) -> Self {
32 Self {
33 pool,
34 notifier: crate::notifications::NotificationSender::new(None, Some(mcp_notifier)),
35 project_path: Some(project_path),
36 }
37 }
38
39 pub fn with_websocket(
41 pool: &'a SqlitePool,
42 ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
43 project_path: String,
44 ) -> Self {
45 Self {
46 pool,
47 notifier: crate::notifications::NotificationSender::new(Some(ws_state), None),
48 project_path: Some(project_path),
49 }
50 }
51
52 async fn notify_task_created(&self, task: &Task) {
54 use crate::dashboard::websocket::DatabaseOperationPayload;
55
56 let Some(project_path) = &self.project_path else {
57 return;
58 };
59
60 let task_json = match serde_json::to_value(task) {
61 Ok(json) => json,
62 Err(e) => {
63 tracing::warn!("Failed to serialize task for notification: {}", e);
64 return;
65 },
66 };
67
68 let payload =
69 DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
70 self.notifier.send(payload).await;
71 }
72
73 async fn notify_task_updated(&self, task: &Task) {
75 use crate::dashboard::websocket::DatabaseOperationPayload;
76
77 let Some(project_path) = &self.project_path else {
78 return;
79 };
80
81 let task_json = match serde_json::to_value(task) {
82 Ok(json) => json,
83 Err(e) => {
84 tracing::warn!("Failed to serialize task for notification: {}", e);
85 return;
86 },
87 };
88
89 let payload =
90 DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
91 self.notifier.send(payload).await;
92 }
93
94 async fn notify_task_deleted(&self, task_id: i64) {
96 use crate::dashboard::websocket::DatabaseOperationPayload;
97
98 let Some(project_path) = &self.project_path else {
99 return;
100 };
101
102 let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
103 self.notifier.send(payload).await;
104 }
105
106 pub async fn add_task(
108 &self,
109 name: &str,
110 spec: Option<&str>,
111 parent_id: Option<i64>,
112 ) -> Result<Task> {
113 if let Some(pid) = parent_id {
115 self.check_task_exists(pid).await?;
116 }
117
118 let now = Utc::now();
119
120 let result = sqlx::query(
121 r#"
122 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
123 VALUES (?, ?, ?, 'todo', ?)
124 "#,
125 )
126 .bind(name)
127 .bind(spec)
128 .bind(parent_id)
129 .bind(now)
130 .execute(self.pool)
131 .await?;
132
133 let id = result.last_insert_rowid();
134 let task = self.get_task(id).await?;
135
136 self.notify_task_created(&task).await;
138
139 Ok(task)
140 }
141
142 pub async fn get_task(&self, id: i64) -> Result<Task> {
144 let task = sqlx::query_as::<_, Task>(
145 r#"
146 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
147 FROM tasks
148 WHERE id = ?
149 "#,
150 )
151 .bind(id)
152 .fetch_optional(self.pool)
153 .await?
154 .ok_or(IntentError::TaskNotFound(id))?;
155
156 Ok(task)
157 }
158
159 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
161 let task = self.get_task(id).await?;
162 let events_summary = self.get_events_summary(id).await?;
163
164 Ok(TaskWithEvents {
165 task,
166 events_summary: Some(events_summary),
167 })
168 }
169
170 pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
179 let mut chain = Vec::new();
180 let mut current_id = Some(task_id);
181
182 while let Some(id) = current_id {
183 let task = self.get_task(id).await?;
184 current_id = task.parent_id;
185 chain.push(task);
186 }
187
188 Ok(chain)
189 }
190
191 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
199 let task = self.get_task(id).await?;
201
202 let mut ancestors = Vec::new();
204 let mut current_parent_id = task.parent_id;
205
206 while let Some(parent_id) = current_parent_id {
207 let parent = self.get_task(parent_id).await?;
208 current_parent_id = parent.parent_id;
209 ancestors.push(parent);
210 }
211
212 let siblings = if let Some(parent_id) = task.parent_id {
214 sqlx::query_as::<_, Task>(
215 r#"
216 SELECT id, parent_id, name, spec, status, complexity, priority,
217 first_todo_at, first_doing_at, first_done_at, active_form
218 FROM tasks
219 WHERE parent_id = ? AND id != ?
220 ORDER BY priority ASC NULLS LAST, id ASC
221 "#,
222 )
223 .bind(parent_id)
224 .bind(id)
225 .fetch_all(self.pool)
226 .await?
227 } else {
228 sqlx::query_as::<_, Task>(
230 r#"
231 SELECT id, parent_id, name, spec, status, complexity, priority,
232 first_todo_at, first_doing_at, first_done_at, active_form
233 FROM tasks
234 WHERE parent_id IS NULL AND id != ?
235 ORDER BY priority ASC NULLS LAST, id ASC
236 "#,
237 )
238 .bind(id)
239 .fetch_all(self.pool)
240 .await?
241 };
242
243 let children = sqlx::query_as::<_, Task>(
245 r#"
246 SELECT id, parent_id, name, spec, status, complexity, priority,
247 first_todo_at, first_doing_at, first_done_at, active_form
248 FROM tasks
249 WHERE parent_id = ?
250 ORDER BY priority ASC NULLS LAST, id ASC
251 "#,
252 )
253 .bind(id)
254 .fetch_all(self.pool)
255 .await?;
256
257 let blocking_tasks = sqlx::query_as::<_, Task>(
259 r#"
260 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
261 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form
262 FROM tasks t
263 JOIN dependencies d ON t.id = d.blocking_task_id
264 WHERE d.blocked_task_id = ?
265 ORDER BY t.priority ASC NULLS LAST, t.id ASC
266 "#,
267 )
268 .bind(id)
269 .fetch_all(self.pool)
270 .await?;
271
272 let blocked_by_tasks = sqlx::query_as::<_, Task>(
274 r#"
275 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
276 t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form
277 FROM tasks t
278 JOIN dependencies d ON t.id = d.blocked_task_id
279 WHERE d.blocking_task_id = ?
280 ORDER BY t.priority ASC NULLS LAST, t.id ASC
281 "#,
282 )
283 .bind(id)
284 .fetch_all(self.pool)
285 .await?;
286
287 Ok(TaskContext {
288 task,
289 ancestors,
290 siblings,
291 children,
292 dependencies: crate::db::models::TaskDependencies {
293 blocking_tasks,
294 blocked_by_tasks,
295 },
296 })
297 }
298
299 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
301 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
302 .bind(task_id)
303 .fetch_one(self.pool)
304 .await?;
305
306 let recent_events = sqlx::query_as::<_, Event>(
307 r#"
308 SELECT id, task_id, timestamp, log_type, discussion_data
309 FROM events
310 WHERE task_id = ?
311 ORDER BY timestamp DESC
312 LIMIT 10
313 "#,
314 )
315 .bind(task_id)
316 .fetch_all(self.pool)
317 .await?;
318
319 Ok(EventsSummary {
320 total_count,
321 recent_events,
322 })
323 }
324
325 #[allow(clippy::too_many_arguments)]
327 pub async fn update_task(
328 &self,
329 id: i64,
330 name: Option<&str>,
331 spec: Option<&str>,
332 parent_id: Option<Option<i64>>,
333 status: Option<&str>,
334 complexity: Option<i32>,
335 priority: Option<i32>,
336 ) -> Result<Task> {
337 let task = self.get_task(id).await?;
339
340 if let Some(s) = status {
342 if !["todo", "doing", "done"].contains(&s) {
343 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
344 }
345 }
346
347 if let Some(Some(pid)) = parent_id {
349 if pid == id {
350 return Err(IntentError::CircularDependency {
351 blocking_task_id: pid,
352 blocked_task_id: id,
353 });
354 }
355 self.check_task_exists(pid).await?;
356 self.check_circular_dependency(id, pid).await?;
357 }
358
359 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
361 sqlx::QueryBuilder::new("UPDATE tasks SET ");
362 let mut has_updates = false;
363
364 if let Some(n) = name {
365 if has_updates {
366 builder.push(", ");
367 }
368 builder.push("name = ").push_bind(n);
369 has_updates = true;
370 }
371
372 if let Some(s) = spec {
373 if has_updates {
374 builder.push(", ");
375 }
376 builder.push("spec = ").push_bind(s);
377 has_updates = true;
378 }
379
380 if let Some(pid) = parent_id {
381 if has_updates {
382 builder.push(", ");
383 }
384 match pid {
385 Some(p) => {
386 builder.push("parent_id = ").push_bind(p);
387 },
388 None => {
389 builder.push("parent_id = NULL");
390 },
391 }
392 has_updates = true;
393 }
394
395 if let Some(c) = complexity {
396 if has_updates {
397 builder.push(", ");
398 }
399 builder.push("complexity = ").push_bind(c);
400 has_updates = true;
401 }
402
403 if let Some(p) = priority {
404 if has_updates {
405 builder.push(", ");
406 }
407 builder.push("priority = ").push_bind(p);
408 has_updates = true;
409 }
410
411 if let Some(s) = status {
412 if has_updates {
413 builder.push(", ");
414 }
415 builder.push("status = ").push_bind(s);
416 has_updates = true;
417
418 let now = Utc::now();
420 let timestamp = now.to_rfc3339();
421 match s {
422 "todo" if task.first_todo_at.is_none() => {
423 builder.push(", first_todo_at = ").push_bind(timestamp);
424 },
425 "doing" if task.first_doing_at.is_none() => {
426 builder.push(", first_doing_at = ").push_bind(timestamp);
427 },
428 "done" if task.first_done_at.is_none() => {
429 builder.push(", first_done_at = ").push_bind(timestamp);
430 },
431 _ => {},
432 }
433 }
434
435 if !has_updates {
436 return Ok(task);
437 }
438
439 builder.push(" WHERE id = ").push_bind(id);
440
441 builder.build().execute(self.pool).await?;
442
443 let task = self.get_task(id).await?;
444
445 self.notify_task_updated(&task).await;
447
448 Ok(task)
449 }
450
451 pub async fn delete_task(&self, id: i64) -> Result<()> {
453 self.check_task_exists(id).await?;
454
455 sqlx::query("DELETE FROM tasks WHERE id = ?")
456 .bind(id)
457 .execute(self.pool)
458 .await?;
459
460 self.notify_task_deleted(id).await;
462
463 Ok(())
464 }
465
466 pub async fn find_tasks(
468 &self,
469 status: Option<&str>,
470 parent_id: Option<Option<i64>>,
471 ) -> Result<Vec<Task>> {
472 let mut query = String::from(
473 "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form FROM tasks WHERE 1=1"
474 );
475 let mut conditions = Vec::new();
476
477 if let Some(s) = status {
478 query.push_str(" AND status = ?");
479 conditions.push(s.to_string());
480 }
481
482 if let Some(pid) = parent_id {
483 if let Some(p) = pid {
484 query.push_str(" AND parent_id = ?");
485 conditions.push(p.to_string());
486 } else {
487 query.push_str(" AND parent_id IS NULL");
488 }
489 }
490
491 query.push_str(" ORDER BY id");
492
493 let mut q = sqlx::query_as::<_, Task>(&query);
494 for cond in conditions {
495 q = q.bind(cond);
496 }
497
498 let tasks = q.fetch_all(self.pool).await?;
499 Ok(tasks)
500 }
501
502 pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
505 if query.trim().is_empty() {
507 return Ok(Vec::new());
508 }
509
510 let has_searchable = query
513 .chars()
514 .any(|c| c.is_alphanumeric() || crate::search::is_cjk_char(c));
515 if !has_searchable {
516 return Ok(Vec::new());
517 }
518
519 if crate::search::needs_like_fallback(query) {
522 self.search_tasks_like(query).await
523 } else {
524 self.search_tasks_fts5(query).await
525 }
526 }
527
528 async fn search_tasks_fts5(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
530 let escaped_query = crate::search::escape_fts5(query);
532
533 let results = sqlx::query(
537 r#"
538 SELECT
539 t.id,
540 t.parent_id,
541 t.name,
542 t.spec,
543 t.status,
544 t.complexity,
545 t.priority,
546 t.first_todo_at,
547 t.first_doing_at,
548 t.first_done_at,
549 t.active_form,
550 COALESCE(
551 snippet(tasks_fts, 1, '**', '**', '...', 15),
552 snippet(tasks_fts, 0, '**', '**', '...', 15)
553 ) as match_snippet
554 FROM tasks_fts
555 INNER JOIN tasks t ON tasks_fts.rowid = t.id
556 WHERE tasks_fts MATCH ?
557 ORDER BY rank
558 "#,
559 )
560 .bind(&escaped_query)
561 .fetch_all(self.pool)
562 .await?;
563
564 let mut search_results = Vec::new();
565 for row in results {
566 let task = Task {
567 id: row.get("id"),
568 parent_id: row.get("parent_id"),
569 name: row.get("name"),
570 spec: row.get("spec"),
571 status: row.get("status"),
572 complexity: row.get("complexity"),
573 priority: row.get("priority"),
574 first_todo_at: row.get("first_todo_at"),
575 first_doing_at: row.get("first_doing_at"),
576 first_done_at: row.get("first_done_at"),
577 active_form: row.get("active_form"),
578 };
579 let match_snippet: String = row.get("match_snippet");
580
581 search_results.push(TaskSearchResult {
582 task,
583 match_snippet,
584 });
585 }
586
587 Ok(search_results)
588 }
589
590 async fn search_tasks_like(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
592 let pattern = format!("%{}%", query);
593
594 let results = sqlx::query(
595 r#"
596 SELECT
597 id,
598 parent_id,
599 name,
600 spec,
601 status,
602 complexity,
603 priority,
604 first_todo_at,
605 first_doing_at,
606 first_done_at,
607 active_form
608 FROM tasks
609 WHERE name LIKE ? OR spec LIKE ?
610 ORDER BY name
611 "#,
612 )
613 .bind(&pattern)
614 .bind(&pattern)
615 .fetch_all(self.pool)
616 .await?;
617
618 let mut search_results = Vec::new();
619 for row in results {
620 let task = Task {
621 id: row.get("id"),
622 parent_id: row.get("parent_id"),
623 name: row.get("name"),
624 spec: row.get("spec"),
625 status: row.get("status"),
626 complexity: row.get("complexity"),
627 priority: row.get("priority"),
628 first_todo_at: row.get("first_todo_at"),
629 first_doing_at: row.get("first_doing_at"),
630 first_done_at: row.get("first_done_at"),
631 active_form: row.get("active_form"),
632 };
633
634 let name: String = row.get("name");
636 let spec: Option<String> = row.get("spec");
637
638 let match_snippet = if name.contains(query) {
639 format!("**{}**", name)
640 } else if let Some(ref s) = spec {
641 if s.contains(query) {
642 format!("**{}**", s)
643 } else {
644 name.clone()
645 }
646 } else {
647 name
648 };
649
650 search_results.push(TaskSearchResult {
651 task,
652 match_snippet,
653 });
654 }
655
656 Ok(search_results)
657 }
658
659 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
661 use crate::dependencies::get_incomplete_blocking_tasks;
663 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
664 return Err(IntentError::TaskBlocked {
665 task_id: id,
666 blocking_task_ids: blocking_tasks,
667 });
668 }
669
670 let mut tx = self.pool.begin().await?;
671
672 let now = Utc::now();
673
674 sqlx::query(
676 r#"
677 UPDATE tasks
678 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
679 WHERE id = ?
680 "#,
681 )
682 .bind(now)
683 .bind(id)
684 .execute(&mut *tx)
685 .await?;
686
687 sqlx::query(
689 r#"
690 INSERT OR REPLACE INTO workspace_state (key, value)
691 VALUES ('current_task_id', ?)
692 "#,
693 )
694 .bind(id.to_string())
695 .execute(&mut *tx)
696 .await?;
697
698 tx.commit().await?;
699
700 if with_events {
701 self.get_task_with_events(id).await
702 } else {
703 let task = self.get_task(id).await?;
704 Ok(TaskWithEvents {
705 task,
706 events_summary: None,
707 })
708 }
709 }
710
711 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
715 let mut tx = self.pool.begin().await?;
716
717 let current_task_id: Option<String> =
719 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
720 .fetch_optional(&mut *tx)
721 .await?;
722
723 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
724 IntentError::InvalidInput(
725 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
726 ),
727 )?;
728
729 let task_info: (String, Option<i64>) =
731 sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
732 .bind(id)
733 .fetch_one(&mut *tx)
734 .await?;
735 let (task_name, parent_id) = task_info;
736
737 let uncompleted_children: i64 = sqlx::query_scalar(
739 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
740 )
741 .bind(id)
742 .fetch_one(&mut *tx)
743 .await?;
744
745 if uncompleted_children > 0 {
746 return Err(IntentError::UncompletedChildren);
747 }
748
749 let now = Utc::now();
750
751 sqlx::query(
753 r#"
754 UPDATE tasks
755 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
756 WHERE id = ?
757 "#,
758 )
759 .bind(now)
760 .bind(id)
761 .execute(&mut *tx)
762 .await?;
763
764 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
766 .execute(&mut *tx)
767 .await?;
768
769 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
771 let remaining_siblings: i64 = sqlx::query_scalar(
773 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
774 )
775 .bind(parent_task_id)
776 .bind(id)
777 .fetch_one(&mut *tx)
778 .await?;
779
780 if remaining_siblings == 0 {
781 let parent_name: String =
783 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
784 .bind(parent_task_id)
785 .fetch_one(&mut *tx)
786 .await?;
787
788 NextStepSuggestion::ParentIsReady {
789 message: format!(
790 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
791 parent_task_id, parent_name
792 ),
793 parent_task_id,
794 parent_task_name: parent_name,
795 }
796 } else {
797 let parent_name: String =
799 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
800 .bind(parent_task_id)
801 .fetch_one(&mut *tx)
802 .await?;
803
804 NextStepSuggestion::SiblingTasksRemain {
805 message: format!(
806 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
807 id, parent_task_id, parent_name
808 ),
809 parent_task_id,
810 parent_task_name: parent_name,
811 remaining_siblings_count: remaining_siblings,
812 }
813 }
814 } else {
815 let child_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_CHILDREN_TOTAL)
817 .bind(id)
818 .fetch_one(&mut *tx)
819 .await?;
820
821 if child_count > 0 {
822 NextStepSuggestion::TopLevelTaskCompleted {
824 message: format!(
825 "Top-level task #{} '{}' has been completed. Well done!",
826 id, task_name
827 ),
828 completed_task_id: id,
829 completed_task_name: task_name.clone(),
830 }
831 } else {
832 let remaining_tasks: i64 = sqlx::query_scalar(
834 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
835 )
836 .bind(id)
837 .fetch_one(&mut *tx)
838 .await?;
839
840 if remaining_tasks == 0 {
841 NextStepSuggestion::WorkspaceIsClear {
842 message: format!(
843 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
844 id
845 ),
846 completed_task_id: id,
847 }
848 } else {
849 NextStepSuggestion::NoParentContext {
850 message: format!("Task #{} '{}' has been completed.", id, task_name),
851 completed_task_id: id,
852 completed_task_name: task_name.clone(),
853 }
854 }
855 }
856 };
857
858 tx.commit().await?;
859
860 let completed_task = self.get_task(id).await?;
861
862 Ok(DoneTaskResponse {
863 completed_task,
864 workspace_status: WorkspaceStatus {
865 current_task_id: None,
866 },
867 next_step_suggestion,
868 })
869 }
870
871 async fn check_task_exists(&self, id: i64) -> Result<()> {
873 let exists: bool = sqlx::query_scalar(crate::sql_constants::CHECK_TASK_EXISTS)
874 .bind(id)
875 .fetch_one(self.pool)
876 .await?;
877
878 if !exists {
879 return Err(IntentError::TaskNotFound(id));
880 }
881
882 Ok(())
883 }
884
885 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
887 let mut current_id = new_parent_id;
888
889 loop {
890 if current_id == task_id {
891 return Err(IntentError::CircularDependency {
892 blocking_task_id: new_parent_id,
893 blocked_task_id: task_id,
894 });
895 }
896
897 let parent: Option<i64> =
898 sqlx::query_scalar(crate::sql_constants::SELECT_TASK_PARENT_ID)
899 .bind(current_id)
900 .fetch_optional(self.pool)
901 .await?;
902
903 match parent {
904 Some(pid) => current_id = pid,
905 None => break,
906 }
907 }
908
909 Ok(())
910 }
911 pub async fn spawn_subtask(
915 &self,
916 name: &str,
917 spec: Option<&str>,
918 ) -> Result<SpawnSubtaskResponse> {
919 let current_task_id: Option<String> =
921 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
922 .fetch_optional(self.pool)
923 .await?;
924
925 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
926 IntentError::InvalidInput("No current task to create subtask under".to_string()),
927 )?;
928
929 let parent_name: String = sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
931 .bind(parent_id)
932 .fetch_one(self.pool)
933 .await?;
934
935 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
937
938 self.start_task(subtask.id, false).await?;
941
942 Ok(SpawnSubtaskResponse {
943 subtask: SubtaskInfo {
944 id: subtask.id,
945 name: subtask.name,
946 parent_id,
947 status: "doing".to_string(),
948 },
949 parent_task: ParentTaskInfo {
950 id: parent_id,
951 name: parent_name,
952 },
953 })
954 }
955
956 pub async fn pick_next_tasks(
969 &self,
970 max_count: usize,
971 capacity_limit: usize,
972 ) -> Result<Vec<Task>> {
973 let mut tx = self.pool.begin().await?;
974
975 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
977 .fetch_one(&mut *tx)
978 .await?;
979
980 let available = capacity_limit.saturating_sub(doing_count as usize);
982 if available == 0 {
983 return Ok(vec![]);
984 }
985
986 let limit = std::cmp::min(max_count, available);
987
988 let todo_tasks = sqlx::query_as::<_, Task>(
990 r#"
991 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
992 FROM tasks
993 WHERE status = 'todo'
994 ORDER BY
995 COALESCE(priority, 0) ASC,
996 COALESCE(complexity, 5) ASC,
997 id ASC
998 LIMIT ?
999 "#,
1000 )
1001 .bind(limit as i64)
1002 .fetch_all(&mut *tx)
1003 .await?;
1004
1005 if todo_tasks.is_empty() {
1006 return Ok(vec![]);
1007 }
1008
1009 let now = Utc::now();
1010
1011 for task in &todo_tasks {
1013 sqlx::query(
1014 r#"
1015 UPDATE tasks
1016 SET status = 'doing',
1017 first_doing_at = COALESCE(first_doing_at, ?)
1018 WHERE id = ?
1019 "#,
1020 )
1021 .bind(now)
1022 .bind(task.id)
1023 .execute(&mut *tx)
1024 .await?;
1025 }
1026
1027 tx.commit().await?;
1028
1029 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1031 let placeholders = vec!["?"; task_ids.len()].join(",");
1032 let query = format!(
1033 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
1034 FROM tasks WHERE id IN ({})
1035 ORDER BY
1036 COALESCE(priority, 0) ASC,
1037 COALESCE(complexity, 5) ASC,
1038 id ASC",
1039 placeholders
1040 );
1041
1042 let mut q = sqlx::query_as::<_, Task>(&query);
1043 for id in task_ids {
1044 q = q.bind(id);
1045 }
1046
1047 let updated_tasks = q.fetch_all(self.pool).await?;
1048 Ok(updated_tasks)
1049 }
1050
1051 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1060 let current_task_id: Option<String> =
1062 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1063 .fetch_optional(self.pool)
1064 .await?;
1065
1066 if let Some(current_id_str) = current_task_id.as_ref() {
1067 if let Ok(current_id) = current_id_str.parse::<i64>() {
1068 let doing_subtasks = sqlx::query_as::<_, Task>(
1071 r#"
1072 SELECT id, parent_id, name, spec, status, complexity, priority,
1073 first_todo_at, first_doing_at, first_done_at, active_form
1074 FROM tasks
1075 WHERE parent_id = ? AND status = 'doing'
1076 AND NOT EXISTS (
1077 SELECT 1 FROM dependencies d
1078 JOIN tasks bt ON d.blocking_task_id = bt.id
1079 WHERE d.blocked_task_id = tasks.id
1080 AND bt.status != 'done'
1081 )
1082 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1083 LIMIT 1
1084 "#,
1085 )
1086 .bind(current_id)
1087 .fetch_optional(self.pool)
1088 .await?;
1089
1090 if let Some(task) = doing_subtasks {
1091 return Ok(PickNextResponse::focused_subtask(task));
1092 }
1093
1094 let todo_subtasks = sqlx::query_as::<_, Task>(
1096 r#"
1097 SELECT id, parent_id, name, spec, status, complexity, priority,
1098 first_todo_at, first_doing_at, first_done_at, active_form
1099 FROM tasks
1100 WHERE parent_id = ? AND status = 'todo'
1101 AND NOT EXISTS (
1102 SELECT 1 FROM dependencies d
1103 JOIN tasks bt ON d.blocking_task_id = bt.id
1104 WHERE d.blocked_task_id = tasks.id
1105 AND bt.status != 'done'
1106 )
1107 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1108 LIMIT 1
1109 "#,
1110 )
1111 .bind(current_id)
1112 .fetch_optional(self.pool)
1113 .await?;
1114
1115 if let Some(task) = todo_subtasks {
1116 return Ok(PickNextResponse::focused_subtask(task));
1117 }
1118 }
1119 }
1120
1121 let doing_top_level = if let Some(current_id_str) = current_task_id.as_ref() {
1124 if let Ok(current_id) = current_id_str.parse::<i64>() {
1125 sqlx::query_as::<_, Task>(
1126 r#"
1127 SELECT id, parent_id, name, spec, status, complexity, priority,
1128 first_todo_at, first_doing_at, first_done_at, active_form
1129 FROM tasks
1130 WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1131 AND NOT EXISTS (
1132 SELECT 1 FROM dependencies d
1133 JOIN tasks bt ON d.blocking_task_id = bt.id
1134 WHERE d.blocked_task_id = tasks.id
1135 AND bt.status != 'done'
1136 )
1137 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1138 LIMIT 1
1139 "#,
1140 )
1141 .bind(current_id)
1142 .fetch_optional(self.pool)
1143 .await?
1144 } else {
1145 None
1146 }
1147 } else {
1148 sqlx::query_as::<_, Task>(
1149 r#"
1150 SELECT id, parent_id, name, spec, status, complexity, priority,
1151 first_todo_at, first_doing_at, first_done_at, active_form
1152 FROM tasks
1153 WHERE parent_id IS NULL AND status = 'doing'
1154 AND NOT EXISTS (
1155 SELECT 1 FROM dependencies d
1156 JOIN tasks bt ON d.blocking_task_id = bt.id
1157 WHERE d.blocked_task_id = tasks.id
1158 AND bt.status != 'done'
1159 )
1160 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1161 LIMIT 1
1162 "#,
1163 )
1164 .fetch_optional(self.pool)
1165 .await?
1166 };
1167
1168 if let Some(task) = doing_top_level {
1169 return Ok(PickNextResponse::top_level_task(task));
1170 }
1171
1172 let todo_top_level = sqlx::query_as::<_, Task>(
1175 r#"
1176 SELECT id, parent_id, name, spec, status, complexity, priority,
1177 first_todo_at, first_doing_at, first_done_at, active_form
1178 FROM tasks
1179 WHERE parent_id IS NULL AND status = 'todo'
1180 AND NOT EXISTS (
1181 SELECT 1 FROM dependencies d
1182 JOIN tasks bt ON d.blocking_task_id = bt.id
1183 WHERE d.blocked_task_id = tasks.id
1184 AND bt.status != 'done'
1185 )
1186 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1187 LIMIT 1
1188 "#,
1189 )
1190 .fetch_optional(self.pool)
1191 .await?;
1192
1193 if let Some(task) = todo_top_level {
1194 return Ok(PickNextResponse::top_level_task(task));
1195 }
1196
1197 let total_tasks: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_TOTAL)
1200 .fetch_one(self.pool)
1201 .await?;
1202
1203 if total_tasks == 0 {
1204 return Ok(PickNextResponse::no_tasks_in_project());
1205 }
1206
1207 let todo_or_doing_count: i64 =
1209 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1210 .fetch_one(self.pool)
1211 .await?;
1212
1213 if todo_or_doing_count == 0 {
1214 return Ok(PickNextResponse::all_tasks_completed());
1215 }
1216
1217 Ok(PickNextResponse::no_available_todos())
1219 }
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224 use super::*;
1225 use crate::events::EventManager;
1226 use crate::test_utils::test_helpers::TestContext;
1227 use crate::workspace::WorkspaceManager;
1228
1229 #[tokio::test]
1230 async fn test_add_task() {
1231 let ctx = TestContext::new().await;
1232 let manager = TaskManager::new(ctx.pool());
1233
1234 let task = manager.add_task("Test task", None, None).await.unwrap();
1235
1236 assert_eq!(task.name, "Test task");
1237 assert_eq!(task.status, "todo");
1238 assert!(task.first_todo_at.is_some());
1239 assert!(task.first_doing_at.is_none());
1240 assert!(task.first_done_at.is_none());
1241 }
1242
1243 #[tokio::test]
1244 async fn test_add_task_with_spec() {
1245 let ctx = TestContext::new().await;
1246 let manager = TaskManager::new(ctx.pool());
1247
1248 let spec = "This is a task specification";
1249 let task = manager
1250 .add_task("Test task", Some(spec), None)
1251 .await
1252 .unwrap();
1253
1254 assert_eq!(task.name, "Test task");
1255 assert_eq!(task.spec.as_deref(), Some(spec));
1256 }
1257
1258 #[tokio::test]
1259 async fn test_add_task_with_parent() {
1260 let ctx = TestContext::new().await;
1261 let manager = TaskManager::new(ctx.pool());
1262
1263 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1264 let child = manager
1265 .add_task("Child task", None, Some(parent.id))
1266 .await
1267 .unwrap();
1268
1269 assert_eq!(child.parent_id, Some(parent.id));
1270 }
1271
1272 #[tokio::test]
1273 async fn test_get_task() {
1274 let ctx = TestContext::new().await;
1275 let manager = TaskManager::new(ctx.pool());
1276
1277 let created = manager.add_task("Test task", None, None).await.unwrap();
1278 let retrieved = manager.get_task(created.id).await.unwrap();
1279
1280 assert_eq!(created.id, retrieved.id);
1281 assert_eq!(created.name, retrieved.name);
1282 }
1283
1284 #[tokio::test]
1285 async fn test_get_task_not_found() {
1286 let ctx = TestContext::new().await;
1287 let manager = TaskManager::new(ctx.pool());
1288
1289 let result = manager.get_task(999).await;
1290 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1291 }
1292
1293 #[tokio::test]
1294 async fn test_update_task_name() {
1295 let ctx = TestContext::new().await;
1296 let manager = TaskManager::new(ctx.pool());
1297
1298 let task = manager.add_task("Original name", None, None).await.unwrap();
1299 let updated = manager
1300 .update_task(task.id, Some("New name"), None, None, None, None, None)
1301 .await
1302 .unwrap();
1303
1304 assert_eq!(updated.name, "New name");
1305 }
1306
1307 #[tokio::test]
1308 async fn test_update_task_status() {
1309 let ctx = TestContext::new().await;
1310 let manager = TaskManager::new(ctx.pool());
1311
1312 let task = manager.add_task("Test task", None, None).await.unwrap();
1313 let updated = manager
1314 .update_task(task.id, None, None, None, Some("doing"), None, None)
1315 .await
1316 .unwrap();
1317
1318 assert_eq!(updated.status, "doing");
1319 assert!(updated.first_doing_at.is_some());
1320 }
1321
1322 #[tokio::test]
1323 async fn test_delete_task() {
1324 let ctx = TestContext::new().await;
1325 let manager = TaskManager::new(ctx.pool());
1326
1327 let task = manager.add_task("Test task", None, None).await.unwrap();
1328 manager.delete_task(task.id).await.unwrap();
1329
1330 let result = manager.get_task(task.id).await;
1331 assert!(result.is_err());
1332 }
1333
1334 #[tokio::test]
1335 async fn test_find_tasks_by_status() {
1336 let ctx = TestContext::new().await;
1337 let manager = TaskManager::new(ctx.pool());
1338
1339 manager.add_task("Todo task", None, None).await.unwrap();
1340 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1341 manager
1342 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1343 .await
1344 .unwrap();
1345
1346 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1347 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1348
1349 assert_eq!(todo_tasks.len(), 1);
1350 assert_eq!(doing_tasks.len(), 1);
1351 assert_eq!(doing_tasks[0].status, "doing");
1352 }
1353
1354 #[tokio::test]
1355 async fn test_find_tasks_by_parent() {
1356 let ctx = TestContext::new().await;
1357 let manager = TaskManager::new(ctx.pool());
1358
1359 let parent = manager.add_task("Parent", None, None).await.unwrap();
1360 manager
1361 .add_task("Child 1", None, Some(parent.id))
1362 .await
1363 .unwrap();
1364 manager
1365 .add_task("Child 2", None, Some(parent.id))
1366 .await
1367 .unwrap();
1368
1369 let children = manager
1370 .find_tasks(None, Some(Some(parent.id)))
1371 .await
1372 .unwrap();
1373
1374 assert_eq!(children.len(), 2);
1375 }
1376
1377 #[tokio::test]
1378 async fn test_start_task() {
1379 let ctx = TestContext::new().await;
1380 let manager = TaskManager::new(ctx.pool());
1381
1382 let task = manager.add_task("Test task", None, None).await.unwrap();
1383 let started = manager.start_task(task.id, false).await.unwrap();
1384
1385 assert_eq!(started.task.status, "doing");
1386 assert!(started.task.first_doing_at.is_some());
1387
1388 let current: Option<String> =
1390 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1391 .fetch_optional(ctx.pool())
1392 .await
1393 .unwrap();
1394
1395 assert_eq!(current, Some(task.id.to_string()));
1396 }
1397
1398 #[tokio::test]
1399 async fn test_start_task_with_events() {
1400 let ctx = TestContext::new().await;
1401 let manager = TaskManager::new(ctx.pool());
1402
1403 let task = manager.add_task("Test task", None, None).await.unwrap();
1404
1405 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1407 .bind(task.id)
1408 .bind("test")
1409 .bind("test event")
1410 .execute(ctx.pool())
1411 .await
1412 .unwrap();
1413
1414 let started = manager.start_task(task.id, true).await.unwrap();
1415
1416 assert!(started.events_summary.is_some());
1417 let summary = started.events_summary.unwrap();
1418 assert_eq!(summary.total_count, 1);
1419 }
1420
1421 #[tokio::test]
1422 async fn test_done_task() {
1423 let ctx = TestContext::new().await;
1424 let manager = TaskManager::new(ctx.pool());
1425
1426 let task = manager.add_task("Test task", None, None).await.unwrap();
1427 manager.start_task(task.id, false).await.unwrap();
1428 let response = manager.done_task().await.unwrap();
1429
1430 assert_eq!(response.completed_task.status, "done");
1431 assert!(response.completed_task.first_done_at.is_some());
1432 assert_eq!(response.workspace_status.current_task_id, None);
1433
1434 match response.next_step_suggestion {
1436 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1437 _ => panic!("Expected WorkspaceIsClear suggestion"),
1438 }
1439
1440 let current: Option<String> =
1442 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1443 .fetch_optional(ctx.pool())
1444 .await
1445 .unwrap();
1446
1447 assert!(current.is_none());
1448 }
1449
1450 #[tokio::test]
1451 async fn test_done_task_with_uncompleted_children() {
1452 let ctx = TestContext::new().await;
1453 let manager = TaskManager::new(ctx.pool());
1454
1455 let parent = manager.add_task("Parent", None, None).await.unwrap();
1456 manager
1457 .add_task("Child", None, Some(parent.id))
1458 .await
1459 .unwrap();
1460
1461 manager.start_task(parent.id, false).await.unwrap();
1463
1464 let result = manager.done_task().await;
1465 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1466 }
1467
1468 #[tokio::test]
1469 async fn test_done_task_with_completed_children() {
1470 let ctx = TestContext::new().await;
1471 let manager = TaskManager::new(ctx.pool());
1472
1473 let parent = manager.add_task("Parent", None, None).await.unwrap();
1474 let child = manager
1475 .add_task("Child", None, Some(parent.id))
1476 .await
1477 .unwrap();
1478
1479 manager.start_task(child.id, false).await.unwrap();
1481 let child_response = manager.done_task().await.unwrap();
1482
1483 match child_response.next_step_suggestion {
1485 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1486 assert_eq!(parent_task_id, parent.id);
1487 },
1488 _ => panic!("Expected ParentIsReady suggestion"),
1489 }
1490
1491 manager.start_task(parent.id, false).await.unwrap();
1493 let parent_response = manager.done_task().await.unwrap();
1494 assert_eq!(parent_response.completed_task.status, "done");
1495
1496 match parent_response.next_step_suggestion {
1498 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1499 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1500 }
1501 }
1502
1503 #[tokio::test]
1504 async fn test_circular_dependency() {
1505 let ctx = TestContext::new().await;
1506 let manager = TaskManager::new(ctx.pool());
1507
1508 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1509 let task2 = manager
1510 .add_task("Task 2", None, Some(task1.id))
1511 .await
1512 .unwrap();
1513
1514 let result = manager
1516 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1517 .await;
1518
1519 assert!(matches!(
1520 result,
1521 Err(IntentError::CircularDependency { .. })
1522 ));
1523 }
1524
1525 #[tokio::test]
1526 async fn test_invalid_parent_id() {
1527 let ctx = TestContext::new().await;
1528 let manager = TaskManager::new(ctx.pool());
1529
1530 let result = manager.add_task("Test", None, Some(999)).await;
1531 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1532 }
1533
1534 #[tokio::test]
1535 async fn test_update_task_complexity_and_priority() {
1536 let ctx = TestContext::new().await;
1537 let manager = TaskManager::new(ctx.pool());
1538
1539 let task = manager.add_task("Test task", None, None).await.unwrap();
1540 let updated = manager
1541 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1542 .await
1543 .unwrap();
1544
1545 assert_eq!(updated.complexity, Some(8));
1546 assert_eq!(updated.priority, Some(10));
1547 }
1548
1549 #[tokio::test]
1550 async fn test_spawn_subtask() {
1551 let ctx = TestContext::new().await;
1552 let manager = TaskManager::new(ctx.pool());
1553
1554 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1556 manager.start_task(parent.id, false).await.unwrap();
1557
1558 let response = manager
1560 .spawn_subtask("Child task", Some("Details"))
1561 .await
1562 .unwrap();
1563
1564 assert_eq!(response.subtask.parent_id, parent.id);
1565 assert_eq!(response.subtask.name, "Child task");
1566 assert_eq!(response.subtask.status, "doing");
1567 assert_eq!(response.parent_task.id, parent.id);
1568 assert_eq!(response.parent_task.name, "Parent task");
1569
1570 let current: Option<String> =
1572 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1573 .fetch_optional(ctx.pool())
1574 .await
1575 .unwrap();
1576
1577 assert_eq!(current, Some(response.subtask.id.to_string()));
1578
1579 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1581 assert_eq!(retrieved.status, "doing");
1582 }
1583
1584 #[tokio::test]
1585 async fn test_spawn_subtask_no_current_task() {
1586 let ctx = TestContext::new().await;
1587 let manager = TaskManager::new(ctx.pool());
1588
1589 let result = manager.spawn_subtask("Child", None).await;
1591 assert!(result.is_err());
1592 }
1593
1594 #[tokio::test]
1595 async fn test_pick_next_tasks_basic() {
1596 let ctx = TestContext::new().await;
1597 let manager = TaskManager::new(ctx.pool());
1598
1599 for i in 1..=10 {
1601 manager
1602 .add_task(&format!("Task {}", i), None, None)
1603 .await
1604 .unwrap();
1605 }
1606
1607 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1609
1610 assert_eq!(picked.len(), 5);
1611 for task in &picked {
1612 assert_eq!(task.status, "doing");
1613 assert!(task.first_doing_at.is_some());
1614 }
1615
1616 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1618 .fetch_one(ctx.pool())
1619 .await
1620 .unwrap();
1621
1622 assert_eq!(doing_count, 5);
1623 }
1624
1625 #[tokio::test]
1626 async fn test_pick_next_tasks_with_existing_doing() {
1627 let ctx = TestContext::new().await;
1628 let manager = TaskManager::new(ctx.pool());
1629
1630 for i in 1..=10 {
1632 manager
1633 .add_task(&format!("Task {}", i), None, None)
1634 .await
1635 .unwrap();
1636 }
1637
1638 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1640 manager.start_task(tasks[0].id, false).await.unwrap();
1641 manager.start_task(tasks[1].id, false).await.unwrap();
1642
1643 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1645
1646 assert_eq!(picked.len(), 3);
1648
1649 let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1651 .fetch_one(ctx.pool())
1652 .await
1653 .unwrap();
1654
1655 assert_eq!(doing_count, 5);
1656 }
1657
1658 #[tokio::test]
1659 async fn test_pick_next_tasks_at_capacity() {
1660 let ctx = TestContext::new().await;
1661 let manager = TaskManager::new(ctx.pool());
1662
1663 for i in 1..=10 {
1665 manager
1666 .add_task(&format!("Task {}", i), None, None)
1667 .await
1668 .unwrap();
1669 }
1670
1671 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1673 assert_eq!(first_batch.len(), 5);
1674
1675 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1677 assert_eq!(second_batch.len(), 0);
1678 }
1679
1680 #[tokio::test]
1681 async fn test_pick_next_tasks_priority_ordering() {
1682 let ctx = TestContext::new().await;
1683 let manager = TaskManager::new(ctx.pool());
1684
1685 let low = manager.add_task("Low priority", None, None).await.unwrap();
1687 manager
1688 .update_task(low.id, None, None, None, None, None, Some(1))
1689 .await
1690 .unwrap();
1691
1692 let high = manager.add_task("High priority", None, None).await.unwrap();
1693 manager
1694 .update_task(high.id, None, None, None, None, None, Some(10))
1695 .await
1696 .unwrap();
1697
1698 let medium = manager
1699 .add_task("Medium priority", None, None)
1700 .await
1701 .unwrap();
1702 manager
1703 .update_task(medium.id, None, None, None, None, None, Some(5))
1704 .await
1705 .unwrap();
1706
1707 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1709
1710 assert_eq!(picked.len(), 3);
1712 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
1716
1717 #[tokio::test]
1718 async fn test_pick_next_tasks_complexity_ordering() {
1719 let ctx = TestContext::new().await;
1720 let manager = TaskManager::new(ctx.pool());
1721
1722 let complex = manager.add_task("Complex", None, None).await.unwrap();
1724 manager
1725 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1726 .await
1727 .unwrap();
1728
1729 let simple = manager.add_task("Simple", None, None).await.unwrap();
1730 manager
1731 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1732 .await
1733 .unwrap();
1734
1735 let medium = manager.add_task("Medium", None, None).await.unwrap();
1736 manager
1737 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1738 .await
1739 .unwrap();
1740
1741 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1743
1744 assert_eq!(picked.len(), 3);
1746 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1750
1751 #[tokio::test]
1752 async fn test_done_task_sibling_tasks_remain() {
1753 let ctx = TestContext::new().await;
1754 let manager = TaskManager::new(ctx.pool());
1755
1756 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1758 let child1 = manager
1759 .add_task("Child 1", None, Some(parent.id))
1760 .await
1761 .unwrap();
1762 let child2 = manager
1763 .add_task("Child 2", None, Some(parent.id))
1764 .await
1765 .unwrap();
1766 let _child3 = manager
1767 .add_task("Child 3", None, Some(parent.id))
1768 .await
1769 .unwrap();
1770
1771 manager.start_task(child1.id, false).await.unwrap();
1773 let response = manager.done_task().await.unwrap();
1774
1775 match response.next_step_suggestion {
1777 NextStepSuggestion::SiblingTasksRemain {
1778 parent_task_id,
1779 remaining_siblings_count,
1780 ..
1781 } => {
1782 assert_eq!(parent_task_id, parent.id);
1783 assert_eq!(remaining_siblings_count, 2); },
1785 _ => panic!("Expected SiblingTasksRemain suggestion"),
1786 }
1787
1788 manager.start_task(child2.id, false).await.unwrap();
1790 let response2 = manager.done_task().await.unwrap();
1791
1792 match response2.next_step_suggestion {
1794 NextStepSuggestion::SiblingTasksRemain {
1795 remaining_siblings_count,
1796 ..
1797 } => {
1798 assert_eq!(remaining_siblings_count, 1); },
1800 _ => panic!("Expected SiblingTasksRemain suggestion"),
1801 }
1802 }
1803
1804 #[tokio::test]
1805 async fn test_done_task_top_level_with_children() {
1806 let ctx = TestContext::new().await;
1807 let manager = TaskManager::new(ctx.pool());
1808
1809 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1811 let child = manager
1812 .add_task("Sub Task", None, Some(parent.id))
1813 .await
1814 .unwrap();
1815
1816 manager.start_task(child.id, false).await.unwrap();
1818 manager.done_task().await.unwrap();
1819
1820 manager.start_task(parent.id, false).await.unwrap();
1822 let response = manager.done_task().await.unwrap();
1823
1824 match response.next_step_suggestion {
1826 NextStepSuggestion::TopLevelTaskCompleted {
1827 completed_task_id,
1828 completed_task_name,
1829 ..
1830 } => {
1831 assert_eq!(completed_task_id, parent.id);
1832 assert_eq!(completed_task_name, "Epic Task");
1833 },
1834 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1835 }
1836 }
1837
1838 #[tokio::test]
1839 async fn test_done_task_no_parent_context() {
1840 let ctx = TestContext::new().await;
1841 let manager = TaskManager::new(ctx.pool());
1842
1843 let task1 = manager
1845 .add_task("Standalone Task 1", None, None)
1846 .await
1847 .unwrap();
1848 let _task2 = manager
1849 .add_task("Standalone Task 2", None, None)
1850 .await
1851 .unwrap();
1852
1853 manager.start_task(task1.id, false).await.unwrap();
1855 let response = manager.done_task().await.unwrap();
1856
1857 match response.next_step_suggestion {
1859 NextStepSuggestion::NoParentContext {
1860 completed_task_id,
1861 completed_task_name,
1862 ..
1863 } => {
1864 assert_eq!(completed_task_id, task1.id);
1865 assert_eq!(completed_task_name, "Standalone Task 1");
1866 },
1867 _ => panic!("Expected NoParentContext suggestion"),
1868 }
1869 }
1870
1871 #[tokio::test]
1872 async fn test_search_tasks_by_name() {
1873 let ctx = TestContext::new().await;
1874 let manager = TaskManager::new(ctx.pool());
1875
1876 manager
1878 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1879 .await
1880 .unwrap();
1881 manager
1882 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1883 .await
1884 .unwrap();
1885 manager
1886 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1887 .await
1888 .unwrap();
1889
1890 let results = manager.search_tasks("authentication").await.unwrap();
1892
1893 assert_eq!(results.len(), 2);
1894 assert!(results[0]
1895 .task
1896 .name
1897 .to_lowercase()
1898 .contains("authentication"));
1899 assert!(results[1]
1900 .task
1901 .name
1902 .to_lowercase()
1903 .contains("authentication"));
1904
1905 assert!(!results[0].match_snippet.is_empty());
1907 }
1908
1909 #[tokio::test]
1910 async fn test_search_tasks_by_spec() {
1911 let ctx = TestContext::new().await;
1912 let manager = TaskManager::new(ctx.pool());
1913
1914 manager
1916 .add_task("Task 1", Some("Implement JWT authentication"), None)
1917 .await
1918 .unwrap();
1919 manager
1920 .add_task("Task 2", Some("Add user registration"), None)
1921 .await
1922 .unwrap();
1923 manager
1924 .add_task("Task 3", Some("JWT token refresh"), None)
1925 .await
1926 .unwrap();
1927
1928 let results = manager.search_tasks("JWT").await.unwrap();
1930
1931 assert_eq!(results.len(), 2);
1932 for result in &results {
1933 assert!(result
1934 .task
1935 .spec
1936 .as_ref()
1937 .unwrap()
1938 .to_uppercase()
1939 .contains("JWT"));
1940 }
1941 }
1942
1943 #[tokio::test]
1944 async fn test_search_tasks_with_advanced_query() {
1945 let ctx = TestContext::new().await;
1946 let manager = TaskManager::new(ctx.pool());
1947
1948 manager
1950 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1951 .await
1952 .unwrap();
1953 manager
1954 .add_task("Feature", Some("Add authentication feature"), None)
1955 .await
1956 .unwrap();
1957 manager
1958 .add_task("Bug report", Some("Report critical database bug"), None)
1959 .await
1960 .unwrap();
1961
1962 let results = manager
1964 .search_tasks("authentication AND bug")
1965 .await
1966 .unwrap();
1967
1968 assert_eq!(results.len(), 1);
1969 assert!(results[0]
1970 .task
1971 .spec
1972 .as_ref()
1973 .unwrap()
1974 .contains("authentication"));
1975 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1976 }
1977
1978 #[tokio::test]
1979 async fn test_search_tasks_no_results() {
1980 let ctx = TestContext::new().await;
1981 let manager = TaskManager::new(ctx.pool());
1982
1983 manager
1985 .add_task("Task 1", Some("Some description"), None)
1986 .await
1987 .unwrap();
1988
1989 let results = manager.search_tasks("nonexistent").await.unwrap();
1991
1992 assert_eq!(results.len(), 0);
1993 }
1994
1995 #[tokio::test]
1996 async fn test_search_tasks_snippet_highlighting() {
1997 let ctx = TestContext::new().await;
1998 let manager = TaskManager::new(ctx.pool());
1999
2000 manager
2002 .add_task(
2003 "Test task",
2004 Some("This is a description with the keyword authentication in the middle"),
2005 None,
2006 )
2007 .await
2008 .unwrap();
2009
2010 let results = manager.search_tasks("authentication").await.unwrap();
2012
2013 assert_eq!(results.len(), 1);
2014 assert!(results[0].match_snippet.contains("**authentication**"));
2016 }
2017
2018 #[tokio::test]
2019 async fn test_pick_next_focused_subtask() {
2020 let ctx = TestContext::new().await;
2021 let manager = TaskManager::new(ctx.pool());
2022
2023 let parent = manager.add_task("Parent task", None, None).await.unwrap();
2025 manager.start_task(parent.id, false).await.unwrap();
2026
2027 let subtask1 = manager
2029 .add_task("Subtask 1", None, Some(parent.id))
2030 .await
2031 .unwrap();
2032 let subtask2 = manager
2033 .add_task("Subtask 2", None, Some(parent.id))
2034 .await
2035 .unwrap();
2036
2037 manager
2039 .update_task(subtask1.id, None, None, None, None, None, Some(2))
2040 .await
2041 .unwrap();
2042 manager
2043 .update_task(subtask2.id, None, None, None, None, None, Some(1))
2044 .await
2045 .unwrap();
2046
2047 let response = manager.pick_next().await.unwrap();
2049
2050 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2051 assert!(response.task.is_some());
2052 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2053 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2054 }
2055
2056 #[tokio::test]
2057 async fn test_pick_next_top_level_task() {
2058 let ctx = TestContext::new().await;
2059 let manager = TaskManager::new(ctx.pool());
2060
2061 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
2063 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
2064
2065 manager
2067 .update_task(task1.id, None, None, None, None, None, Some(5))
2068 .await
2069 .unwrap();
2070 manager
2071 .update_task(task2.id, None, None, None, None, None, Some(3))
2072 .await
2073 .unwrap();
2074
2075 let response = manager.pick_next().await.unwrap();
2077
2078 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2079 assert!(response.task.is_some());
2080 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2081 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2082 }
2083
2084 #[tokio::test]
2085 async fn test_pick_next_no_tasks() {
2086 let ctx = TestContext::new().await;
2087 let manager = TaskManager::new(ctx.pool());
2088
2089 let response = manager.pick_next().await.unwrap();
2091
2092 assert_eq!(response.suggestion_type, "NONE");
2093 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2094 assert!(response.message.is_some());
2095 }
2096
2097 #[tokio::test]
2098 async fn test_pick_next_all_completed() {
2099 let ctx = TestContext::new().await;
2100 let manager = TaskManager::new(ctx.pool());
2101
2102 let task = manager.add_task("Task 1", None, None).await.unwrap();
2104 manager.start_task(task.id, false).await.unwrap();
2105 manager.done_task().await.unwrap();
2106
2107 let response = manager.pick_next().await.unwrap();
2109
2110 assert_eq!(response.suggestion_type, "NONE");
2111 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2112 assert!(response.message.is_some());
2113 }
2114
2115 #[tokio::test]
2116 async fn test_pick_next_no_available_todos() {
2117 let ctx = TestContext::new().await;
2118 let manager = TaskManager::new(ctx.pool());
2119
2120 let parent = manager.add_task("Parent task", None, None).await.unwrap();
2122 manager.start_task(parent.id, false).await.unwrap();
2123
2124 let subtask = manager
2126 .add_task("Subtask", None, Some(parent.id))
2127 .await
2128 .unwrap();
2129 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2131 .bind(subtask.id)
2132 .execute(ctx.pool())
2133 .await
2134 .unwrap();
2135
2136 sqlx::query(
2138 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
2139 )
2140 .bind(subtask.id.to_string())
2141 .execute(ctx.pool())
2142 .await
2143 .unwrap();
2144
2145 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2147 .bind(parent.id)
2148 .execute(ctx.pool())
2149 .await
2150 .unwrap();
2151
2152 let response = manager.pick_next().await.unwrap();
2155
2156 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2157 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2158 assert_eq!(response.task.as_ref().unwrap().status, "doing");
2159 }
2160
2161 #[tokio::test]
2162 async fn test_pick_next_priority_ordering() {
2163 let ctx = TestContext::new().await;
2164 let manager = TaskManager::new(ctx.pool());
2165
2166 let parent = manager.add_task("Parent", None, None).await.unwrap();
2168 manager.start_task(parent.id, false).await.unwrap();
2169
2170 let sub1 = manager
2172 .add_task("Priority 10", None, Some(parent.id))
2173 .await
2174 .unwrap();
2175 manager
2176 .update_task(sub1.id, None, None, None, None, None, Some(10))
2177 .await
2178 .unwrap();
2179
2180 let sub2 = manager
2181 .add_task("Priority 1", None, Some(parent.id))
2182 .await
2183 .unwrap();
2184 manager
2185 .update_task(sub2.id, None, None, None, None, None, Some(1))
2186 .await
2187 .unwrap();
2188
2189 let sub3 = manager
2190 .add_task("Priority 5", None, Some(parent.id))
2191 .await
2192 .unwrap();
2193 manager
2194 .update_task(sub3.id, None, None, None, None, None, Some(5))
2195 .await
2196 .unwrap();
2197
2198 let response = manager.pick_next().await.unwrap();
2200
2201 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2202 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2203 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2204 }
2205
2206 #[tokio::test]
2207 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2208 let ctx = TestContext::new().await;
2209 let manager = TaskManager::new(ctx.pool());
2210
2211 let parent = manager.add_task("Parent", None, None).await.unwrap();
2213 manager.start_task(parent.id, false).await.unwrap();
2214
2215 let top_level = manager
2217 .add_task("Top level task", None, None)
2218 .await
2219 .unwrap();
2220
2221 let response = manager.pick_next().await.unwrap();
2223
2224 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2225 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2226 }
2227
2228 #[tokio::test]
2231 async fn test_get_task_with_events() {
2232 let ctx = TestContext::new().await;
2233 let task_mgr = TaskManager::new(ctx.pool());
2234 let event_mgr = EventManager::new(ctx.pool());
2235
2236 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2237
2238 event_mgr
2240 .add_event(task.id, "progress", "Event 1")
2241 .await
2242 .unwrap();
2243 event_mgr
2244 .add_event(task.id, "decision", "Event 2")
2245 .await
2246 .unwrap();
2247
2248 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2249
2250 assert_eq!(result.task.id, task.id);
2251 assert!(result.events_summary.is_some());
2252
2253 let summary = result.events_summary.unwrap();
2254 assert_eq!(summary.total_count, 2);
2255 assert_eq!(summary.recent_events.len(), 2);
2256 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2258 }
2259
2260 #[tokio::test]
2261 async fn test_get_task_with_events_nonexistent() {
2262 let ctx = TestContext::new().await;
2263 let task_mgr = TaskManager::new(ctx.pool());
2264
2265 let result = task_mgr.get_task_with_events(999).await;
2266 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2267 }
2268
2269 #[tokio::test]
2270 async fn test_get_task_with_many_events() {
2271 let ctx = TestContext::new().await;
2272 let task_mgr = TaskManager::new(ctx.pool());
2273 let event_mgr = EventManager::new(ctx.pool());
2274
2275 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2276
2277 for i in 0..20 {
2279 event_mgr
2280 .add_event(task.id, "test", &format!("Event {}", i))
2281 .await
2282 .unwrap();
2283 }
2284
2285 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2286 let summary = result.events_summary.unwrap();
2287
2288 assert_eq!(summary.total_count, 20);
2289 assert_eq!(summary.recent_events.len(), 10); }
2291
2292 #[tokio::test]
2293 async fn test_get_task_with_no_events() {
2294 let ctx = TestContext::new().await;
2295 let task_mgr = TaskManager::new(ctx.pool());
2296
2297 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2298
2299 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2300 let summary = result.events_summary.unwrap();
2301
2302 assert_eq!(summary.total_count, 0);
2303 assert_eq!(summary.recent_events.len(), 0);
2304 }
2305
2306 #[tokio::test]
2307 async fn test_pick_next_tasks_zero_capacity() {
2308 let ctx = TestContext::new().await;
2309 let task_mgr = TaskManager::new(ctx.pool());
2310
2311 task_mgr.add_task("Task 1", None, None).await.unwrap();
2312
2313 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2315 assert_eq!(results.len(), 0);
2316 }
2317
2318 #[tokio::test]
2319 async fn test_pick_next_tasks_capacity_exceeds_available() {
2320 let ctx = TestContext::new().await;
2321 let task_mgr = TaskManager::new(ctx.pool());
2322
2323 task_mgr.add_task("Task 1", None, None).await.unwrap();
2324 task_mgr.add_task("Task 2", None, None).await.unwrap();
2325
2326 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2328 assert_eq!(results.len(), 2); }
2330
2331 #[tokio::test]
2334 async fn test_get_task_context_root_task_no_relations() {
2335 let ctx = TestContext::new().await;
2336 let task_mgr = TaskManager::new(ctx.pool());
2337
2338 let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2340
2341 let context = task_mgr.get_task_context(task.id).await.unwrap();
2342
2343 assert_eq!(context.task.id, task.id);
2345 assert_eq!(context.task.name, "Root task");
2346
2347 assert_eq!(context.ancestors.len(), 0);
2349
2350 assert_eq!(context.siblings.len(), 0);
2352
2353 assert_eq!(context.children.len(), 0);
2355 }
2356
2357 #[tokio::test]
2358 async fn test_get_task_context_with_siblings() {
2359 let ctx = TestContext::new().await;
2360 let task_mgr = TaskManager::new(ctx.pool());
2361
2362 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2364 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2365 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2366
2367 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2368
2369 assert_eq!(context.task.id, task2.id);
2371
2372 assert_eq!(context.ancestors.len(), 0);
2374
2375 assert_eq!(context.siblings.len(), 2);
2377 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2378 assert!(sibling_ids.contains(&task1.id));
2379 assert!(sibling_ids.contains(&task3.id));
2380 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2384 }
2385
2386 #[tokio::test]
2387 async fn test_get_task_context_with_parent() {
2388 let ctx = TestContext::new().await;
2389 let task_mgr = TaskManager::new(ctx.pool());
2390
2391 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2393 let child = task_mgr
2394 .add_task("Child task", None, Some(parent.id))
2395 .await
2396 .unwrap();
2397
2398 let context = task_mgr.get_task_context(child.id).await.unwrap();
2399
2400 assert_eq!(context.task.id, child.id);
2402 assert_eq!(context.task.parent_id, Some(parent.id));
2403
2404 assert_eq!(context.ancestors.len(), 1);
2406 assert_eq!(context.ancestors[0].id, parent.id);
2407 assert_eq!(context.ancestors[0].name, "Parent task");
2408
2409 assert_eq!(context.siblings.len(), 0);
2411
2412 assert_eq!(context.children.len(), 0);
2414 }
2415
2416 #[tokio::test]
2417 async fn test_get_task_context_with_children() {
2418 let ctx = TestContext::new().await;
2419 let task_mgr = TaskManager::new(ctx.pool());
2420
2421 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2423 let child1 = task_mgr
2424 .add_task("Child 1", None, Some(parent.id))
2425 .await
2426 .unwrap();
2427 let child2 = task_mgr
2428 .add_task("Child 2", None, Some(parent.id))
2429 .await
2430 .unwrap();
2431 let child3 = task_mgr
2432 .add_task("Child 3", None, Some(parent.id))
2433 .await
2434 .unwrap();
2435
2436 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2437
2438 assert_eq!(context.task.id, parent.id);
2440
2441 assert_eq!(context.ancestors.len(), 0);
2443
2444 assert_eq!(context.siblings.len(), 0);
2446
2447 assert_eq!(context.children.len(), 3);
2449 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2450 assert!(child_ids.contains(&child1.id));
2451 assert!(child_ids.contains(&child2.id));
2452 assert!(child_ids.contains(&child3.id));
2453 }
2454
2455 #[tokio::test]
2456 async fn test_get_task_context_multi_level_hierarchy() {
2457 let ctx = TestContext::new().await;
2458 let task_mgr = TaskManager::new(ctx.pool());
2459
2460 let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2462 let parent = task_mgr
2463 .add_task("Parent", None, Some(grandparent.id))
2464 .await
2465 .unwrap();
2466 let child = task_mgr
2467 .add_task("Child", None, Some(parent.id))
2468 .await
2469 .unwrap();
2470
2471 let context = task_mgr.get_task_context(child.id).await.unwrap();
2472
2473 assert_eq!(context.task.id, child.id);
2475
2476 assert_eq!(context.ancestors.len(), 2);
2478 assert_eq!(context.ancestors[0].id, parent.id);
2479 assert_eq!(context.ancestors[0].name, "Parent");
2480 assert_eq!(context.ancestors[1].id, grandparent.id);
2481 assert_eq!(context.ancestors[1].name, "Grandparent");
2482
2483 assert_eq!(context.siblings.len(), 0);
2485
2486 assert_eq!(context.children.len(), 0);
2488 }
2489
2490 #[tokio::test]
2491 async fn test_get_task_context_complex_family_tree() {
2492 let ctx = TestContext::new().await;
2493 let task_mgr = TaskManager::new(ctx.pool());
2494
2495 let root = task_mgr.add_task("Root", None, None).await.unwrap();
2503 let child1 = task_mgr
2504 .add_task("Child1", None, Some(root.id))
2505 .await
2506 .unwrap();
2507 let child2 = task_mgr
2508 .add_task("Child2", None, Some(root.id))
2509 .await
2510 .unwrap();
2511 let grandchild1 = task_mgr
2512 .add_task("Grandchild1", None, Some(child1.id))
2513 .await
2514 .unwrap();
2515 let grandchild2 = task_mgr
2516 .add_task("Grandchild2", None, Some(child1.id))
2517 .await
2518 .unwrap();
2519
2520 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2522
2523 assert_eq!(context.task.id, grandchild2.id);
2525
2526 assert_eq!(context.ancestors.len(), 2);
2528 assert_eq!(context.ancestors[0].id, child1.id);
2529 assert_eq!(context.ancestors[1].id, root.id);
2530
2531 assert_eq!(context.siblings.len(), 1);
2533 assert_eq!(context.siblings[0].id, grandchild1.id);
2534
2535 assert_eq!(context.children.len(), 0);
2537
2538 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2540 assert_eq!(context_child1.ancestors.len(), 1);
2541 assert_eq!(context_child1.ancestors[0].id, root.id);
2542 assert_eq!(context_child1.siblings.len(), 1);
2543 assert_eq!(context_child1.siblings[0].id, child2.id);
2544 assert_eq!(context_child1.children.len(), 2);
2545 }
2546
2547 #[tokio::test]
2548 async fn test_get_task_context_respects_priority_ordering() {
2549 let ctx = TestContext::new().await;
2550 let task_mgr = TaskManager::new(ctx.pool());
2551
2552 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2554
2555 let child_low = task_mgr
2557 .add_task("Low priority", None, Some(parent.id))
2558 .await
2559 .unwrap();
2560 let _ = task_mgr
2561 .update_task(child_low.id, None, None, None, None, None, Some(10))
2562 .await
2563 .unwrap();
2564
2565 let child_high = task_mgr
2566 .add_task("High priority", None, Some(parent.id))
2567 .await
2568 .unwrap();
2569 let _ = task_mgr
2570 .update_task(child_high.id, None, None, None, None, None, Some(1))
2571 .await
2572 .unwrap();
2573
2574 let child_medium = task_mgr
2575 .add_task("Medium priority", None, Some(parent.id))
2576 .await
2577 .unwrap();
2578 let _ = task_mgr
2579 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2580 .await
2581 .unwrap();
2582
2583 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2584
2585 assert_eq!(context.children.len(), 3);
2587 assert_eq!(context.children[0].priority, Some(1));
2588 assert_eq!(context.children[1].priority, Some(5));
2589 assert_eq!(context.children[2].priority, Some(10));
2590 }
2591
2592 #[tokio::test]
2593 async fn test_get_task_context_nonexistent_task() {
2594 let ctx = TestContext::new().await;
2595 let task_mgr = TaskManager::new(ctx.pool());
2596
2597 let result = task_mgr.get_task_context(99999).await;
2598 assert!(result.is_err());
2599 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2600 }
2601
2602 #[tokio::test]
2603 async fn test_get_task_context_handles_null_priority() {
2604 let ctx = TestContext::new().await;
2605 let task_mgr = TaskManager::new(ctx.pool());
2606
2607 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2609 let _ = task_mgr
2610 .update_task(task1.id, None, None, None, None, None, Some(1))
2611 .await
2612 .unwrap();
2613
2614 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2615 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2618 let _ = task_mgr
2619 .update_task(task3.id, None, None, None, None, None, Some(5))
2620 .await
2621 .unwrap();
2622
2623 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2624
2625 assert_eq!(context.siblings.len(), 2);
2627 assert_eq!(context.siblings[0].id, task1.id);
2629 assert_eq!(context.siblings[0].priority, Some(1));
2630 assert_eq!(context.siblings[1].id, task3.id);
2632 assert_eq!(context.siblings[1].priority, Some(5));
2633 }
2634
2635 #[tokio::test]
2636 async fn test_pick_next_tasks_priority_order() {
2637 let ctx = TestContext::new().await;
2638 let task_mgr = TaskManager::new(ctx.pool());
2639
2640 let critical = task_mgr
2642 .add_task("Critical Task", None, None)
2643 .await
2644 .unwrap();
2645 task_mgr
2646 .update_task(critical.id, None, None, None, None, None, Some(1))
2647 .await
2648 .unwrap();
2649
2650 let low = task_mgr.add_task("Low Task", None, None).await.unwrap();
2651 task_mgr
2652 .update_task(low.id, None, None, None, None, None, Some(4))
2653 .await
2654 .unwrap();
2655
2656 let high = task_mgr.add_task("High Task", None, None).await.unwrap();
2657 task_mgr
2658 .update_task(high.id, None, None, None, None, None, Some(2))
2659 .await
2660 .unwrap();
2661
2662 let medium = task_mgr.add_task("Medium Task", None, None).await.unwrap();
2663 task_mgr
2664 .update_task(medium.id, None, None, None, None, None, Some(3))
2665 .await
2666 .unwrap();
2667
2668 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2670
2671 assert_eq!(tasks.len(), 4);
2672 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); }
2677
2678 #[tokio::test]
2679 async fn test_pick_next_prefers_doing_over_todo() {
2680 let ctx = TestContext::new().await;
2681 let task_mgr = TaskManager::new(ctx.pool());
2682 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2683
2684 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2686 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
2687 workspace_mgr
2688 .set_current_task(parent_started.task.id)
2689 .await
2690 .unwrap();
2691
2692 let doing_subtask = task_mgr
2694 .add_task("Doing Subtask", None, Some(parent.id))
2695 .await
2696 .unwrap();
2697 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
2698 workspace_mgr.set_current_task(parent.id).await.unwrap();
2700
2701 let _todo_subtask = task_mgr
2702 .add_task("Todo Subtask", None, Some(parent.id))
2703 .await
2704 .unwrap();
2705
2706 let result = task_mgr.pick_next().await.unwrap();
2708
2709 if let Some(task) = result.task {
2710 assert_eq!(
2711 task.id, doing_subtask.id,
2712 "Should recommend doing subtask over todo subtask"
2713 );
2714 assert_eq!(task.status, "doing");
2715 } else {
2716 panic!("Expected a task recommendation");
2717 }
2718 }
2719
2720 #[tokio::test]
2721 async fn test_multiple_doing_tasks_allowed() {
2722 let ctx = TestContext::new().await;
2723 let task_mgr = TaskManager::new(ctx.pool());
2724 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2725
2726 let task_a = task_mgr.add_task("Task A", None, None).await.unwrap();
2728 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
2729 assert_eq!(task_a_started.task.status, "doing");
2730
2731 let current = workspace_mgr.get_current_task().await.unwrap();
2733 assert_eq!(current.current_task_id, Some(task_a.id));
2734
2735 let task_b = task_mgr.add_task("Task B", None, None).await.unwrap();
2737 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
2738 assert_eq!(task_b_started.task.status, "doing");
2739
2740 let current = workspace_mgr.get_current_task().await.unwrap();
2742 assert_eq!(current.current_task_id, Some(task_b.id));
2743
2744 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
2746 assert_eq!(
2747 task_a_after.status, "doing",
2748 "Task A should remain doing even though it is not current"
2749 );
2750
2751 let doing_tasks: Vec<Task> = sqlx::query_as(
2753 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
2754 FROM tasks WHERE status = 'doing' ORDER BY id"#
2755 )
2756 .fetch_all(ctx.pool())
2757 .await
2758 .unwrap();
2759
2760 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
2761 assert_eq!(doing_tasks[0].id, task_a.id);
2762 assert_eq!(doing_tasks[1].id, task_b.id);
2763 }
2764}
2765
2766