1use crate::db::models::{
2 CurrentTaskInfo, DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, ParentTaskInfo,
3 PickNextResponse, PreviousTaskInfo, SpawnSubtaskResponse, SubtaskInfo, SwitchTaskResponse,
4 Task, TaskContext, TaskSearchResult, TaskWithEvents, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::{Row, SqlitePool};
9
10pub struct TaskManager<'a> {
11 pool: &'a SqlitePool,
12}
13
14impl<'a> TaskManager<'a> {
15 pub fn new(pool: &'a SqlitePool) -> Self {
16 Self { pool }
17 }
18
19 pub async fn add_task(
21 &self,
22 name: &str,
23 spec: Option<&str>,
24 parent_id: Option<i64>,
25 ) -> Result<Task> {
26 if let Some(pid) = parent_id {
28 self.check_task_exists(pid).await?;
29 }
30
31 let now = Utc::now();
32
33 let result = sqlx::query(
34 r#"
35 INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
36 VALUES (?, ?, ?, 'todo', ?)
37 "#,
38 )
39 .bind(name)
40 .bind(spec)
41 .bind(parent_id)
42 .bind(now)
43 .execute(self.pool)
44 .await?;
45
46 let id = result.last_insert_rowid();
47 self.get_task(id).await
48 }
49
50 pub async fn get_task(&self, id: i64) -> Result<Task> {
52 let task = sqlx::query_as::<_, Task>(
53 r#"
54 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
55 FROM tasks
56 WHERE id = ?
57 "#,
58 )
59 .bind(id)
60 .fetch_optional(self.pool)
61 .await?
62 .ok_or(IntentError::TaskNotFound(id))?;
63
64 Ok(task)
65 }
66
67 pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
69 let task = self.get_task(id).await?;
70 let events_summary = self.get_events_summary(id).await?;
71
72 Ok(TaskWithEvents {
73 task,
74 events_summary: Some(events_summary),
75 })
76 }
77
78 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
86 let task = self.get_task(id).await?;
88
89 let mut ancestors = Vec::new();
91 let mut current_parent_id = task.parent_id;
92
93 while let Some(parent_id) = current_parent_id {
94 let parent = self.get_task(parent_id).await?;
95 current_parent_id = parent.parent_id;
96 ancestors.push(parent);
97 }
98
99 let siblings = if let Some(parent_id) = task.parent_id {
101 sqlx::query_as::<_, Task>(
102 r#"
103 SELECT id, parent_id, name, spec, status, complexity, priority,
104 first_todo_at, first_doing_at, first_done_at
105 FROM tasks
106 WHERE parent_id = ? AND id != ?
107 ORDER BY priority ASC NULLS LAST, id ASC
108 "#,
109 )
110 .bind(parent_id)
111 .bind(id)
112 .fetch_all(self.pool)
113 .await?
114 } else {
115 sqlx::query_as::<_, Task>(
117 r#"
118 SELECT id, parent_id, name, spec, status, complexity, priority,
119 first_todo_at, first_doing_at, first_done_at
120 FROM tasks
121 WHERE parent_id IS NULL AND id != ?
122 ORDER BY priority ASC NULLS LAST, id ASC
123 "#,
124 )
125 .bind(id)
126 .fetch_all(self.pool)
127 .await?
128 };
129
130 let children = sqlx::query_as::<_, Task>(
132 r#"
133 SELECT id, parent_id, name, spec, status, complexity, priority,
134 first_todo_at, first_doing_at, first_done_at
135 FROM tasks
136 WHERE parent_id = ?
137 ORDER BY priority ASC NULLS LAST, id ASC
138 "#,
139 )
140 .bind(id)
141 .fetch_all(self.pool)
142 .await?;
143
144 let blocking_tasks = sqlx::query_as::<_, Task>(
146 r#"
147 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
148 t.first_todo_at, t.first_doing_at, t.first_done_at
149 FROM tasks t
150 JOIN dependencies d ON t.id = d.blocking_task_id
151 WHERE d.blocked_task_id = ?
152 ORDER BY t.priority ASC NULLS LAST, t.id ASC
153 "#,
154 )
155 .bind(id)
156 .fetch_all(self.pool)
157 .await?;
158
159 let blocked_by_tasks = sqlx::query_as::<_, Task>(
161 r#"
162 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
163 t.first_todo_at, t.first_doing_at, t.first_done_at
164 FROM tasks t
165 JOIN dependencies d ON t.id = d.blocked_task_id
166 WHERE d.blocking_task_id = ?
167 ORDER BY t.priority ASC NULLS LAST, t.id ASC
168 "#,
169 )
170 .bind(id)
171 .fetch_all(self.pool)
172 .await?;
173
174 Ok(TaskContext {
175 task,
176 ancestors,
177 siblings,
178 children,
179 dependencies: crate::db::models::TaskDependencies {
180 blocking_tasks,
181 blocked_by_tasks,
182 },
183 })
184 }
185
186 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
188 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
189 .bind(task_id)
190 .fetch_one(self.pool)
191 .await?;
192
193 let recent_events = sqlx::query_as::<_, Event>(
194 r#"
195 SELECT id, task_id, timestamp, log_type, discussion_data
196 FROM events
197 WHERE task_id = ?
198 ORDER BY timestamp DESC
199 LIMIT 10
200 "#,
201 )
202 .bind(task_id)
203 .fetch_all(self.pool)
204 .await?;
205
206 Ok(EventsSummary {
207 total_count,
208 recent_events,
209 })
210 }
211
212 #[allow(clippy::too_many_arguments)]
214 pub async fn update_task(
215 &self,
216 id: i64,
217 name: Option<&str>,
218 spec: Option<&str>,
219 parent_id: Option<Option<i64>>,
220 status: Option<&str>,
221 complexity: Option<i32>,
222 priority: Option<i32>,
223 ) -> Result<Task> {
224 let task = self.get_task(id).await?;
226
227 if let Some(s) = status {
229 if !["todo", "doing", "done"].contains(&s) {
230 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
231 }
232 }
233
234 if let Some(Some(pid)) = parent_id {
236 if pid == id {
237 return Err(IntentError::CircularDependency {
238 blocking_task_id: pid,
239 blocked_task_id: id,
240 });
241 }
242 self.check_task_exists(pid).await?;
243 self.check_circular_dependency(id, pid).await?;
244 }
245
246 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
248 sqlx::QueryBuilder::new("UPDATE tasks SET ");
249 let mut has_updates = false;
250
251 if let Some(n) = name {
252 if has_updates {
253 builder.push(", ");
254 }
255 builder.push("name = ").push_bind(n);
256 has_updates = true;
257 }
258
259 if let Some(s) = spec {
260 if has_updates {
261 builder.push(", ");
262 }
263 builder.push("spec = ").push_bind(s);
264 has_updates = true;
265 }
266
267 if let Some(pid) = parent_id {
268 if has_updates {
269 builder.push(", ");
270 }
271 match pid {
272 Some(p) => {
273 builder.push("parent_id = ").push_bind(p);
274 },
275 None => {
276 builder.push("parent_id = NULL");
277 },
278 }
279 has_updates = true;
280 }
281
282 if let Some(c) = complexity {
283 if has_updates {
284 builder.push(", ");
285 }
286 builder.push("complexity = ").push_bind(c);
287 has_updates = true;
288 }
289
290 if let Some(p) = priority {
291 if has_updates {
292 builder.push(", ");
293 }
294 builder.push("priority = ").push_bind(p);
295 has_updates = true;
296 }
297
298 if let Some(s) = status {
299 if has_updates {
300 builder.push(", ");
301 }
302 builder.push("status = ").push_bind(s);
303 has_updates = true;
304
305 let now = Utc::now();
307 let timestamp = now.to_rfc3339();
308 match s {
309 "todo" if task.first_todo_at.is_none() => {
310 builder.push(", first_todo_at = ").push_bind(timestamp);
311 },
312 "doing" if task.first_doing_at.is_none() => {
313 builder.push(", first_doing_at = ").push_bind(timestamp);
314 },
315 "done" if task.first_done_at.is_none() => {
316 builder.push(", first_done_at = ").push_bind(timestamp);
317 },
318 _ => {},
319 }
320 }
321
322 if !has_updates {
323 return Ok(task);
324 }
325
326 builder.push(" WHERE id = ").push_bind(id);
327
328 builder.build().execute(self.pool).await?;
329
330 self.get_task(id).await
331 }
332
333 pub async fn delete_task(&self, id: i64) -> Result<()> {
335 self.check_task_exists(id).await?;
336
337 sqlx::query("DELETE FROM tasks WHERE id = ?")
338 .bind(id)
339 .execute(self.pool)
340 .await?;
341
342 Ok(())
343 }
344
345 pub async fn find_tasks(
347 &self,
348 status: Option<&str>,
349 parent_id: Option<Option<i64>>,
350 ) -> Result<Vec<Task>> {
351 let mut query = String::from(
352 "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at FROM tasks WHERE 1=1"
353 );
354 let mut conditions = Vec::new();
355
356 if let Some(s) = status {
357 query.push_str(" AND status = ?");
358 conditions.push(s.to_string());
359 }
360
361 if let Some(pid) = parent_id {
362 if let Some(p) = pid {
363 query.push_str(" AND parent_id = ?");
364 conditions.push(p.to_string());
365 } else {
366 query.push_str(" AND parent_id IS NULL");
367 }
368 }
369
370 query.push_str(" ORDER BY id");
371
372 let mut q = sqlx::query_as::<_, Task>(&query);
373 for cond in conditions {
374 q = q.bind(cond);
375 }
376
377 let tasks = q.fetch_all(self.pool).await?;
378 Ok(tasks)
379 }
380
381 pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
384 if query.trim().is_empty() {
386 return Ok(Vec::new());
387 }
388
389 let has_searchable = query
392 .chars()
393 .any(|c| c.is_alphanumeric() || crate::search::is_cjk_char(c));
394 if !has_searchable {
395 return Ok(Vec::new());
396 }
397
398 if crate::search::needs_like_fallback(query) {
401 self.search_tasks_like(query).await
402 } else {
403 self.search_tasks_fts5(query).await
404 }
405 }
406
407 async fn search_tasks_fts5(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
409 let escaped_query = self.escape_fts_query(query);
411
412 let results = sqlx::query(
416 r#"
417 SELECT
418 t.id,
419 t.parent_id,
420 t.name,
421 t.spec,
422 t.status,
423 t.complexity,
424 t.priority,
425 t.first_todo_at,
426 t.first_doing_at,
427 t.first_done_at,
428 COALESCE(
429 snippet(tasks_fts, 1, '**', '**', '...', 15),
430 snippet(tasks_fts, 0, '**', '**', '...', 15)
431 ) as match_snippet
432 FROM tasks_fts
433 INNER JOIN tasks t ON tasks_fts.rowid = t.id
434 WHERE tasks_fts MATCH ?
435 ORDER BY rank
436 "#,
437 )
438 .bind(&escaped_query)
439 .fetch_all(self.pool)
440 .await?;
441
442 let mut search_results = Vec::new();
443 for row in results {
444 let task = Task {
445 id: row.get("id"),
446 parent_id: row.get("parent_id"),
447 name: row.get("name"),
448 spec: row.get("spec"),
449 status: row.get("status"),
450 complexity: row.get("complexity"),
451 priority: row.get("priority"),
452 first_todo_at: row.get("first_todo_at"),
453 first_doing_at: row.get("first_doing_at"),
454 first_done_at: row.get("first_done_at"),
455 };
456 let match_snippet: String = row.get("match_snippet");
457
458 search_results.push(TaskSearchResult {
459 task,
460 match_snippet,
461 });
462 }
463
464 Ok(search_results)
465 }
466
467 async fn search_tasks_like(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
469 let pattern = format!("%{}%", query);
470
471 let results = sqlx::query(
472 r#"
473 SELECT
474 id,
475 parent_id,
476 name,
477 spec,
478 status,
479 complexity,
480 priority,
481 first_todo_at,
482 first_doing_at,
483 first_done_at
484 FROM tasks
485 WHERE name LIKE ? OR spec LIKE ?
486 ORDER BY name
487 "#,
488 )
489 .bind(&pattern)
490 .bind(&pattern)
491 .fetch_all(self.pool)
492 .await?;
493
494 let mut search_results = Vec::new();
495 for row in results {
496 let task = Task {
497 id: row.get("id"),
498 parent_id: row.get("parent_id"),
499 name: row.get("name"),
500 spec: row.get("spec"),
501 status: row.get("status"),
502 complexity: row.get("complexity"),
503 priority: row.get("priority"),
504 first_todo_at: row.get("first_todo_at"),
505 first_doing_at: row.get("first_doing_at"),
506 first_done_at: row.get("first_done_at"),
507 };
508
509 let name: String = row.get("name");
511 let spec: Option<String> = row.get("spec");
512
513 let match_snippet = if name.contains(query) {
514 format!("**{}**", name)
515 } else if let Some(ref s) = spec {
516 if s.contains(query) {
517 format!("**{}**", s)
518 } else {
519 name.clone()
520 }
521 } else {
522 name
523 };
524
525 search_results.push(TaskSearchResult {
526 task,
527 match_snippet,
528 });
529 }
530
531 Ok(search_results)
532 }
533
534 fn escape_fts_query(&self, query: &str) -> String {
536 query.replace('"', "\"\"")
540 }
541
542 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
544 use crate::dependencies::get_incomplete_blocking_tasks;
546 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
547 return Err(IntentError::TaskBlocked {
548 task_id: id,
549 blocking_task_ids: blocking_tasks,
550 });
551 }
552
553 let mut tx = self.pool.begin().await?;
554
555 let now = Utc::now();
556
557 sqlx::query(
559 r#"
560 UPDATE tasks
561 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
562 WHERE id = ?
563 "#,
564 )
565 .bind(now)
566 .bind(id)
567 .execute(&mut *tx)
568 .await?;
569
570 sqlx::query(
572 r#"
573 INSERT OR REPLACE INTO workspace_state (key, value)
574 VALUES ('current_task_id', ?)
575 "#,
576 )
577 .bind(id.to_string())
578 .execute(&mut *tx)
579 .await?;
580
581 tx.commit().await?;
582
583 if with_events {
584 self.get_task_with_events(id).await
585 } else {
586 let task = self.get_task(id).await?;
587 Ok(TaskWithEvents {
588 task,
589 events_summary: None,
590 })
591 }
592 }
593
594 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
598 let mut tx = self.pool.begin().await?;
599
600 let current_task_id: Option<String> =
602 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
603 .fetch_optional(&mut *tx)
604 .await?;
605
606 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
607 IntentError::InvalidInput(
608 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
609 ),
610 )?;
611
612 let task_info: (String, Option<i64>) =
614 sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
615 .bind(id)
616 .fetch_one(&mut *tx)
617 .await?;
618 let (task_name, parent_id) = task_info;
619
620 let uncompleted_children: i64 = sqlx::query_scalar(
622 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
623 )
624 .bind(id)
625 .fetch_one(&mut *tx)
626 .await?;
627
628 if uncompleted_children > 0 {
629 return Err(IntentError::UncompletedChildren);
630 }
631
632 let now = Utc::now();
633
634 sqlx::query(
636 r#"
637 UPDATE tasks
638 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
639 WHERE id = ?
640 "#,
641 )
642 .bind(now)
643 .bind(id)
644 .execute(&mut *tx)
645 .await?;
646
647 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
649 .execute(&mut *tx)
650 .await?;
651
652 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
654 let remaining_siblings: i64 = sqlx::query_scalar(
656 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
657 )
658 .bind(parent_task_id)
659 .bind(id)
660 .fetch_one(&mut *tx)
661 .await?;
662
663 if remaining_siblings == 0 {
664 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
666 .bind(parent_task_id)
667 .fetch_one(&mut *tx)
668 .await?;
669
670 NextStepSuggestion::ParentIsReady {
671 message: format!(
672 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
673 parent_task_id, parent_name
674 ),
675 parent_task_id,
676 parent_task_name: parent_name,
677 }
678 } else {
679 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
681 .bind(parent_task_id)
682 .fetch_one(&mut *tx)
683 .await?;
684
685 NextStepSuggestion::SiblingTasksRemain {
686 message: format!(
687 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
688 id, parent_task_id, parent_name
689 ),
690 parent_task_id,
691 parent_task_name: parent_name,
692 remaining_siblings_count: remaining_siblings,
693 }
694 }
695 } else {
696 let child_count: i64 =
698 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
699 .bind(id)
700 .fetch_one(&mut *tx)
701 .await?;
702
703 if child_count > 0 {
704 NextStepSuggestion::TopLevelTaskCompleted {
706 message: format!(
707 "Top-level task #{} '{}' has been completed. Well done!",
708 id, task_name
709 ),
710 completed_task_id: id,
711 completed_task_name: task_name.clone(),
712 }
713 } else {
714 let remaining_tasks: i64 = sqlx::query_scalar(
716 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
717 )
718 .bind(id)
719 .fetch_one(&mut *tx)
720 .await?;
721
722 if remaining_tasks == 0 {
723 NextStepSuggestion::WorkspaceIsClear {
724 message: format!(
725 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
726 id
727 ),
728 completed_task_id: id,
729 }
730 } else {
731 NextStepSuggestion::NoParentContext {
732 message: format!("Task #{} '{}' has been completed.", id, task_name),
733 completed_task_id: id,
734 completed_task_name: task_name.clone(),
735 }
736 }
737 }
738 };
739
740 tx.commit().await?;
741
742 let completed_task = self.get_task(id).await?;
743
744 Ok(DoneTaskResponse {
745 completed_task,
746 workspace_status: WorkspaceStatus {
747 current_task_id: None,
748 },
749 next_step_suggestion,
750 })
751 }
752
753 async fn check_task_exists(&self, id: i64) -> Result<()> {
755 let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
756 .bind(id)
757 .fetch_one(self.pool)
758 .await?;
759
760 if !exists {
761 return Err(IntentError::TaskNotFound(id));
762 }
763
764 Ok(())
765 }
766
767 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
769 let mut current_id = new_parent_id;
770
771 loop {
772 if current_id == task_id {
773 return Err(IntentError::CircularDependency {
774 blocking_task_id: new_parent_id,
775 blocked_task_id: task_id,
776 });
777 }
778
779 let parent: Option<i64> =
780 sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
781 .bind(current_id)
782 .fetch_optional(self.pool)
783 .await?;
784
785 match parent {
786 Some(pid) => current_id = pid,
787 None => break,
788 }
789 }
790
791 Ok(())
792 }
793
794 pub async fn switch_to_task(&self, id: i64) -> Result<SwitchTaskResponse> {
798 self.check_task_exists(id).await?;
800
801 let mut tx = self.pool.begin().await?;
802 let now = Utc::now();
803
804 let current_task_id: Option<String> =
806 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
807 .fetch_optional(&mut *tx)
808 .await?;
809
810 let previous_task = if let Some(prev_id_str) = current_task_id {
811 if let Ok(prev_id) = prev_id_str.parse::<i64>() {
812 sqlx::query(
814 r#"
815 UPDATE tasks
816 SET status = 'todo'
817 WHERE id = ? AND status = 'doing'
818 "#,
819 )
820 .bind(prev_id)
821 .execute(&mut *tx)
822 .await?;
823
824 Some(PreviousTaskInfo {
825 id: prev_id,
826 status: "todo".to_string(),
827 })
828 } else {
829 None
830 }
831 } else {
832 None
833 };
834
835 sqlx::query(
837 r#"
838 UPDATE tasks
839 SET status = 'doing',
840 first_doing_at = COALESCE(first_doing_at, ?)
841 WHERE id = ? AND status != 'doing'
842 "#,
843 )
844 .bind(now)
845 .bind(id)
846 .execute(&mut *tx)
847 .await?;
848
849 let (task_name, task_status): (String, String) =
851 sqlx::query_as("SELECT name, status FROM tasks WHERE id = ?")
852 .bind(id)
853 .fetch_one(&mut *tx)
854 .await?;
855
856 sqlx::query(
858 r#"
859 INSERT OR REPLACE INTO workspace_state (key, value)
860 VALUES ('current_task_id', ?)
861 "#,
862 )
863 .bind(id.to_string())
864 .execute(&mut *tx)
865 .await?;
866
867 tx.commit().await?;
868
869 Ok(SwitchTaskResponse {
870 previous_task,
871 current_task: CurrentTaskInfo {
872 id,
873 name: task_name,
874 status: task_status,
875 },
876 })
877 }
878
879 pub async fn spawn_subtask(
883 &self,
884 name: &str,
885 spec: Option<&str>,
886 ) -> Result<SpawnSubtaskResponse> {
887 let current_task_id: Option<String> =
889 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
890 .fetch_optional(self.pool)
891 .await?;
892
893 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
894 IntentError::InvalidInput("No current task to create subtask under".to_string()),
895 )?;
896
897 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
899 .bind(parent_id)
900 .fetch_one(self.pool)
901 .await?;
902
903 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
905
906 self.switch_to_task(subtask.id).await?;
908
909 Ok(SpawnSubtaskResponse {
910 subtask: SubtaskInfo {
911 id: subtask.id,
912 name: subtask.name,
913 parent_id,
914 status: "doing".to_string(),
915 },
916 parent_task: ParentTaskInfo {
917 id: parent_id,
918 name: parent_name,
919 },
920 })
921 }
922
923 pub async fn pick_next_tasks(
936 &self,
937 max_count: usize,
938 capacity_limit: usize,
939 ) -> Result<Vec<Task>> {
940 let mut tx = self.pool.begin().await?;
941
942 let doing_count: i64 =
944 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
945 .fetch_one(&mut *tx)
946 .await?;
947
948 let available = capacity_limit.saturating_sub(doing_count as usize);
950 if available == 0 {
951 return Ok(vec![]);
952 }
953
954 let limit = std::cmp::min(max_count, available);
955
956 let todo_tasks = sqlx::query_as::<_, Task>(
958 r#"
959 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
960 FROM tasks
961 WHERE status = 'todo'
962 ORDER BY
963 COALESCE(priority, 0) DESC,
964 COALESCE(complexity, 5) ASC,
965 id ASC
966 LIMIT ?
967 "#,
968 )
969 .bind(limit as i64)
970 .fetch_all(&mut *tx)
971 .await?;
972
973 if todo_tasks.is_empty() {
974 return Ok(vec![]);
975 }
976
977 let now = Utc::now();
978
979 for task in &todo_tasks {
981 sqlx::query(
982 r#"
983 UPDATE tasks
984 SET status = 'doing',
985 first_doing_at = COALESCE(first_doing_at, ?)
986 WHERE id = ?
987 "#,
988 )
989 .bind(now)
990 .bind(task.id)
991 .execute(&mut *tx)
992 .await?;
993 }
994
995 tx.commit().await?;
996
997 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
999 let placeholders = vec!["?"; task_ids.len()].join(",");
1000 let query = format!(
1001 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
1002 FROM tasks WHERE id IN ({})
1003 ORDER BY
1004 COALESCE(priority, 0) DESC,
1005 COALESCE(complexity, 5) ASC,
1006 id ASC",
1007 placeholders
1008 );
1009
1010 let mut q = sqlx::query_as::<_, Task>(&query);
1011 for id in task_ids {
1012 q = q.bind(id);
1013 }
1014
1015 let updated_tasks = q.fetch_all(self.pool).await?;
1016 Ok(updated_tasks)
1017 }
1018
1019 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1028 let current_task_id: Option<String> =
1030 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1031 .fetch_optional(self.pool)
1032 .await?;
1033
1034 if let Some(current_id_str) = current_task_id {
1035 if let Ok(current_id) = current_id_str.parse::<i64>() {
1036 let subtasks = sqlx::query_as::<_, Task>(
1039 r#"
1040 SELECT id, parent_id, name, spec, status, complexity, priority,
1041 first_todo_at, first_doing_at, first_done_at
1042 FROM tasks
1043 WHERE parent_id = ? AND status = 'todo'
1044 AND NOT EXISTS (
1045 SELECT 1 FROM dependencies d
1046 JOIN tasks bt ON d.blocking_task_id = bt.id
1047 WHERE d.blocked_task_id = tasks.id
1048 AND bt.status != 'done'
1049 )
1050 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1051 LIMIT 1
1052 "#,
1053 )
1054 .bind(current_id)
1055 .fetch_optional(self.pool)
1056 .await?;
1057
1058 if let Some(task) = subtasks {
1059 return Ok(PickNextResponse::focused_subtask(task));
1060 }
1061 }
1062 }
1063
1064 let top_level_task = sqlx::query_as::<_, Task>(
1067 r#"
1068 SELECT id, parent_id, name, spec, status, complexity, priority,
1069 first_todo_at, first_doing_at, first_done_at
1070 FROM tasks
1071 WHERE parent_id IS NULL AND status = 'todo'
1072 AND NOT EXISTS (
1073 SELECT 1 FROM dependencies d
1074 JOIN tasks bt ON d.blocking_task_id = bt.id
1075 WHERE d.blocked_task_id = tasks.id
1076 AND bt.status != 'done'
1077 )
1078 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1079 LIMIT 1
1080 "#,
1081 )
1082 .fetch_optional(self.pool)
1083 .await?;
1084
1085 if let Some(task) = top_level_task {
1086 return Ok(PickNextResponse::top_level_task(task));
1087 }
1088
1089 let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
1092 .fetch_one(self.pool)
1093 .await?;
1094
1095 if total_tasks == 0 {
1096 return Ok(PickNextResponse::no_tasks_in_project());
1097 }
1098
1099 let todo_or_doing_count: i64 =
1101 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1102 .fetch_one(self.pool)
1103 .await?;
1104
1105 if todo_or_doing_count == 0 {
1106 return Ok(PickNextResponse::all_tasks_completed());
1107 }
1108
1109 Ok(PickNextResponse::no_available_todos())
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117 use crate::events::EventManager;
1118 use crate::test_utils::test_helpers::TestContext;
1119
1120 #[tokio::test]
1121 async fn test_add_task() {
1122 let ctx = TestContext::new().await;
1123 let manager = TaskManager::new(ctx.pool());
1124
1125 let task = manager.add_task("Test task", None, None).await.unwrap();
1126
1127 assert_eq!(task.name, "Test task");
1128 assert_eq!(task.status, "todo");
1129 assert!(task.first_todo_at.is_some());
1130 assert!(task.first_doing_at.is_none());
1131 assert!(task.first_done_at.is_none());
1132 }
1133
1134 #[tokio::test]
1135 async fn test_add_task_with_spec() {
1136 let ctx = TestContext::new().await;
1137 let manager = TaskManager::new(ctx.pool());
1138
1139 let spec = "This is a task specification";
1140 let task = manager
1141 .add_task("Test task", Some(spec), None)
1142 .await
1143 .unwrap();
1144
1145 assert_eq!(task.name, "Test task");
1146 assert_eq!(task.spec.as_deref(), Some(spec));
1147 }
1148
1149 #[tokio::test]
1150 async fn test_add_task_with_parent() {
1151 let ctx = TestContext::new().await;
1152 let manager = TaskManager::new(ctx.pool());
1153
1154 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1155 let child = manager
1156 .add_task("Child task", None, Some(parent.id))
1157 .await
1158 .unwrap();
1159
1160 assert_eq!(child.parent_id, Some(parent.id));
1161 }
1162
1163 #[tokio::test]
1164 async fn test_get_task() {
1165 let ctx = TestContext::new().await;
1166 let manager = TaskManager::new(ctx.pool());
1167
1168 let created = manager.add_task("Test task", None, None).await.unwrap();
1169 let retrieved = manager.get_task(created.id).await.unwrap();
1170
1171 assert_eq!(created.id, retrieved.id);
1172 assert_eq!(created.name, retrieved.name);
1173 }
1174
1175 #[tokio::test]
1176 async fn test_get_task_not_found() {
1177 let ctx = TestContext::new().await;
1178 let manager = TaskManager::new(ctx.pool());
1179
1180 let result = manager.get_task(999).await;
1181 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1182 }
1183
1184 #[tokio::test]
1185 async fn test_update_task_name() {
1186 let ctx = TestContext::new().await;
1187 let manager = TaskManager::new(ctx.pool());
1188
1189 let task = manager.add_task("Original name", None, None).await.unwrap();
1190 let updated = manager
1191 .update_task(task.id, Some("New name"), None, None, None, None, None)
1192 .await
1193 .unwrap();
1194
1195 assert_eq!(updated.name, "New name");
1196 }
1197
1198 #[tokio::test]
1199 async fn test_update_task_status() {
1200 let ctx = TestContext::new().await;
1201 let manager = TaskManager::new(ctx.pool());
1202
1203 let task = manager.add_task("Test task", None, None).await.unwrap();
1204 let updated = manager
1205 .update_task(task.id, None, None, None, Some("doing"), None, None)
1206 .await
1207 .unwrap();
1208
1209 assert_eq!(updated.status, "doing");
1210 assert!(updated.first_doing_at.is_some());
1211 }
1212
1213 #[tokio::test]
1214 async fn test_delete_task() {
1215 let ctx = TestContext::new().await;
1216 let manager = TaskManager::new(ctx.pool());
1217
1218 let task = manager.add_task("Test task", None, None).await.unwrap();
1219 manager.delete_task(task.id).await.unwrap();
1220
1221 let result = manager.get_task(task.id).await;
1222 assert!(result.is_err());
1223 }
1224
1225 #[tokio::test]
1226 async fn test_find_tasks_by_status() {
1227 let ctx = TestContext::new().await;
1228 let manager = TaskManager::new(ctx.pool());
1229
1230 manager.add_task("Todo task", None, None).await.unwrap();
1231 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1232 manager
1233 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1234 .await
1235 .unwrap();
1236
1237 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1238 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1239
1240 assert_eq!(todo_tasks.len(), 1);
1241 assert_eq!(doing_tasks.len(), 1);
1242 assert_eq!(doing_tasks[0].status, "doing");
1243 }
1244
1245 #[tokio::test]
1246 async fn test_find_tasks_by_parent() {
1247 let ctx = TestContext::new().await;
1248 let manager = TaskManager::new(ctx.pool());
1249
1250 let parent = manager.add_task("Parent", None, None).await.unwrap();
1251 manager
1252 .add_task("Child 1", None, Some(parent.id))
1253 .await
1254 .unwrap();
1255 manager
1256 .add_task("Child 2", None, Some(parent.id))
1257 .await
1258 .unwrap();
1259
1260 let children = manager
1261 .find_tasks(None, Some(Some(parent.id)))
1262 .await
1263 .unwrap();
1264
1265 assert_eq!(children.len(), 2);
1266 }
1267
1268 #[tokio::test]
1269 async fn test_start_task() {
1270 let ctx = TestContext::new().await;
1271 let manager = TaskManager::new(ctx.pool());
1272
1273 let task = manager.add_task("Test task", None, None).await.unwrap();
1274 let started = manager.start_task(task.id, false).await.unwrap();
1275
1276 assert_eq!(started.task.status, "doing");
1277 assert!(started.task.first_doing_at.is_some());
1278
1279 let current: Option<String> =
1281 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1282 .fetch_optional(ctx.pool())
1283 .await
1284 .unwrap();
1285
1286 assert_eq!(current, Some(task.id.to_string()));
1287 }
1288
1289 #[tokio::test]
1290 async fn test_start_task_with_events() {
1291 let ctx = TestContext::new().await;
1292 let manager = TaskManager::new(ctx.pool());
1293
1294 let task = manager.add_task("Test task", None, None).await.unwrap();
1295
1296 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1298 .bind(task.id)
1299 .bind("test")
1300 .bind("test event")
1301 .execute(ctx.pool())
1302 .await
1303 .unwrap();
1304
1305 let started = manager.start_task(task.id, true).await.unwrap();
1306
1307 assert!(started.events_summary.is_some());
1308 let summary = started.events_summary.unwrap();
1309 assert_eq!(summary.total_count, 1);
1310 }
1311
1312 #[tokio::test]
1313 async fn test_done_task() {
1314 let ctx = TestContext::new().await;
1315 let manager = TaskManager::new(ctx.pool());
1316
1317 let task = manager.add_task("Test task", None, None).await.unwrap();
1318 manager.start_task(task.id, false).await.unwrap();
1319 let response = manager.done_task().await.unwrap();
1320
1321 assert_eq!(response.completed_task.status, "done");
1322 assert!(response.completed_task.first_done_at.is_some());
1323 assert_eq!(response.workspace_status.current_task_id, None);
1324
1325 match response.next_step_suggestion {
1327 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1328 _ => panic!("Expected WorkspaceIsClear suggestion"),
1329 }
1330
1331 let current: Option<String> =
1333 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1334 .fetch_optional(ctx.pool())
1335 .await
1336 .unwrap();
1337
1338 assert!(current.is_none());
1339 }
1340
1341 #[tokio::test]
1342 async fn test_done_task_with_uncompleted_children() {
1343 let ctx = TestContext::new().await;
1344 let manager = TaskManager::new(ctx.pool());
1345
1346 let parent = manager.add_task("Parent", None, None).await.unwrap();
1347 manager
1348 .add_task("Child", None, Some(parent.id))
1349 .await
1350 .unwrap();
1351
1352 manager.start_task(parent.id, false).await.unwrap();
1354
1355 let result = manager.done_task().await;
1356 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1357 }
1358
1359 #[tokio::test]
1360 async fn test_done_task_with_completed_children() {
1361 let ctx = TestContext::new().await;
1362 let manager = TaskManager::new(ctx.pool());
1363
1364 let parent = manager.add_task("Parent", None, None).await.unwrap();
1365 let child = manager
1366 .add_task("Child", None, Some(parent.id))
1367 .await
1368 .unwrap();
1369
1370 manager.start_task(child.id, false).await.unwrap();
1372 let child_response = manager.done_task().await.unwrap();
1373
1374 match child_response.next_step_suggestion {
1376 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1377 assert_eq!(parent_task_id, parent.id);
1378 },
1379 _ => panic!("Expected ParentIsReady suggestion"),
1380 }
1381
1382 manager.start_task(parent.id, false).await.unwrap();
1384 let parent_response = manager.done_task().await.unwrap();
1385 assert_eq!(parent_response.completed_task.status, "done");
1386
1387 match parent_response.next_step_suggestion {
1389 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1390 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1391 }
1392 }
1393
1394 #[tokio::test]
1395 async fn test_circular_dependency() {
1396 let ctx = TestContext::new().await;
1397 let manager = TaskManager::new(ctx.pool());
1398
1399 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1400 let task2 = manager
1401 .add_task("Task 2", None, Some(task1.id))
1402 .await
1403 .unwrap();
1404
1405 let result = manager
1407 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1408 .await;
1409
1410 assert!(matches!(
1411 result,
1412 Err(IntentError::CircularDependency { .. })
1413 ));
1414 }
1415
1416 #[tokio::test]
1417 async fn test_invalid_parent_id() {
1418 let ctx = TestContext::new().await;
1419 let manager = TaskManager::new(ctx.pool());
1420
1421 let result = manager.add_task("Test", None, Some(999)).await;
1422 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1423 }
1424
1425 #[tokio::test]
1426 async fn test_update_task_complexity_and_priority() {
1427 let ctx = TestContext::new().await;
1428 let manager = TaskManager::new(ctx.pool());
1429
1430 let task = manager.add_task("Test task", None, None).await.unwrap();
1431 let updated = manager
1432 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1433 .await
1434 .unwrap();
1435
1436 assert_eq!(updated.complexity, Some(8));
1437 assert_eq!(updated.priority, Some(10));
1438 }
1439
1440 #[tokio::test]
1441 async fn test_switch_to_task() {
1442 let ctx = TestContext::new().await;
1443 let manager = TaskManager::new(ctx.pool());
1444
1445 let task = manager.add_task("Test task", None, None).await.unwrap();
1447 assert_eq!(task.status, "todo");
1448
1449 let response = manager.switch_to_task(task.id).await.unwrap();
1451 assert_eq!(response.current_task.id, task.id);
1452 assert_eq!(response.current_task.status, "doing");
1453 assert!(response.previous_task.is_none());
1454
1455 let current: Option<String> =
1457 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1458 .fetch_optional(ctx.pool())
1459 .await
1460 .unwrap();
1461
1462 assert_eq!(current, Some(task.id.to_string()));
1463 }
1464
1465 #[tokio::test]
1466 async fn test_switch_to_task_already_doing() {
1467 let ctx = TestContext::new().await;
1468 let manager = TaskManager::new(ctx.pool());
1469
1470 let task = manager.add_task("Test task", None, None).await.unwrap();
1472 manager.start_task(task.id, false).await.unwrap();
1473
1474 let response = manager.switch_to_task(task.id).await.unwrap();
1476 assert_eq!(response.current_task.id, task.id);
1477 assert_eq!(response.current_task.status, "doing");
1478 }
1479
1480 #[tokio::test]
1481 async fn test_spawn_subtask() {
1482 let ctx = TestContext::new().await;
1483 let manager = TaskManager::new(ctx.pool());
1484
1485 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1487 manager.start_task(parent.id, false).await.unwrap();
1488
1489 let response = manager
1491 .spawn_subtask("Child task", Some("Details"))
1492 .await
1493 .unwrap();
1494
1495 assert_eq!(response.subtask.parent_id, parent.id);
1496 assert_eq!(response.subtask.name, "Child task");
1497 assert_eq!(response.subtask.status, "doing");
1498 assert_eq!(response.parent_task.id, parent.id);
1499 assert_eq!(response.parent_task.name, "Parent task");
1500
1501 let current: Option<String> =
1503 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1504 .fetch_optional(ctx.pool())
1505 .await
1506 .unwrap();
1507
1508 assert_eq!(current, Some(response.subtask.id.to_string()));
1509
1510 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1512 assert_eq!(retrieved.status, "doing");
1513 }
1514
1515 #[tokio::test]
1516 async fn test_spawn_subtask_no_current_task() {
1517 let ctx = TestContext::new().await;
1518 let manager = TaskManager::new(ctx.pool());
1519
1520 let result = manager.spawn_subtask("Child", None).await;
1522 assert!(result.is_err());
1523 }
1524
1525 #[tokio::test]
1526 async fn test_pick_next_tasks_basic() {
1527 let ctx = TestContext::new().await;
1528 let manager = TaskManager::new(ctx.pool());
1529
1530 for i in 1..=10 {
1532 manager
1533 .add_task(&format!("Task {}", i), None, None)
1534 .await
1535 .unwrap();
1536 }
1537
1538 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1540
1541 assert_eq!(picked.len(), 5);
1542 for task in &picked {
1543 assert_eq!(task.status, "doing");
1544 assert!(task.first_doing_at.is_some());
1545 }
1546
1547 let doing_count: i64 =
1549 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1550 .fetch_one(ctx.pool())
1551 .await
1552 .unwrap();
1553
1554 assert_eq!(doing_count, 5);
1555 }
1556
1557 #[tokio::test]
1558 async fn test_pick_next_tasks_with_existing_doing() {
1559 let ctx = TestContext::new().await;
1560 let manager = TaskManager::new(ctx.pool());
1561
1562 for i in 1..=10 {
1564 manager
1565 .add_task(&format!("Task {}", i), None, None)
1566 .await
1567 .unwrap();
1568 }
1569
1570 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1572 manager.start_task(tasks[0].id, false).await.unwrap();
1573 manager.start_task(tasks[1].id, false).await.unwrap();
1574
1575 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1577
1578 assert_eq!(picked.len(), 3);
1580
1581 let doing_count: i64 =
1583 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1584 .fetch_one(ctx.pool())
1585 .await
1586 .unwrap();
1587
1588 assert_eq!(doing_count, 5);
1589 }
1590
1591 #[tokio::test]
1592 async fn test_pick_next_tasks_at_capacity() {
1593 let ctx = TestContext::new().await;
1594 let manager = TaskManager::new(ctx.pool());
1595
1596 for i in 1..=10 {
1598 manager
1599 .add_task(&format!("Task {}", i), None, None)
1600 .await
1601 .unwrap();
1602 }
1603
1604 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1606 assert_eq!(first_batch.len(), 5);
1607
1608 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1610 assert_eq!(second_batch.len(), 0);
1611 }
1612
1613 #[tokio::test]
1614 async fn test_pick_next_tasks_priority_ordering() {
1615 let ctx = TestContext::new().await;
1616 let manager = TaskManager::new(ctx.pool());
1617
1618 let low = manager.add_task("Low priority", None, None).await.unwrap();
1620 manager
1621 .update_task(low.id, None, None, None, None, None, Some(1))
1622 .await
1623 .unwrap();
1624
1625 let high = manager.add_task("High priority", None, None).await.unwrap();
1626 manager
1627 .update_task(high.id, None, None, None, None, None, Some(10))
1628 .await
1629 .unwrap();
1630
1631 let medium = manager
1632 .add_task("Medium priority", None, None)
1633 .await
1634 .unwrap();
1635 manager
1636 .update_task(medium.id, None, None, None, None, None, Some(5))
1637 .await
1638 .unwrap();
1639
1640 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1642
1643 assert_eq!(picked.len(), 3);
1645 assert_eq!(picked[0].priority, Some(10)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(1)); }
1649
1650 #[tokio::test]
1651 async fn test_pick_next_tasks_complexity_ordering() {
1652 let ctx = TestContext::new().await;
1653 let manager = TaskManager::new(ctx.pool());
1654
1655 let complex = manager.add_task("Complex", None, None).await.unwrap();
1657 manager
1658 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1659 .await
1660 .unwrap();
1661
1662 let simple = manager.add_task("Simple", None, None).await.unwrap();
1663 manager
1664 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1665 .await
1666 .unwrap();
1667
1668 let medium = manager.add_task("Medium", None, None).await.unwrap();
1669 manager
1670 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1671 .await
1672 .unwrap();
1673
1674 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1676
1677 assert_eq!(picked.len(), 3);
1679 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1683
1684 #[tokio::test]
1685 async fn test_done_task_sibling_tasks_remain() {
1686 let ctx = TestContext::new().await;
1687 let manager = TaskManager::new(ctx.pool());
1688
1689 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1691 let child1 = manager
1692 .add_task("Child 1", None, Some(parent.id))
1693 .await
1694 .unwrap();
1695 let child2 = manager
1696 .add_task("Child 2", None, Some(parent.id))
1697 .await
1698 .unwrap();
1699 let _child3 = manager
1700 .add_task("Child 3", None, Some(parent.id))
1701 .await
1702 .unwrap();
1703
1704 manager.start_task(child1.id, false).await.unwrap();
1706 let response = manager.done_task().await.unwrap();
1707
1708 match response.next_step_suggestion {
1710 NextStepSuggestion::SiblingTasksRemain {
1711 parent_task_id,
1712 remaining_siblings_count,
1713 ..
1714 } => {
1715 assert_eq!(parent_task_id, parent.id);
1716 assert_eq!(remaining_siblings_count, 2); },
1718 _ => panic!("Expected SiblingTasksRemain suggestion"),
1719 }
1720
1721 manager.start_task(child2.id, false).await.unwrap();
1723 let response2 = manager.done_task().await.unwrap();
1724
1725 match response2.next_step_suggestion {
1727 NextStepSuggestion::SiblingTasksRemain {
1728 remaining_siblings_count,
1729 ..
1730 } => {
1731 assert_eq!(remaining_siblings_count, 1); },
1733 _ => panic!("Expected SiblingTasksRemain suggestion"),
1734 }
1735 }
1736
1737 #[tokio::test]
1738 async fn test_done_task_top_level_with_children() {
1739 let ctx = TestContext::new().await;
1740 let manager = TaskManager::new(ctx.pool());
1741
1742 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1744 let child = manager
1745 .add_task("Sub Task", None, Some(parent.id))
1746 .await
1747 .unwrap();
1748
1749 manager.start_task(child.id, false).await.unwrap();
1751 manager.done_task().await.unwrap();
1752
1753 manager.start_task(parent.id, false).await.unwrap();
1755 let response = manager.done_task().await.unwrap();
1756
1757 match response.next_step_suggestion {
1759 NextStepSuggestion::TopLevelTaskCompleted {
1760 completed_task_id,
1761 completed_task_name,
1762 ..
1763 } => {
1764 assert_eq!(completed_task_id, parent.id);
1765 assert_eq!(completed_task_name, "Epic Task");
1766 },
1767 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1768 }
1769 }
1770
1771 #[tokio::test]
1772 async fn test_done_task_no_parent_context() {
1773 let ctx = TestContext::new().await;
1774 let manager = TaskManager::new(ctx.pool());
1775
1776 let task1 = manager
1778 .add_task("Standalone Task 1", None, None)
1779 .await
1780 .unwrap();
1781 let _task2 = manager
1782 .add_task("Standalone Task 2", None, None)
1783 .await
1784 .unwrap();
1785
1786 manager.start_task(task1.id, false).await.unwrap();
1788 let response = manager.done_task().await.unwrap();
1789
1790 match response.next_step_suggestion {
1792 NextStepSuggestion::NoParentContext {
1793 completed_task_id,
1794 completed_task_name,
1795 ..
1796 } => {
1797 assert_eq!(completed_task_id, task1.id);
1798 assert_eq!(completed_task_name, "Standalone Task 1");
1799 },
1800 _ => panic!("Expected NoParentContext suggestion"),
1801 }
1802 }
1803
1804 #[tokio::test]
1805 async fn test_search_tasks_by_name() {
1806 let ctx = TestContext::new().await;
1807 let manager = TaskManager::new(ctx.pool());
1808
1809 manager
1811 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1812 .await
1813 .unwrap();
1814 manager
1815 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1816 .await
1817 .unwrap();
1818 manager
1819 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1820 .await
1821 .unwrap();
1822
1823 let results = manager.search_tasks("authentication").await.unwrap();
1825
1826 assert_eq!(results.len(), 2);
1827 assert!(results[0]
1828 .task
1829 .name
1830 .to_lowercase()
1831 .contains("authentication"));
1832 assert!(results[1]
1833 .task
1834 .name
1835 .to_lowercase()
1836 .contains("authentication"));
1837
1838 assert!(!results[0].match_snippet.is_empty());
1840 }
1841
1842 #[tokio::test]
1843 async fn test_search_tasks_by_spec() {
1844 let ctx = TestContext::new().await;
1845 let manager = TaskManager::new(ctx.pool());
1846
1847 manager
1849 .add_task("Task 1", Some("Implement JWT authentication"), None)
1850 .await
1851 .unwrap();
1852 manager
1853 .add_task("Task 2", Some("Add user registration"), None)
1854 .await
1855 .unwrap();
1856 manager
1857 .add_task("Task 3", Some("JWT token refresh"), None)
1858 .await
1859 .unwrap();
1860
1861 let results = manager.search_tasks("JWT").await.unwrap();
1863
1864 assert_eq!(results.len(), 2);
1865 for result in &results {
1866 assert!(result
1867 .task
1868 .spec
1869 .as_ref()
1870 .unwrap()
1871 .to_uppercase()
1872 .contains("JWT"));
1873 }
1874 }
1875
1876 #[tokio::test]
1877 async fn test_search_tasks_with_advanced_query() {
1878 let ctx = TestContext::new().await;
1879 let manager = TaskManager::new(ctx.pool());
1880
1881 manager
1883 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1884 .await
1885 .unwrap();
1886 manager
1887 .add_task("Feature", Some("Add authentication feature"), None)
1888 .await
1889 .unwrap();
1890 manager
1891 .add_task("Bug report", Some("Report critical database bug"), None)
1892 .await
1893 .unwrap();
1894
1895 let results = manager
1897 .search_tasks("authentication AND bug")
1898 .await
1899 .unwrap();
1900
1901 assert_eq!(results.len(), 1);
1902 assert!(results[0]
1903 .task
1904 .spec
1905 .as_ref()
1906 .unwrap()
1907 .contains("authentication"));
1908 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1909 }
1910
1911 #[tokio::test]
1912 async fn test_search_tasks_no_results() {
1913 let ctx = TestContext::new().await;
1914 let manager = TaskManager::new(ctx.pool());
1915
1916 manager
1918 .add_task("Task 1", Some("Some description"), None)
1919 .await
1920 .unwrap();
1921
1922 let results = manager.search_tasks("nonexistent").await.unwrap();
1924
1925 assert_eq!(results.len(), 0);
1926 }
1927
1928 #[tokio::test]
1929 async fn test_search_tasks_snippet_highlighting() {
1930 let ctx = TestContext::new().await;
1931 let manager = TaskManager::new(ctx.pool());
1932
1933 manager
1935 .add_task(
1936 "Test task",
1937 Some("This is a description with the keyword authentication in the middle"),
1938 None,
1939 )
1940 .await
1941 .unwrap();
1942
1943 let results = manager.search_tasks("authentication").await.unwrap();
1945
1946 assert_eq!(results.len(), 1);
1947 assert!(results[0].match_snippet.contains("**authentication**"));
1949 }
1950
1951 #[tokio::test]
1952 async fn test_pick_next_focused_subtask() {
1953 let ctx = TestContext::new().await;
1954 let manager = TaskManager::new(ctx.pool());
1955
1956 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1958 manager.start_task(parent.id, false).await.unwrap();
1959
1960 let subtask1 = manager
1962 .add_task("Subtask 1", None, Some(parent.id))
1963 .await
1964 .unwrap();
1965 let subtask2 = manager
1966 .add_task("Subtask 2", None, Some(parent.id))
1967 .await
1968 .unwrap();
1969
1970 manager
1972 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1973 .await
1974 .unwrap();
1975 manager
1976 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1977 .await
1978 .unwrap();
1979
1980 let response = manager.pick_next().await.unwrap();
1982
1983 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1984 assert!(response.task.is_some());
1985 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1986 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1987 }
1988
1989 #[tokio::test]
1990 async fn test_pick_next_top_level_task() {
1991 let ctx = TestContext::new().await;
1992 let manager = TaskManager::new(ctx.pool());
1993
1994 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1996 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1997
1998 manager
2000 .update_task(task1.id, None, None, None, None, None, Some(5))
2001 .await
2002 .unwrap();
2003 manager
2004 .update_task(task2.id, None, None, None, None, None, Some(3))
2005 .await
2006 .unwrap();
2007
2008 let response = manager.pick_next().await.unwrap();
2010
2011 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2012 assert!(response.task.is_some());
2013 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2014 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2015 }
2016
2017 #[tokio::test]
2018 async fn test_pick_next_no_tasks() {
2019 let ctx = TestContext::new().await;
2020 let manager = TaskManager::new(ctx.pool());
2021
2022 let response = manager.pick_next().await.unwrap();
2024
2025 assert_eq!(response.suggestion_type, "NONE");
2026 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2027 assert!(response.message.is_some());
2028 }
2029
2030 #[tokio::test]
2031 async fn test_pick_next_all_completed() {
2032 let ctx = TestContext::new().await;
2033 let manager = TaskManager::new(ctx.pool());
2034
2035 let task = manager.add_task("Task 1", None, None).await.unwrap();
2037 manager.start_task(task.id, false).await.unwrap();
2038 manager.done_task().await.unwrap();
2039
2040 let response = manager.pick_next().await.unwrap();
2042
2043 assert_eq!(response.suggestion_type, "NONE");
2044 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2045 assert!(response.message.is_some());
2046 }
2047
2048 #[tokio::test]
2049 async fn test_pick_next_no_available_todos() {
2050 let ctx = TestContext::new().await;
2051 let manager = TaskManager::new(ctx.pool());
2052
2053 let parent = manager.add_task("Parent task", None, None).await.unwrap();
2055 manager.start_task(parent.id, false).await.unwrap();
2056
2057 let subtask = manager
2059 .add_task("Subtask", None, Some(parent.id))
2060 .await
2061 .unwrap();
2062 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2064 .bind(subtask.id)
2065 .execute(ctx.pool())
2066 .await
2067 .unwrap();
2068
2069 sqlx::query(
2071 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
2072 )
2073 .bind(subtask.id.to_string())
2074 .execute(ctx.pool())
2075 .await
2076 .unwrap();
2077
2078 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2080 .bind(parent.id)
2081 .execute(ctx.pool())
2082 .await
2083 .unwrap();
2084
2085 let response = manager.pick_next().await.unwrap();
2087
2088 assert_eq!(response.suggestion_type, "NONE");
2089 assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
2090 assert!(response.message.is_some());
2091 }
2092
2093 #[tokio::test]
2094 async fn test_pick_next_priority_ordering() {
2095 let ctx = TestContext::new().await;
2096 let manager = TaskManager::new(ctx.pool());
2097
2098 let parent = manager.add_task("Parent", None, None).await.unwrap();
2100 manager.start_task(parent.id, false).await.unwrap();
2101
2102 let sub1 = manager
2104 .add_task("Priority 10", None, Some(parent.id))
2105 .await
2106 .unwrap();
2107 manager
2108 .update_task(sub1.id, None, None, None, None, None, Some(10))
2109 .await
2110 .unwrap();
2111
2112 let sub2 = manager
2113 .add_task("Priority 1", None, Some(parent.id))
2114 .await
2115 .unwrap();
2116 manager
2117 .update_task(sub2.id, None, None, None, None, None, Some(1))
2118 .await
2119 .unwrap();
2120
2121 let sub3 = manager
2122 .add_task("Priority 5", None, Some(parent.id))
2123 .await
2124 .unwrap();
2125 manager
2126 .update_task(sub3.id, None, None, None, None, None, Some(5))
2127 .await
2128 .unwrap();
2129
2130 let response = manager.pick_next().await.unwrap();
2132
2133 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2134 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2135 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2136 }
2137
2138 #[tokio::test]
2139 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2140 let ctx = TestContext::new().await;
2141 let manager = TaskManager::new(ctx.pool());
2142
2143 let parent = manager.add_task("Parent", None, None).await.unwrap();
2145 manager.start_task(parent.id, false).await.unwrap();
2146
2147 let top_level = manager
2149 .add_task("Top level task", None, None)
2150 .await
2151 .unwrap();
2152
2153 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, top_level.id);
2158 }
2159
2160 #[tokio::test]
2163 async fn test_get_task_with_events() {
2164 let ctx = TestContext::new().await;
2165 let task_mgr = TaskManager::new(ctx.pool());
2166 let event_mgr = EventManager::new(ctx.pool());
2167
2168 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2169
2170 event_mgr
2172 .add_event(task.id, "progress", "Event 1")
2173 .await
2174 .unwrap();
2175 event_mgr
2176 .add_event(task.id, "decision", "Event 2")
2177 .await
2178 .unwrap();
2179
2180 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2181
2182 assert_eq!(result.task.id, task.id);
2183 assert!(result.events_summary.is_some());
2184
2185 let summary = result.events_summary.unwrap();
2186 assert_eq!(summary.total_count, 2);
2187 assert_eq!(summary.recent_events.len(), 2);
2188 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2190 }
2191
2192 #[tokio::test]
2193 async fn test_get_task_with_events_nonexistent() {
2194 let ctx = TestContext::new().await;
2195 let task_mgr = TaskManager::new(ctx.pool());
2196
2197 let result = task_mgr.get_task_with_events(999).await;
2198 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2199 }
2200
2201 #[tokio::test]
2202 async fn test_get_task_with_many_events() {
2203 let ctx = TestContext::new().await;
2204 let task_mgr = TaskManager::new(ctx.pool());
2205 let event_mgr = EventManager::new(ctx.pool());
2206
2207 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2208
2209 for i in 0..20 {
2211 event_mgr
2212 .add_event(task.id, "test", &format!("Event {}", i))
2213 .await
2214 .unwrap();
2215 }
2216
2217 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2218 let summary = result.events_summary.unwrap();
2219
2220 assert_eq!(summary.total_count, 20);
2221 assert_eq!(summary.recent_events.len(), 10); }
2223
2224 #[tokio::test]
2225 async fn test_get_task_with_no_events() {
2226 let ctx = TestContext::new().await;
2227 let task_mgr = TaskManager::new(ctx.pool());
2228
2229 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2230
2231 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2232 let summary = result.events_summary.unwrap();
2233
2234 assert_eq!(summary.total_count, 0);
2235 assert_eq!(summary.recent_events.len(), 0);
2236 }
2237
2238 #[tokio::test]
2239 async fn test_pick_next_tasks_zero_capacity() {
2240 let ctx = TestContext::new().await;
2241 let task_mgr = TaskManager::new(ctx.pool());
2242
2243 task_mgr.add_task("Task 1", None, None).await.unwrap();
2244
2245 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2247 assert_eq!(results.len(), 0);
2248 }
2249
2250 #[tokio::test]
2251 async fn test_pick_next_tasks_capacity_exceeds_available() {
2252 let ctx = TestContext::new().await;
2253 let task_mgr = TaskManager::new(ctx.pool());
2254
2255 task_mgr.add_task("Task 1", None, None).await.unwrap();
2256 task_mgr.add_task("Task 2", None, None).await.unwrap();
2257
2258 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2260 assert_eq!(results.len(), 2); }
2262
2263 #[tokio::test]
2266 async fn test_get_task_context_root_task_no_relations() {
2267 let ctx = TestContext::new().await;
2268 let task_mgr = TaskManager::new(ctx.pool());
2269
2270 let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2272
2273 let context = task_mgr.get_task_context(task.id).await.unwrap();
2274
2275 assert_eq!(context.task.id, task.id);
2277 assert_eq!(context.task.name, "Root task");
2278
2279 assert_eq!(context.ancestors.len(), 0);
2281
2282 assert_eq!(context.siblings.len(), 0);
2284
2285 assert_eq!(context.children.len(), 0);
2287 }
2288
2289 #[tokio::test]
2290 async fn test_get_task_context_with_siblings() {
2291 let ctx = TestContext::new().await;
2292 let task_mgr = TaskManager::new(ctx.pool());
2293
2294 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2296 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2297 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2298
2299 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2300
2301 assert_eq!(context.task.id, task2.id);
2303
2304 assert_eq!(context.ancestors.len(), 0);
2306
2307 assert_eq!(context.siblings.len(), 2);
2309 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2310 assert!(sibling_ids.contains(&task1.id));
2311 assert!(sibling_ids.contains(&task3.id));
2312 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2316 }
2317
2318 #[tokio::test]
2319 async fn test_get_task_context_with_parent() {
2320 let ctx = TestContext::new().await;
2321 let task_mgr = TaskManager::new(ctx.pool());
2322
2323 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2325 let child = task_mgr
2326 .add_task("Child task", None, Some(parent.id))
2327 .await
2328 .unwrap();
2329
2330 let context = task_mgr.get_task_context(child.id).await.unwrap();
2331
2332 assert_eq!(context.task.id, child.id);
2334 assert_eq!(context.task.parent_id, Some(parent.id));
2335
2336 assert_eq!(context.ancestors.len(), 1);
2338 assert_eq!(context.ancestors[0].id, parent.id);
2339 assert_eq!(context.ancestors[0].name, "Parent task");
2340
2341 assert_eq!(context.siblings.len(), 0);
2343
2344 assert_eq!(context.children.len(), 0);
2346 }
2347
2348 #[tokio::test]
2349 async fn test_get_task_context_with_children() {
2350 let ctx = TestContext::new().await;
2351 let task_mgr = TaskManager::new(ctx.pool());
2352
2353 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2355 let child1 = task_mgr
2356 .add_task("Child 1", None, Some(parent.id))
2357 .await
2358 .unwrap();
2359 let child2 = task_mgr
2360 .add_task("Child 2", None, Some(parent.id))
2361 .await
2362 .unwrap();
2363 let child3 = task_mgr
2364 .add_task("Child 3", None, Some(parent.id))
2365 .await
2366 .unwrap();
2367
2368 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2369
2370 assert_eq!(context.task.id, parent.id);
2372
2373 assert_eq!(context.ancestors.len(), 0);
2375
2376 assert_eq!(context.siblings.len(), 0);
2378
2379 assert_eq!(context.children.len(), 3);
2381 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2382 assert!(child_ids.contains(&child1.id));
2383 assert!(child_ids.contains(&child2.id));
2384 assert!(child_ids.contains(&child3.id));
2385 }
2386
2387 #[tokio::test]
2388 async fn test_get_task_context_multi_level_hierarchy() {
2389 let ctx = TestContext::new().await;
2390 let task_mgr = TaskManager::new(ctx.pool());
2391
2392 let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2394 let parent = task_mgr
2395 .add_task("Parent", None, Some(grandparent.id))
2396 .await
2397 .unwrap();
2398 let child = task_mgr
2399 .add_task("Child", None, Some(parent.id))
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).await.unwrap();
2435 let child1 = task_mgr
2436 .add_task("Child1", None, Some(root.id))
2437 .await
2438 .unwrap();
2439 let child2 = task_mgr
2440 .add_task("Child2", None, Some(root.id))
2441 .await
2442 .unwrap();
2443 let grandchild1 = task_mgr
2444 .add_task("Grandchild1", None, Some(child1.id))
2445 .await
2446 .unwrap();
2447 let grandchild2 = task_mgr
2448 .add_task("Grandchild2", None, Some(child1.id))
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).await.unwrap();
2486
2487 let child_low = task_mgr
2489 .add_task("Low priority", None, Some(parent.id))
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))
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))
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).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).await.unwrap();
2547 let task3 = task_mgr.add_task("Task 3", 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}