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_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
87 let mut chain = Vec::new();
88 let mut current_id = Some(task_id);
89
90 while let Some(id) = current_id {
91 let task = self.get_task(id).await?;
92 current_id = task.parent_id;
93 chain.push(task);
94 }
95
96 Ok(chain)
97 }
98
99 pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
107 let task = self.get_task(id).await?;
109
110 let mut ancestors = Vec::new();
112 let mut current_parent_id = task.parent_id;
113
114 while let Some(parent_id) = current_parent_id {
115 let parent = self.get_task(parent_id).await?;
116 current_parent_id = parent.parent_id;
117 ancestors.push(parent);
118 }
119
120 let siblings = if let Some(parent_id) = task.parent_id {
122 sqlx::query_as::<_, Task>(
123 r#"
124 SELECT id, parent_id, name, spec, status, complexity, priority,
125 first_todo_at, first_doing_at, first_done_at
126 FROM tasks
127 WHERE parent_id = ? AND id != ?
128 ORDER BY priority ASC NULLS LAST, id ASC
129 "#,
130 )
131 .bind(parent_id)
132 .bind(id)
133 .fetch_all(self.pool)
134 .await?
135 } else {
136 sqlx::query_as::<_, Task>(
138 r#"
139 SELECT id, parent_id, name, spec, status, complexity, priority,
140 first_todo_at, first_doing_at, first_done_at
141 FROM tasks
142 WHERE parent_id IS NULL AND id != ?
143 ORDER BY priority ASC NULLS LAST, id ASC
144 "#,
145 )
146 .bind(id)
147 .fetch_all(self.pool)
148 .await?
149 };
150
151 let children = sqlx::query_as::<_, Task>(
153 r#"
154 SELECT id, parent_id, name, spec, status, complexity, priority,
155 first_todo_at, first_doing_at, first_done_at
156 FROM tasks
157 WHERE parent_id = ?
158 ORDER BY priority ASC NULLS LAST, id ASC
159 "#,
160 )
161 .bind(id)
162 .fetch_all(self.pool)
163 .await?;
164
165 let blocking_tasks = sqlx::query_as::<_, Task>(
167 r#"
168 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
169 t.first_todo_at, t.first_doing_at, t.first_done_at
170 FROM tasks t
171 JOIN dependencies d ON t.id = d.blocking_task_id
172 WHERE d.blocked_task_id = ?
173 ORDER BY t.priority ASC NULLS LAST, t.id ASC
174 "#,
175 )
176 .bind(id)
177 .fetch_all(self.pool)
178 .await?;
179
180 let blocked_by_tasks = sqlx::query_as::<_, Task>(
182 r#"
183 SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
184 t.first_todo_at, t.first_doing_at, t.first_done_at
185 FROM tasks t
186 JOIN dependencies d ON t.id = d.blocked_task_id
187 WHERE d.blocking_task_id = ?
188 ORDER BY t.priority ASC NULLS LAST, t.id ASC
189 "#,
190 )
191 .bind(id)
192 .fetch_all(self.pool)
193 .await?;
194
195 Ok(TaskContext {
196 task,
197 ancestors,
198 siblings,
199 children,
200 dependencies: crate::db::models::TaskDependencies {
201 blocking_tasks,
202 blocked_by_tasks,
203 },
204 })
205 }
206
207 async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
209 let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
210 .bind(task_id)
211 .fetch_one(self.pool)
212 .await?;
213
214 let recent_events = sqlx::query_as::<_, Event>(
215 r#"
216 SELECT id, task_id, timestamp, log_type, discussion_data
217 FROM events
218 WHERE task_id = ?
219 ORDER BY timestamp DESC
220 LIMIT 10
221 "#,
222 )
223 .bind(task_id)
224 .fetch_all(self.pool)
225 .await?;
226
227 Ok(EventsSummary {
228 total_count,
229 recent_events,
230 })
231 }
232
233 #[allow(clippy::too_many_arguments)]
235 pub async fn update_task(
236 &self,
237 id: i64,
238 name: Option<&str>,
239 spec: Option<&str>,
240 parent_id: Option<Option<i64>>,
241 status: Option<&str>,
242 complexity: Option<i32>,
243 priority: Option<i32>,
244 ) -> Result<Task> {
245 let task = self.get_task(id).await?;
247
248 if let Some(s) = status {
250 if !["todo", "doing", "done"].contains(&s) {
251 return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
252 }
253 }
254
255 if let Some(Some(pid)) = parent_id {
257 if pid == id {
258 return Err(IntentError::CircularDependency {
259 blocking_task_id: pid,
260 blocked_task_id: id,
261 });
262 }
263 self.check_task_exists(pid).await?;
264 self.check_circular_dependency(id, pid).await?;
265 }
266
267 let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
269 sqlx::QueryBuilder::new("UPDATE tasks SET ");
270 let mut has_updates = false;
271
272 if let Some(n) = name {
273 if has_updates {
274 builder.push(", ");
275 }
276 builder.push("name = ").push_bind(n);
277 has_updates = true;
278 }
279
280 if let Some(s) = spec {
281 if has_updates {
282 builder.push(", ");
283 }
284 builder.push("spec = ").push_bind(s);
285 has_updates = true;
286 }
287
288 if let Some(pid) = parent_id {
289 if has_updates {
290 builder.push(", ");
291 }
292 match pid {
293 Some(p) => {
294 builder.push("parent_id = ").push_bind(p);
295 },
296 None => {
297 builder.push("parent_id = NULL");
298 },
299 }
300 has_updates = true;
301 }
302
303 if let Some(c) = complexity {
304 if has_updates {
305 builder.push(", ");
306 }
307 builder.push("complexity = ").push_bind(c);
308 has_updates = true;
309 }
310
311 if let Some(p) = priority {
312 if has_updates {
313 builder.push(", ");
314 }
315 builder.push("priority = ").push_bind(p);
316 has_updates = true;
317 }
318
319 if let Some(s) = status {
320 if has_updates {
321 builder.push(", ");
322 }
323 builder.push("status = ").push_bind(s);
324 has_updates = true;
325
326 let now = Utc::now();
328 let timestamp = now.to_rfc3339();
329 match s {
330 "todo" if task.first_todo_at.is_none() => {
331 builder.push(", first_todo_at = ").push_bind(timestamp);
332 },
333 "doing" if task.first_doing_at.is_none() => {
334 builder.push(", first_doing_at = ").push_bind(timestamp);
335 },
336 "done" if task.first_done_at.is_none() => {
337 builder.push(", first_done_at = ").push_bind(timestamp);
338 },
339 _ => {},
340 }
341 }
342
343 if !has_updates {
344 return Ok(task);
345 }
346
347 builder.push(" WHERE id = ").push_bind(id);
348
349 builder.build().execute(self.pool).await?;
350
351 self.get_task(id).await
352 }
353
354 pub async fn delete_task(&self, id: i64) -> Result<()> {
356 self.check_task_exists(id).await?;
357
358 sqlx::query("DELETE FROM tasks WHERE id = ?")
359 .bind(id)
360 .execute(self.pool)
361 .await?;
362
363 Ok(())
364 }
365
366 pub async fn find_tasks(
368 &self,
369 status: Option<&str>,
370 parent_id: Option<Option<i64>>,
371 ) -> Result<Vec<Task>> {
372 let mut query = String::from(
373 "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"
374 );
375 let mut conditions = Vec::new();
376
377 if let Some(s) = status {
378 query.push_str(" AND status = ?");
379 conditions.push(s.to_string());
380 }
381
382 if let Some(pid) = parent_id {
383 if let Some(p) = pid {
384 query.push_str(" AND parent_id = ?");
385 conditions.push(p.to_string());
386 } else {
387 query.push_str(" AND parent_id IS NULL");
388 }
389 }
390
391 query.push_str(" ORDER BY id");
392
393 let mut q = sqlx::query_as::<_, Task>(&query);
394 for cond in conditions {
395 q = q.bind(cond);
396 }
397
398 let tasks = q.fetch_all(self.pool).await?;
399 Ok(tasks)
400 }
401
402 pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
405 if query.trim().is_empty() {
407 return Ok(Vec::new());
408 }
409
410 let has_searchable = query
413 .chars()
414 .any(|c| c.is_alphanumeric() || crate::search::is_cjk_char(c));
415 if !has_searchable {
416 return Ok(Vec::new());
417 }
418
419 if crate::search::needs_like_fallback(query) {
422 self.search_tasks_like(query).await
423 } else {
424 self.search_tasks_fts5(query).await
425 }
426 }
427
428 async fn search_tasks_fts5(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
430 let escaped_query = self.escape_fts_query(query);
432
433 let results = sqlx::query(
437 r#"
438 SELECT
439 t.id,
440 t.parent_id,
441 t.name,
442 t.spec,
443 t.status,
444 t.complexity,
445 t.priority,
446 t.first_todo_at,
447 t.first_doing_at,
448 t.first_done_at,
449 COALESCE(
450 snippet(tasks_fts, 1, '**', '**', '...', 15),
451 snippet(tasks_fts, 0, '**', '**', '...', 15)
452 ) as match_snippet
453 FROM tasks_fts
454 INNER JOIN tasks t ON tasks_fts.rowid = t.id
455 WHERE tasks_fts MATCH ?
456 ORDER BY rank
457 "#,
458 )
459 .bind(&escaped_query)
460 .fetch_all(self.pool)
461 .await?;
462
463 let mut search_results = Vec::new();
464 for row in results {
465 let task = Task {
466 id: row.get("id"),
467 parent_id: row.get("parent_id"),
468 name: row.get("name"),
469 spec: row.get("spec"),
470 status: row.get("status"),
471 complexity: row.get("complexity"),
472 priority: row.get("priority"),
473 first_todo_at: row.get("first_todo_at"),
474 first_doing_at: row.get("first_doing_at"),
475 first_done_at: row.get("first_done_at"),
476 };
477 let match_snippet: String = row.get("match_snippet");
478
479 search_results.push(TaskSearchResult {
480 task,
481 match_snippet,
482 });
483 }
484
485 Ok(search_results)
486 }
487
488 async fn search_tasks_like(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
490 let pattern = format!("%{}%", query);
491
492 let results = sqlx::query(
493 r#"
494 SELECT
495 id,
496 parent_id,
497 name,
498 spec,
499 status,
500 complexity,
501 priority,
502 first_todo_at,
503 first_doing_at,
504 first_done_at
505 FROM tasks
506 WHERE name LIKE ? OR spec LIKE ?
507 ORDER BY name
508 "#,
509 )
510 .bind(&pattern)
511 .bind(&pattern)
512 .fetch_all(self.pool)
513 .await?;
514
515 let mut search_results = Vec::new();
516 for row in results {
517 let task = Task {
518 id: row.get("id"),
519 parent_id: row.get("parent_id"),
520 name: row.get("name"),
521 spec: row.get("spec"),
522 status: row.get("status"),
523 complexity: row.get("complexity"),
524 priority: row.get("priority"),
525 first_todo_at: row.get("first_todo_at"),
526 first_doing_at: row.get("first_doing_at"),
527 first_done_at: row.get("first_done_at"),
528 };
529
530 let name: String = row.get("name");
532 let spec: Option<String> = row.get("spec");
533
534 let match_snippet = if name.contains(query) {
535 format!("**{}**", name)
536 } else if let Some(ref s) = spec {
537 if s.contains(query) {
538 format!("**{}**", s)
539 } else {
540 name.clone()
541 }
542 } else {
543 name
544 };
545
546 search_results.push(TaskSearchResult {
547 task,
548 match_snippet,
549 });
550 }
551
552 Ok(search_results)
553 }
554
555 fn escape_fts_query(&self, query: &str) -> String {
557 query.replace('"', "\"\"")
561 }
562
563 pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
565 use crate::dependencies::get_incomplete_blocking_tasks;
567 if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
568 return Err(IntentError::TaskBlocked {
569 task_id: id,
570 blocking_task_ids: blocking_tasks,
571 });
572 }
573
574 let mut tx = self.pool.begin().await?;
575
576 let now = Utc::now();
577
578 sqlx::query(
580 r#"
581 UPDATE tasks
582 SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
583 WHERE id = ?
584 "#,
585 )
586 .bind(now)
587 .bind(id)
588 .execute(&mut *tx)
589 .await?;
590
591 sqlx::query(
593 r#"
594 INSERT OR REPLACE INTO workspace_state (key, value)
595 VALUES ('current_task_id', ?)
596 "#,
597 )
598 .bind(id.to_string())
599 .execute(&mut *tx)
600 .await?;
601
602 tx.commit().await?;
603
604 if with_events {
605 self.get_task_with_events(id).await
606 } else {
607 let task = self.get_task(id).await?;
608 Ok(TaskWithEvents {
609 task,
610 events_summary: None,
611 })
612 }
613 }
614
615 pub async fn done_task(&self) -> Result<DoneTaskResponse> {
619 let mut tx = self.pool.begin().await?;
620
621 let current_task_id: Option<String> =
623 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
624 .fetch_optional(&mut *tx)
625 .await?;
626
627 let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
628 IntentError::InvalidInput(
629 "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
630 ),
631 )?;
632
633 let task_info: (String, Option<i64>) =
635 sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
636 .bind(id)
637 .fetch_one(&mut *tx)
638 .await?;
639 let (task_name, parent_id) = task_info;
640
641 let uncompleted_children: i64 = sqlx::query_scalar(
643 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
644 )
645 .bind(id)
646 .fetch_one(&mut *tx)
647 .await?;
648
649 if uncompleted_children > 0 {
650 return Err(IntentError::UncompletedChildren);
651 }
652
653 let now = Utc::now();
654
655 sqlx::query(
657 r#"
658 UPDATE tasks
659 SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
660 WHERE id = ?
661 "#,
662 )
663 .bind(now)
664 .bind(id)
665 .execute(&mut *tx)
666 .await?;
667
668 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
670 .execute(&mut *tx)
671 .await?;
672
673 let next_step_suggestion = if let Some(parent_task_id) = parent_id {
675 let remaining_siblings: i64 = sqlx::query_scalar(
677 "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
678 )
679 .bind(parent_task_id)
680 .bind(id)
681 .fetch_one(&mut *tx)
682 .await?;
683
684 if remaining_siblings == 0 {
685 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
687 .bind(parent_task_id)
688 .fetch_one(&mut *tx)
689 .await?;
690
691 NextStepSuggestion::ParentIsReady {
692 message: format!(
693 "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
694 parent_task_id, parent_name
695 ),
696 parent_task_id,
697 parent_task_name: parent_name,
698 }
699 } else {
700 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
702 .bind(parent_task_id)
703 .fetch_one(&mut *tx)
704 .await?;
705
706 NextStepSuggestion::SiblingTasksRemain {
707 message: format!(
708 "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
709 id, parent_task_id, parent_name
710 ),
711 parent_task_id,
712 parent_task_name: parent_name,
713 remaining_siblings_count: remaining_siblings,
714 }
715 }
716 } else {
717 let child_count: i64 =
719 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
720 .bind(id)
721 .fetch_one(&mut *tx)
722 .await?;
723
724 if child_count > 0 {
725 NextStepSuggestion::TopLevelTaskCompleted {
727 message: format!(
728 "Top-level task #{} '{}' has been completed. Well done!",
729 id, task_name
730 ),
731 completed_task_id: id,
732 completed_task_name: task_name.clone(),
733 }
734 } else {
735 let remaining_tasks: i64 = sqlx::query_scalar(
737 "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
738 )
739 .bind(id)
740 .fetch_one(&mut *tx)
741 .await?;
742
743 if remaining_tasks == 0 {
744 NextStepSuggestion::WorkspaceIsClear {
745 message: format!(
746 "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
747 id
748 ),
749 completed_task_id: id,
750 }
751 } else {
752 NextStepSuggestion::NoParentContext {
753 message: format!("Task #{} '{}' has been completed.", id, task_name),
754 completed_task_id: id,
755 completed_task_name: task_name.clone(),
756 }
757 }
758 }
759 };
760
761 tx.commit().await?;
762
763 let completed_task = self.get_task(id).await?;
764
765 Ok(DoneTaskResponse {
766 completed_task,
767 workspace_status: WorkspaceStatus {
768 current_task_id: None,
769 },
770 next_step_suggestion,
771 })
772 }
773
774 async fn check_task_exists(&self, id: i64) -> Result<()> {
776 let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
777 .bind(id)
778 .fetch_one(self.pool)
779 .await?;
780
781 if !exists {
782 return Err(IntentError::TaskNotFound(id));
783 }
784
785 Ok(())
786 }
787
788 async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
790 let mut current_id = new_parent_id;
791
792 loop {
793 if current_id == task_id {
794 return Err(IntentError::CircularDependency {
795 blocking_task_id: new_parent_id,
796 blocked_task_id: task_id,
797 });
798 }
799
800 let parent: Option<i64> =
801 sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
802 .bind(current_id)
803 .fetch_optional(self.pool)
804 .await?;
805
806 match parent {
807 Some(pid) => current_id = pid,
808 None => break,
809 }
810 }
811
812 Ok(())
813 }
814
815 pub async fn switch_to_task(&self, id: i64) -> Result<SwitchTaskResponse> {
819 self.check_task_exists(id).await?;
821
822 let mut tx = self.pool.begin().await?;
823 let now = Utc::now();
824
825 let current_task_id: Option<String> =
827 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
828 .fetch_optional(&mut *tx)
829 .await?;
830
831 let previous_task = if let Some(prev_id_str) = current_task_id {
832 if let Ok(prev_id) = prev_id_str.parse::<i64>() {
833 sqlx::query(
835 r#"
836 UPDATE tasks
837 SET status = 'todo'
838 WHERE id = ? AND status = 'doing'
839 "#,
840 )
841 .bind(prev_id)
842 .execute(&mut *tx)
843 .await?;
844
845 Some(PreviousTaskInfo {
846 id: prev_id,
847 status: "todo".to_string(),
848 })
849 } else {
850 None
851 }
852 } else {
853 None
854 };
855
856 sqlx::query(
858 r#"
859 UPDATE tasks
860 SET status = 'doing',
861 first_doing_at = COALESCE(first_doing_at, ?)
862 WHERE id = ? AND status != 'doing'
863 "#,
864 )
865 .bind(now)
866 .bind(id)
867 .execute(&mut *tx)
868 .await?;
869
870 let (task_name, task_status): (String, String) =
872 sqlx::query_as("SELECT name, status FROM tasks WHERE id = ?")
873 .bind(id)
874 .fetch_one(&mut *tx)
875 .await?;
876
877 sqlx::query(
879 r#"
880 INSERT OR REPLACE INTO workspace_state (key, value)
881 VALUES ('current_task_id', ?)
882 "#,
883 )
884 .bind(id.to_string())
885 .execute(&mut *tx)
886 .await?;
887
888 tx.commit().await?;
889
890 Ok(SwitchTaskResponse {
891 previous_task,
892 current_task: CurrentTaskInfo {
893 id,
894 name: task_name,
895 status: task_status,
896 },
897 })
898 }
899
900 pub async fn spawn_subtask(
904 &self,
905 name: &str,
906 spec: Option<&str>,
907 ) -> Result<SpawnSubtaskResponse> {
908 let current_task_id: Option<String> =
910 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
911 .fetch_optional(self.pool)
912 .await?;
913
914 let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
915 IntentError::InvalidInput("No current task to create subtask under".to_string()),
916 )?;
917
918 let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
920 .bind(parent_id)
921 .fetch_one(self.pool)
922 .await?;
923
924 let subtask = self.add_task(name, spec, Some(parent_id)).await?;
926
927 self.switch_to_task(subtask.id).await?;
929
930 Ok(SpawnSubtaskResponse {
931 subtask: SubtaskInfo {
932 id: subtask.id,
933 name: subtask.name,
934 parent_id,
935 status: "doing".to_string(),
936 },
937 parent_task: ParentTaskInfo {
938 id: parent_id,
939 name: parent_name,
940 },
941 })
942 }
943
944 pub async fn pick_next_tasks(
957 &self,
958 max_count: usize,
959 capacity_limit: usize,
960 ) -> Result<Vec<Task>> {
961 let mut tx = self.pool.begin().await?;
962
963 let doing_count: i64 =
965 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
966 .fetch_one(&mut *tx)
967 .await?;
968
969 let available = capacity_limit.saturating_sub(doing_count as usize);
971 if available == 0 {
972 return Ok(vec![]);
973 }
974
975 let limit = std::cmp::min(max_count, available);
976
977 let todo_tasks = sqlx::query_as::<_, Task>(
979 r#"
980 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
981 FROM tasks
982 WHERE status = 'todo'
983 ORDER BY
984 COALESCE(priority, 0) DESC,
985 COALESCE(complexity, 5) ASC,
986 id ASC
987 LIMIT ?
988 "#,
989 )
990 .bind(limit as i64)
991 .fetch_all(&mut *tx)
992 .await?;
993
994 if todo_tasks.is_empty() {
995 return Ok(vec![]);
996 }
997
998 let now = Utc::now();
999
1000 for task in &todo_tasks {
1002 sqlx::query(
1003 r#"
1004 UPDATE tasks
1005 SET status = 'doing',
1006 first_doing_at = COALESCE(first_doing_at, ?)
1007 WHERE id = ?
1008 "#,
1009 )
1010 .bind(now)
1011 .bind(task.id)
1012 .execute(&mut *tx)
1013 .await?;
1014 }
1015
1016 tx.commit().await?;
1017
1018 let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1020 let placeholders = vec!["?"; task_ids.len()].join(",");
1021 let query = format!(
1022 "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
1023 FROM tasks WHERE id IN ({})
1024 ORDER BY
1025 COALESCE(priority, 0) DESC,
1026 COALESCE(complexity, 5) ASC,
1027 id ASC",
1028 placeholders
1029 );
1030
1031 let mut q = sqlx::query_as::<_, Task>(&query);
1032 for id in task_ids {
1033 q = q.bind(id);
1034 }
1035
1036 let updated_tasks = q.fetch_all(self.pool).await?;
1037 Ok(updated_tasks)
1038 }
1039
1040 pub async fn pick_next(&self) -> Result<PickNextResponse> {
1049 let current_task_id: Option<String> =
1051 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1052 .fetch_optional(self.pool)
1053 .await?;
1054
1055 if let Some(current_id_str) = current_task_id {
1056 if let Ok(current_id) = current_id_str.parse::<i64>() {
1057 let subtasks = sqlx::query_as::<_, Task>(
1060 r#"
1061 SELECT id, parent_id, name, spec, status, complexity, priority,
1062 first_todo_at, first_doing_at, first_done_at
1063 FROM tasks
1064 WHERE parent_id = ? AND status = 'todo'
1065 AND NOT EXISTS (
1066 SELECT 1 FROM dependencies d
1067 JOIN tasks bt ON d.blocking_task_id = bt.id
1068 WHERE d.blocked_task_id = tasks.id
1069 AND bt.status != 'done'
1070 )
1071 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1072 LIMIT 1
1073 "#,
1074 )
1075 .bind(current_id)
1076 .fetch_optional(self.pool)
1077 .await?;
1078
1079 if let Some(task) = subtasks {
1080 return Ok(PickNextResponse::focused_subtask(task));
1081 }
1082 }
1083 }
1084
1085 let top_level_task = sqlx::query_as::<_, Task>(
1088 r#"
1089 SELECT id, parent_id, name, spec, status, complexity, priority,
1090 first_todo_at, first_doing_at, first_done_at
1091 FROM tasks
1092 WHERE parent_id IS NULL AND status = 'todo'
1093 AND NOT EXISTS (
1094 SELECT 1 FROM dependencies d
1095 JOIN tasks bt ON d.blocking_task_id = bt.id
1096 WHERE d.blocked_task_id = tasks.id
1097 AND bt.status != 'done'
1098 )
1099 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1100 LIMIT 1
1101 "#,
1102 )
1103 .fetch_optional(self.pool)
1104 .await?;
1105
1106 if let Some(task) = top_level_task {
1107 return Ok(PickNextResponse::top_level_task(task));
1108 }
1109
1110 let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
1113 .fetch_one(self.pool)
1114 .await?;
1115
1116 if total_tasks == 0 {
1117 return Ok(PickNextResponse::no_tasks_in_project());
1118 }
1119
1120 let todo_or_doing_count: i64 =
1122 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1123 .fetch_one(self.pool)
1124 .await?;
1125
1126 if todo_or_doing_count == 0 {
1127 return Ok(PickNextResponse::all_tasks_completed());
1128 }
1129
1130 Ok(PickNextResponse::no_available_todos())
1132 }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138 use crate::events::EventManager;
1139 use crate::test_utils::test_helpers::TestContext;
1140
1141 #[tokio::test]
1142 async fn test_add_task() {
1143 let ctx = TestContext::new().await;
1144 let manager = TaskManager::new(ctx.pool());
1145
1146 let task = manager.add_task("Test task", None, None).await.unwrap();
1147
1148 assert_eq!(task.name, "Test task");
1149 assert_eq!(task.status, "todo");
1150 assert!(task.first_todo_at.is_some());
1151 assert!(task.first_doing_at.is_none());
1152 assert!(task.first_done_at.is_none());
1153 }
1154
1155 #[tokio::test]
1156 async fn test_add_task_with_spec() {
1157 let ctx = TestContext::new().await;
1158 let manager = TaskManager::new(ctx.pool());
1159
1160 let spec = "This is a task specification";
1161 let task = manager
1162 .add_task("Test task", Some(spec), None)
1163 .await
1164 .unwrap();
1165
1166 assert_eq!(task.name, "Test task");
1167 assert_eq!(task.spec.as_deref(), Some(spec));
1168 }
1169
1170 #[tokio::test]
1171 async fn test_add_task_with_parent() {
1172 let ctx = TestContext::new().await;
1173 let manager = TaskManager::new(ctx.pool());
1174
1175 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1176 let child = manager
1177 .add_task("Child task", None, Some(parent.id))
1178 .await
1179 .unwrap();
1180
1181 assert_eq!(child.parent_id, Some(parent.id));
1182 }
1183
1184 #[tokio::test]
1185 async fn test_get_task() {
1186 let ctx = TestContext::new().await;
1187 let manager = TaskManager::new(ctx.pool());
1188
1189 let created = manager.add_task("Test task", None, None).await.unwrap();
1190 let retrieved = manager.get_task(created.id).await.unwrap();
1191
1192 assert_eq!(created.id, retrieved.id);
1193 assert_eq!(created.name, retrieved.name);
1194 }
1195
1196 #[tokio::test]
1197 async fn test_get_task_not_found() {
1198 let ctx = TestContext::new().await;
1199 let manager = TaskManager::new(ctx.pool());
1200
1201 let result = manager.get_task(999).await;
1202 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1203 }
1204
1205 #[tokio::test]
1206 async fn test_update_task_name() {
1207 let ctx = TestContext::new().await;
1208 let manager = TaskManager::new(ctx.pool());
1209
1210 let task = manager.add_task("Original name", None, None).await.unwrap();
1211 let updated = manager
1212 .update_task(task.id, Some("New name"), None, None, None, None, None)
1213 .await
1214 .unwrap();
1215
1216 assert_eq!(updated.name, "New name");
1217 }
1218
1219 #[tokio::test]
1220 async fn test_update_task_status() {
1221 let ctx = TestContext::new().await;
1222 let manager = TaskManager::new(ctx.pool());
1223
1224 let task = manager.add_task("Test task", None, None).await.unwrap();
1225 let updated = manager
1226 .update_task(task.id, None, None, None, Some("doing"), None, None)
1227 .await
1228 .unwrap();
1229
1230 assert_eq!(updated.status, "doing");
1231 assert!(updated.first_doing_at.is_some());
1232 }
1233
1234 #[tokio::test]
1235 async fn test_delete_task() {
1236 let ctx = TestContext::new().await;
1237 let manager = TaskManager::new(ctx.pool());
1238
1239 let task = manager.add_task("Test task", None, None).await.unwrap();
1240 manager.delete_task(task.id).await.unwrap();
1241
1242 let result = manager.get_task(task.id).await;
1243 assert!(result.is_err());
1244 }
1245
1246 #[tokio::test]
1247 async fn test_find_tasks_by_status() {
1248 let ctx = TestContext::new().await;
1249 let manager = TaskManager::new(ctx.pool());
1250
1251 manager.add_task("Todo task", None, None).await.unwrap();
1252 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1253 manager
1254 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1255 .await
1256 .unwrap();
1257
1258 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1259 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1260
1261 assert_eq!(todo_tasks.len(), 1);
1262 assert_eq!(doing_tasks.len(), 1);
1263 assert_eq!(doing_tasks[0].status, "doing");
1264 }
1265
1266 #[tokio::test]
1267 async fn test_find_tasks_by_parent() {
1268 let ctx = TestContext::new().await;
1269 let manager = TaskManager::new(ctx.pool());
1270
1271 let parent = manager.add_task("Parent", None, None).await.unwrap();
1272 manager
1273 .add_task("Child 1", None, Some(parent.id))
1274 .await
1275 .unwrap();
1276 manager
1277 .add_task("Child 2", None, Some(parent.id))
1278 .await
1279 .unwrap();
1280
1281 let children = manager
1282 .find_tasks(None, Some(Some(parent.id)))
1283 .await
1284 .unwrap();
1285
1286 assert_eq!(children.len(), 2);
1287 }
1288
1289 #[tokio::test]
1290 async fn test_start_task() {
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 let started = manager.start_task(task.id, false).await.unwrap();
1296
1297 assert_eq!(started.task.status, "doing");
1298 assert!(started.task.first_doing_at.is_some());
1299
1300 let current: Option<String> =
1302 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1303 .fetch_optional(ctx.pool())
1304 .await
1305 .unwrap();
1306
1307 assert_eq!(current, Some(task.id.to_string()));
1308 }
1309
1310 #[tokio::test]
1311 async fn test_start_task_with_events() {
1312 let ctx = TestContext::new().await;
1313 let manager = TaskManager::new(ctx.pool());
1314
1315 let task = manager.add_task("Test task", None, None).await.unwrap();
1316
1317 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1319 .bind(task.id)
1320 .bind("test")
1321 .bind("test event")
1322 .execute(ctx.pool())
1323 .await
1324 .unwrap();
1325
1326 let started = manager.start_task(task.id, true).await.unwrap();
1327
1328 assert!(started.events_summary.is_some());
1329 let summary = started.events_summary.unwrap();
1330 assert_eq!(summary.total_count, 1);
1331 }
1332
1333 #[tokio::test]
1334 async fn test_done_task() {
1335 let ctx = TestContext::new().await;
1336 let manager = TaskManager::new(ctx.pool());
1337
1338 let task = manager.add_task("Test task", None, None).await.unwrap();
1339 manager.start_task(task.id, false).await.unwrap();
1340 let response = manager.done_task().await.unwrap();
1341
1342 assert_eq!(response.completed_task.status, "done");
1343 assert!(response.completed_task.first_done_at.is_some());
1344 assert_eq!(response.workspace_status.current_task_id, None);
1345
1346 match response.next_step_suggestion {
1348 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1349 _ => panic!("Expected WorkspaceIsClear suggestion"),
1350 }
1351
1352 let current: Option<String> =
1354 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1355 .fetch_optional(ctx.pool())
1356 .await
1357 .unwrap();
1358
1359 assert!(current.is_none());
1360 }
1361
1362 #[tokio::test]
1363 async fn test_done_task_with_uncompleted_children() {
1364 let ctx = TestContext::new().await;
1365 let manager = TaskManager::new(ctx.pool());
1366
1367 let parent = manager.add_task("Parent", None, None).await.unwrap();
1368 manager
1369 .add_task("Child", None, Some(parent.id))
1370 .await
1371 .unwrap();
1372
1373 manager.start_task(parent.id, false).await.unwrap();
1375
1376 let result = manager.done_task().await;
1377 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1378 }
1379
1380 #[tokio::test]
1381 async fn test_done_task_with_completed_children() {
1382 let ctx = TestContext::new().await;
1383 let manager = TaskManager::new(ctx.pool());
1384
1385 let parent = manager.add_task("Parent", None, None).await.unwrap();
1386 let child = manager
1387 .add_task("Child", None, Some(parent.id))
1388 .await
1389 .unwrap();
1390
1391 manager.start_task(child.id, false).await.unwrap();
1393 let child_response = manager.done_task().await.unwrap();
1394
1395 match child_response.next_step_suggestion {
1397 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1398 assert_eq!(parent_task_id, parent.id);
1399 },
1400 _ => panic!("Expected ParentIsReady suggestion"),
1401 }
1402
1403 manager.start_task(parent.id, false).await.unwrap();
1405 let parent_response = manager.done_task().await.unwrap();
1406 assert_eq!(parent_response.completed_task.status, "done");
1407
1408 match parent_response.next_step_suggestion {
1410 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1411 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1412 }
1413 }
1414
1415 #[tokio::test]
1416 async fn test_circular_dependency() {
1417 let ctx = TestContext::new().await;
1418 let manager = TaskManager::new(ctx.pool());
1419
1420 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1421 let task2 = manager
1422 .add_task("Task 2", None, Some(task1.id))
1423 .await
1424 .unwrap();
1425
1426 let result = manager
1428 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1429 .await;
1430
1431 assert!(matches!(
1432 result,
1433 Err(IntentError::CircularDependency { .. })
1434 ));
1435 }
1436
1437 #[tokio::test]
1438 async fn test_invalid_parent_id() {
1439 let ctx = TestContext::new().await;
1440 let manager = TaskManager::new(ctx.pool());
1441
1442 let result = manager.add_task("Test", None, Some(999)).await;
1443 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1444 }
1445
1446 #[tokio::test]
1447 async fn test_update_task_complexity_and_priority() {
1448 let ctx = TestContext::new().await;
1449 let manager = TaskManager::new(ctx.pool());
1450
1451 let task = manager.add_task("Test task", None, None).await.unwrap();
1452 let updated = manager
1453 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1454 .await
1455 .unwrap();
1456
1457 assert_eq!(updated.complexity, Some(8));
1458 assert_eq!(updated.priority, Some(10));
1459 }
1460
1461 #[tokio::test]
1462 async fn test_switch_to_task() {
1463 let ctx = TestContext::new().await;
1464 let manager = TaskManager::new(ctx.pool());
1465
1466 let task = manager.add_task("Test task", None, None).await.unwrap();
1468 assert_eq!(task.status, "todo");
1469
1470 let response = manager.switch_to_task(task.id).await.unwrap();
1472 assert_eq!(response.current_task.id, task.id);
1473 assert_eq!(response.current_task.status, "doing");
1474 assert!(response.previous_task.is_none());
1475
1476 let current: Option<String> =
1478 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1479 .fetch_optional(ctx.pool())
1480 .await
1481 .unwrap();
1482
1483 assert_eq!(current, Some(task.id.to_string()));
1484 }
1485
1486 #[tokio::test]
1487 async fn test_switch_to_task_already_doing() {
1488 let ctx = TestContext::new().await;
1489 let manager = TaskManager::new(ctx.pool());
1490
1491 let task = manager.add_task("Test task", None, None).await.unwrap();
1493 manager.start_task(task.id, false).await.unwrap();
1494
1495 let response = manager.switch_to_task(task.id).await.unwrap();
1497 assert_eq!(response.current_task.id, task.id);
1498 assert_eq!(response.current_task.status, "doing");
1499 }
1500
1501 #[tokio::test]
1502 async fn test_spawn_subtask() {
1503 let ctx = TestContext::new().await;
1504 let manager = TaskManager::new(ctx.pool());
1505
1506 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1508 manager.start_task(parent.id, false).await.unwrap();
1509
1510 let response = manager
1512 .spawn_subtask("Child task", Some("Details"))
1513 .await
1514 .unwrap();
1515
1516 assert_eq!(response.subtask.parent_id, parent.id);
1517 assert_eq!(response.subtask.name, "Child task");
1518 assert_eq!(response.subtask.status, "doing");
1519 assert_eq!(response.parent_task.id, parent.id);
1520 assert_eq!(response.parent_task.name, "Parent task");
1521
1522 let current: Option<String> =
1524 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1525 .fetch_optional(ctx.pool())
1526 .await
1527 .unwrap();
1528
1529 assert_eq!(current, Some(response.subtask.id.to_string()));
1530
1531 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1533 assert_eq!(retrieved.status, "doing");
1534 }
1535
1536 #[tokio::test]
1537 async fn test_spawn_subtask_no_current_task() {
1538 let ctx = TestContext::new().await;
1539 let manager = TaskManager::new(ctx.pool());
1540
1541 let result = manager.spawn_subtask("Child", None).await;
1543 assert!(result.is_err());
1544 }
1545
1546 #[tokio::test]
1547 async fn test_pick_next_tasks_basic() {
1548 let ctx = TestContext::new().await;
1549 let manager = TaskManager::new(ctx.pool());
1550
1551 for i in 1..=10 {
1553 manager
1554 .add_task(&format!("Task {}", i), None, None)
1555 .await
1556 .unwrap();
1557 }
1558
1559 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1561
1562 assert_eq!(picked.len(), 5);
1563 for task in &picked {
1564 assert_eq!(task.status, "doing");
1565 assert!(task.first_doing_at.is_some());
1566 }
1567
1568 let doing_count: i64 =
1570 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1571 .fetch_one(ctx.pool())
1572 .await
1573 .unwrap();
1574
1575 assert_eq!(doing_count, 5);
1576 }
1577
1578 #[tokio::test]
1579 async fn test_pick_next_tasks_with_existing_doing() {
1580 let ctx = TestContext::new().await;
1581 let manager = TaskManager::new(ctx.pool());
1582
1583 for i in 1..=10 {
1585 manager
1586 .add_task(&format!("Task {}", i), None, None)
1587 .await
1588 .unwrap();
1589 }
1590
1591 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1593 manager.start_task(tasks[0].id, false).await.unwrap();
1594 manager.start_task(tasks[1].id, false).await.unwrap();
1595
1596 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1598
1599 assert_eq!(picked.len(), 3);
1601
1602 let doing_count: i64 =
1604 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1605 .fetch_one(ctx.pool())
1606 .await
1607 .unwrap();
1608
1609 assert_eq!(doing_count, 5);
1610 }
1611
1612 #[tokio::test]
1613 async fn test_pick_next_tasks_at_capacity() {
1614 let ctx = TestContext::new().await;
1615 let manager = TaskManager::new(ctx.pool());
1616
1617 for i in 1..=10 {
1619 manager
1620 .add_task(&format!("Task {}", i), None, None)
1621 .await
1622 .unwrap();
1623 }
1624
1625 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1627 assert_eq!(first_batch.len(), 5);
1628
1629 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1631 assert_eq!(second_batch.len(), 0);
1632 }
1633
1634 #[tokio::test]
1635 async fn test_pick_next_tasks_priority_ordering() {
1636 let ctx = TestContext::new().await;
1637 let manager = TaskManager::new(ctx.pool());
1638
1639 let low = manager.add_task("Low priority", None, None).await.unwrap();
1641 manager
1642 .update_task(low.id, None, None, None, None, None, Some(1))
1643 .await
1644 .unwrap();
1645
1646 let high = manager.add_task("High priority", None, None).await.unwrap();
1647 manager
1648 .update_task(high.id, None, None, None, None, None, Some(10))
1649 .await
1650 .unwrap();
1651
1652 let medium = manager
1653 .add_task("Medium priority", None, None)
1654 .await
1655 .unwrap();
1656 manager
1657 .update_task(medium.id, None, None, None, None, None, Some(5))
1658 .await
1659 .unwrap();
1660
1661 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1663
1664 assert_eq!(picked.len(), 3);
1666 assert_eq!(picked[0].priority, Some(10)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(1)); }
1670
1671 #[tokio::test]
1672 async fn test_pick_next_tasks_complexity_ordering() {
1673 let ctx = TestContext::new().await;
1674 let manager = TaskManager::new(ctx.pool());
1675
1676 let complex = manager.add_task("Complex", None, None).await.unwrap();
1678 manager
1679 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1680 .await
1681 .unwrap();
1682
1683 let simple = manager.add_task("Simple", None, None).await.unwrap();
1684 manager
1685 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1686 .await
1687 .unwrap();
1688
1689 let medium = manager.add_task("Medium", None, None).await.unwrap();
1690 manager
1691 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1692 .await
1693 .unwrap();
1694
1695 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1697
1698 assert_eq!(picked.len(), 3);
1700 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1704
1705 #[tokio::test]
1706 async fn test_done_task_sibling_tasks_remain() {
1707 let ctx = TestContext::new().await;
1708 let manager = TaskManager::new(ctx.pool());
1709
1710 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1712 let child1 = manager
1713 .add_task("Child 1", None, Some(parent.id))
1714 .await
1715 .unwrap();
1716 let child2 = manager
1717 .add_task("Child 2", None, Some(parent.id))
1718 .await
1719 .unwrap();
1720 let _child3 = manager
1721 .add_task("Child 3", None, Some(parent.id))
1722 .await
1723 .unwrap();
1724
1725 manager.start_task(child1.id, false).await.unwrap();
1727 let response = manager.done_task().await.unwrap();
1728
1729 match response.next_step_suggestion {
1731 NextStepSuggestion::SiblingTasksRemain {
1732 parent_task_id,
1733 remaining_siblings_count,
1734 ..
1735 } => {
1736 assert_eq!(parent_task_id, parent.id);
1737 assert_eq!(remaining_siblings_count, 2); },
1739 _ => panic!("Expected SiblingTasksRemain suggestion"),
1740 }
1741
1742 manager.start_task(child2.id, false).await.unwrap();
1744 let response2 = manager.done_task().await.unwrap();
1745
1746 match response2.next_step_suggestion {
1748 NextStepSuggestion::SiblingTasksRemain {
1749 remaining_siblings_count,
1750 ..
1751 } => {
1752 assert_eq!(remaining_siblings_count, 1); },
1754 _ => panic!("Expected SiblingTasksRemain suggestion"),
1755 }
1756 }
1757
1758 #[tokio::test]
1759 async fn test_done_task_top_level_with_children() {
1760 let ctx = TestContext::new().await;
1761 let manager = TaskManager::new(ctx.pool());
1762
1763 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1765 let child = manager
1766 .add_task("Sub Task", None, Some(parent.id))
1767 .await
1768 .unwrap();
1769
1770 manager.start_task(child.id, false).await.unwrap();
1772 manager.done_task().await.unwrap();
1773
1774 manager.start_task(parent.id, false).await.unwrap();
1776 let response = manager.done_task().await.unwrap();
1777
1778 match response.next_step_suggestion {
1780 NextStepSuggestion::TopLevelTaskCompleted {
1781 completed_task_id,
1782 completed_task_name,
1783 ..
1784 } => {
1785 assert_eq!(completed_task_id, parent.id);
1786 assert_eq!(completed_task_name, "Epic Task");
1787 },
1788 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1789 }
1790 }
1791
1792 #[tokio::test]
1793 async fn test_done_task_no_parent_context() {
1794 let ctx = TestContext::new().await;
1795 let manager = TaskManager::new(ctx.pool());
1796
1797 let task1 = manager
1799 .add_task("Standalone Task 1", None, None)
1800 .await
1801 .unwrap();
1802 let _task2 = manager
1803 .add_task("Standalone Task 2", None, None)
1804 .await
1805 .unwrap();
1806
1807 manager.start_task(task1.id, false).await.unwrap();
1809 let response = manager.done_task().await.unwrap();
1810
1811 match response.next_step_suggestion {
1813 NextStepSuggestion::NoParentContext {
1814 completed_task_id,
1815 completed_task_name,
1816 ..
1817 } => {
1818 assert_eq!(completed_task_id, task1.id);
1819 assert_eq!(completed_task_name, "Standalone Task 1");
1820 },
1821 _ => panic!("Expected NoParentContext suggestion"),
1822 }
1823 }
1824
1825 #[tokio::test]
1826 async fn test_search_tasks_by_name() {
1827 let ctx = TestContext::new().await;
1828 let manager = TaskManager::new(ctx.pool());
1829
1830 manager
1832 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1833 .await
1834 .unwrap();
1835 manager
1836 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1837 .await
1838 .unwrap();
1839 manager
1840 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1841 .await
1842 .unwrap();
1843
1844 let results = manager.search_tasks("authentication").await.unwrap();
1846
1847 assert_eq!(results.len(), 2);
1848 assert!(results[0]
1849 .task
1850 .name
1851 .to_lowercase()
1852 .contains("authentication"));
1853 assert!(results[1]
1854 .task
1855 .name
1856 .to_lowercase()
1857 .contains("authentication"));
1858
1859 assert!(!results[0].match_snippet.is_empty());
1861 }
1862
1863 #[tokio::test]
1864 async fn test_search_tasks_by_spec() {
1865 let ctx = TestContext::new().await;
1866 let manager = TaskManager::new(ctx.pool());
1867
1868 manager
1870 .add_task("Task 1", Some("Implement JWT authentication"), None)
1871 .await
1872 .unwrap();
1873 manager
1874 .add_task("Task 2", Some("Add user registration"), None)
1875 .await
1876 .unwrap();
1877 manager
1878 .add_task("Task 3", Some("JWT token refresh"), None)
1879 .await
1880 .unwrap();
1881
1882 let results = manager.search_tasks("JWT").await.unwrap();
1884
1885 assert_eq!(results.len(), 2);
1886 for result in &results {
1887 assert!(result
1888 .task
1889 .spec
1890 .as_ref()
1891 .unwrap()
1892 .to_uppercase()
1893 .contains("JWT"));
1894 }
1895 }
1896
1897 #[tokio::test]
1898 async fn test_search_tasks_with_advanced_query() {
1899 let ctx = TestContext::new().await;
1900 let manager = TaskManager::new(ctx.pool());
1901
1902 manager
1904 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1905 .await
1906 .unwrap();
1907 manager
1908 .add_task("Feature", Some("Add authentication feature"), None)
1909 .await
1910 .unwrap();
1911 manager
1912 .add_task("Bug report", Some("Report critical database bug"), None)
1913 .await
1914 .unwrap();
1915
1916 let results = manager
1918 .search_tasks("authentication AND bug")
1919 .await
1920 .unwrap();
1921
1922 assert_eq!(results.len(), 1);
1923 assert!(results[0]
1924 .task
1925 .spec
1926 .as_ref()
1927 .unwrap()
1928 .contains("authentication"));
1929 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1930 }
1931
1932 #[tokio::test]
1933 async fn test_search_tasks_no_results() {
1934 let ctx = TestContext::new().await;
1935 let manager = TaskManager::new(ctx.pool());
1936
1937 manager
1939 .add_task("Task 1", Some("Some description"), None)
1940 .await
1941 .unwrap();
1942
1943 let results = manager.search_tasks("nonexistent").await.unwrap();
1945
1946 assert_eq!(results.len(), 0);
1947 }
1948
1949 #[tokio::test]
1950 async fn test_search_tasks_snippet_highlighting() {
1951 let ctx = TestContext::new().await;
1952 let manager = TaskManager::new(ctx.pool());
1953
1954 manager
1956 .add_task(
1957 "Test task",
1958 Some("This is a description with the keyword authentication in the middle"),
1959 None,
1960 )
1961 .await
1962 .unwrap();
1963
1964 let results = manager.search_tasks("authentication").await.unwrap();
1966
1967 assert_eq!(results.len(), 1);
1968 assert!(results[0].match_snippet.contains("**authentication**"));
1970 }
1971
1972 #[tokio::test]
1973 async fn test_pick_next_focused_subtask() {
1974 let ctx = TestContext::new().await;
1975 let manager = TaskManager::new(ctx.pool());
1976
1977 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1979 manager.start_task(parent.id, false).await.unwrap();
1980
1981 let subtask1 = manager
1983 .add_task("Subtask 1", None, Some(parent.id))
1984 .await
1985 .unwrap();
1986 let subtask2 = manager
1987 .add_task("Subtask 2", None, Some(parent.id))
1988 .await
1989 .unwrap();
1990
1991 manager
1993 .update_task(subtask1.id, None, None, None, None, None, Some(2))
1994 .await
1995 .unwrap();
1996 manager
1997 .update_task(subtask2.id, None, None, None, None, None, Some(1))
1998 .await
1999 .unwrap();
2000
2001 let response = manager.pick_next().await.unwrap();
2003
2004 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2005 assert!(response.task.is_some());
2006 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2007 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2008 }
2009
2010 #[tokio::test]
2011 async fn test_pick_next_top_level_task() {
2012 let ctx = TestContext::new().await;
2013 let manager = TaskManager::new(ctx.pool());
2014
2015 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
2017 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
2018
2019 manager
2021 .update_task(task1.id, None, None, None, None, None, Some(5))
2022 .await
2023 .unwrap();
2024 manager
2025 .update_task(task2.id, None, None, None, None, None, Some(3))
2026 .await
2027 .unwrap();
2028
2029 let response = manager.pick_next().await.unwrap();
2031
2032 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2033 assert!(response.task.is_some());
2034 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2035 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2036 }
2037
2038 #[tokio::test]
2039 async fn test_pick_next_no_tasks() {
2040 let ctx = TestContext::new().await;
2041 let manager = TaskManager::new(ctx.pool());
2042
2043 let response = manager.pick_next().await.unwrap();
2045
2046 assert_eq!(response.suggestion_type, "NONE");
2047 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2048 assert!(response.message.is_some());
2049 }
2050
2051 #[tokio::test]
2052 async fn test_pick_next_all_completed() {
2053 let ctx = TestContext::new().await;
2054 let manager = TaskManager::new(ctx.pool());
2055
2056 let task = manager.add_task("Task 1", None, None).await.unwrap();
2058 manager.start_task(task.id, false).await.unwrap();
2059 manager.done_task().await.unwrap();
2060
2061 let response = manager.pick_next().await.unwrap();
2063
2064 assert_eq!(response.suggestion_type, "NONE");
2065 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2066 assert!(response.message.is_some());
2067 }
2068
2069 #[tokio::test]
2070 async fn test_pick_next_no_available_todos() {
2071 let ctx = TestContext::new().await;
2072 let manager = TaskManager::new(ctx.pool());
2073
2074 let parent = manager.add_task("Parent task", None, None).await.unwrap();
2076 manager.start_task(parent.id, false).await.unwrap();
2077
2078 let subtask = manager
2080 .add_task("Subtask", None, Some(parent.id))
2081 .await
2082 .unwrap();
2083 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2085 .bind(subtask.id)
2086 .execute(ctx.pool())
2087 .await
2088 .unwrap();
2089
2090 sqlx::query(
2092 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
2093 )
2094 .bind(subtask.id.to_string())
2095 .execute(ctx.pool())
2096 .await
2097 .unwrap();
2098
2099 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2101 .bind(parent.id)
2102 .execute(ctx.pool())
2103 .await
2104 .unwrap();
2105
2106 let response = manager.pick_next().await.unwrap();
2108
2109 assert_eq!(response.suggestion_type, "NONE");
2110 assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
2111 assert!(response.message.is_some());
2112 }
2113
2114 #[tokio::test]
2115 async fn test_pick_next_priority_ordering() {
2116 let ctx = TestContext::new().await;
2117 let manager = TaskManager::new(ctx.pool());
2118
2119 let parent = manager.add_task("Parent", None, None).await.unwrap();
2121 manager.start_task(parent.id, false).await.unwrap();
2122
2123 let sub1 = manager
2125 .add_task("Priority 10", None, Some(parent.id))
2126 .await
2127 .unwrap();
2128 manager
2129 .update_task(sub1.id, None, None, None, None, None, Some(10))
2130 .await
2131 .unwrap();
2132
2133 let sub2 = manager
2134 .add_task("Priority 1", None, Some(parent.id))
2135 .await
2136 .unwrap();
2137 manager
2138 .update_task(sub2.id, None, None, None, None, None, Some(1))
2139 .await
2140 .unwrap();
2141
2142 let sub3 = manager
2143 .add_task("Priority 5", None, Some(parent.id))
2144 .await
2145 .unwrap();
2146 manager
2147 .update_task(sub3.id, None, None, None, None, None, Some(5))
2148 .await
2149 .unwrap();
2150
2151 let response = manager.pick_next().await.unwrap();
2153
2154 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2155 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2156 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2157 }
2158
2159 #[tokio::test]
2160 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2161 let ctx = TestContext::new().await;
2162 let manager = TaskManager::new(ctx.pool());
2163
2164 let parent = manager.add_task("Parent", None, None).await.unwrap();
2166 manager.start_task(parent.id, false).await.unwrap();
2167
2168 let top_level = manager
2170 .add_task("Top level task", None, None)
2171 .await
2172 .unwrap();
2173
2174 let response = manager.pick_next().await.unwrap();
2176
2177 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2178 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2179 }
2180
2181 #[tokio::test]
2184 async fn test_get_task_with_events() {
2185 let ctx = TestContext::new().await;
2186 let task_mgr = TaskManager::new(ctx.pool());
2187 let event_mgr = EventManager::new(ctx.pool());
2188
2189 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2190
2191 event_mgr
2193 .add_event(task.id, "progress", "Event 1")
2194 .await
2195 .unwrap();
2196 event_mgr
2197 .add_event(task.id, "decision", "Event 2")
2198 .await
2199 .unwrap();
2200
2201 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2202
2203 assert_eq!(result.task.id, task.id);
2204 assert!(result.events_summary.is_some());
2205
2206 let summary = result.events_summary.unwrap();
2207 assert_eq!(summary.total_count, 2);
2208 assert_eq!(summary.recent_events.len(), 2);
2209 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2211 }
2212
2213 #[tokio::test]
2214 async fn test_get_task_with_events_nonexistent() {
2215 let ctx = TestContext::new().await;
2216 let task_mgr = TaskManager::new(ctx.pool());
2217
2218 let result = task_mgr.get_task_with_events(999).await;
2219 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2220 }
2221
2222 #[tokio::test]
2223 async fn test_get_task_with_many_events() {
2224 let ctx = TestContext::new().await;
2225 let task_mgr = TaskManager::new(ctx.pool());
2226 let event_mgr = EventManager::new(ctx.pool());
2227
2228 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2229
2230 for i in 0..20 {
2232 event_mgr
2233 .add_event(task.id, "test", &format!("Event {}", i))
2234 .await
2235 .unwrap();
2236 }
2237
2238 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2239 let summary = result.events_summary.unwrap();
2240
2241 assert_eq!(summary.total_count, 20);
2242 assert_eq!(summary.recent_events.len(), 10); }
2244
2245 #[tokio::test]
2246 async fn test_get_task_with_no_events() {
2247 let ctx = TestContext::new().await;
2248 let task_mgr = TaskManager::new(ctx.pool());
2249
2250 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2251
2252 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2253 let summary = result.events_summary.unwrap();
2254
2255 assert_eq!(summary.total_count, 0);
2256 assert_eq!(summary.recent_events.len(), 0);
2257 }
2258
2259 #[tokio::test]
2260 async fn test_pick_next_tasks_zero_capacity() {
2261 let ctx = TestContext::new().await;
2262 let task_mgr = TaskManager::new(ctx.pool());
2263
2264 task_mgr.add_task("Task 1", None, None).await.unwrap();
2265
2266 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2268 assert_eq!(results.len(), 0);
2269 }
2270
2271 #[tokio::test]
2272 async fn test_pick_next_tasks_capacity_exceeds_available() {
2273 let ctx = TestContext::new().await;
2274 let task_mgr = TaskManager::new(ctx.pool());
2275
2276 task_mgr.add_task("Task 1", None, None).await.unwrap();
2277 task_mgr.add_task("Task 2", None, None).await.unwrap();
2278
2279 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2281 assert_eq!(results.len(), 2); }
2283
2284 #[tokio::test]
2287 async fn test_get_task_context_root_task_no_relations() {
2288 let ctx = TestContext::new().await;
2289 let task_mgr = TaskManager::new(ctx.pool());
2290
2291 let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2293
2294 let context = task_mgr.get_task_context(task.id).await.unwrap();
2295
2296 assert_eq!(context.task.id, task.id);
2298 assert_eq!(context.task.name, "Root task");
2299
2300 assert_eq!(context.ancestors.len(), 0);
2302
2303 assert_eq!(context.siblings.len(), 0);
2305
2306 assert_eq!(context.children.len(), 0);
2308 }
2309
2310 #[tokio::test]
2311 async fn test_get_task_context_with_siblings() {
2312 let ctx = TestContext::new().await;
2313 let task_mgr = TaskManager::new(ctx.pool());
2314
2315 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2317 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2318 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2319
2320 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2321
2322 assert_eq!(context.task.id, task2.id);
2324
2325 assert_eq!(context.ancestors.len(), 0);
2327
2328 assert_eq!(context.siblings.len(), 2);
2330 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2331 assert!(sibling_ids.contains(&task1.id));
2332 assert!(sibling_ids.contains(&task3.id));
2333 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2337 }
2338
2339 #[tokio::test]
2340 async fn test_get_task_context_with_parent() {
2341 let ctx = TestContext::new().await;
2342 let task_mgr = TaskManager::new(ctx.pool());
2343
2344 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2346 let child = task_mgr
2347 .add_task("Child task", None, Some(parent.id))
2348 .await
2349 .unwrap();
2350
2351 let context = task_mgr.get_task_context(child.id).await.unwrap();
2352
2353 assert_eq!(context.task.id, child.id);
2355 assert_eq!(context.task.parent_id, Some(parent.id));
2356
2357 assert_eq!(context.ancestors.len(), 1);
2359 assert_eq!(context.ancestors[0].id, parent.id);
2360 assert_eq!(context.ancestors[0].name, "Parent task");
2361
2362 assert_eq!(context.siblings.len(), 0);
2364
2365 assert_eq!(context.children.len(), 0);
2367 }
2368
2369 #[tokio::test]
2370 async fn test_get_task_context_with_children() {
2371 let ctx = TestContext::new().await;
2372 let task_mgr = TaskManager::new(ctx.pool());
2373
2374 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2376 let child1 = task_mgr
2377 .add_task("Child 1", None, Some(parent.id))
2378 .await
2379 .unwrap();
2380 let child2 = task_mgr
2381 .add_task("Child 2", None, Some(parent.id))
2382 .await
2383 .unwrap();
2384 let child3 = task_mgr
2385 .add_task("Child 3", None, Some(parent.id))
2386 .await
2387 .unwrap();
2388
2389 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2390
2391 assert_eq!(context.task.id, parent.id);
2393
2394 assert_eq!(context.ancestors.len(), 0);
2396
2397 assert_eq!(context.siblings.len(), 0);
2399
2400 assert_eq!(context.children.len(), 3);
2402 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2403 assert!(child_ids.contains(&child1.id));
2404 assert!(child_ids.contains(&child2.id));
2405 assert!(child_ids.contains(&child3.id));
2406 }
2407
2408 #[tokio::test]
2409 async fn test_get_task_context_multi_level_hierarchy() {
2410 let ctx = TestContext::new().await;
2411 let task_mgr = TaskManager::new(ctx.pool());
2412
2413 let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2415 let parent = task_mgr
2416 .add_task("Parent", None, Some(grandparent.id))
2417 .await
2418 .unwrap();
2419 let child = task_mgr
2420 .add_task("Child", None, Some(parent.id))
2421 .await
2422 .unwrap();
2423
2424 let context = task_mgr.get_task_context(child.id).await.unwrap();
2425
2426 assert_eq!(context.task.id, child.id);
2428
2429 assert_eq!(context.ancestors.len(), 2);
2431 assert_eq!(context.ancestors[0].id, parent.id);
2432 assert_eq!(context.ancestors[0].name, "Parent");
2433 assert_eq!(context.ancestors[1].id, grandparent.id);
2434 assert_eq!(context.ancestors[1].name, "Grandparent");
2435
2436 assert_eq!(context.siblings.len(), 0);
2438
2439 assert_eq!(context.children.len(), 0);
2441 }
2442
2443 #[tokio::test]
2444 async fn test_get_task_context_complex_family_tree() {
2445 let ctx = TestContext::new().await;
2446 let task_mgr = TaskManager::new(ctx.pool());
2447
2448 let root = task_mgr.add_task("Root", None, None).await.unwrap();
2456 let child1 = task_mgr
2457 .add_task("Child1", None, Some(root.id))
2458 .await
2459 .unwrap();
2460 let child2 = task_mgr
2461 .add_task("Child2", None, Some(root.id))
2462 .await
2463 .unwrap();
2464 let grandchild1 = task_mgr
2465 .add_task("Grandchild1", None, Some(child1.id))
2466 .await
2467 .unwrap();
2468 let grandchild2 = task_mgr
2469 .add_task("Grandchild2", None, Some(child1.id))
2470 .await
2471 .unwrap();
2472
2473 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2475
2476 assert_eq!(context.task.id, grandchild2.id);
2478
2479 assert_eq!(context.ancestors.len(), 2);
2481 assert_eq!(context.ancestors[0].id, child1.id);
2482 assert_eq!(context.ancestors[1].id, root.id);
2483
2484 assert_eq!(context.siblings.len(), 1);
2486 assert_eq!(context.siblings[0].id, grandchild1.id);
2487
2488 assert_eq!(context.children.len(), 0);
2490
2491 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2493 assert_eq!(context_child1.ancestors.len(), 1);
2494 assert_eq!(context_child1.ancestors[0].id, root.id);
2495 assert_eq!(context_child1.siblings.len(), 1);
2496 assert_eq!(context_child1.siblings[0].id, child2.id);
2497 assert_eq!(context_child1.children.len(), 2);
2498 }
2499
2500 #[tokio::test]
2501 async fn test_get_task_context_respects_priority_ordering() {
2502 let ctx = TestContext::new().await;
2503 let task_mgr = TaskManager::new(ctx.pool());
2504
2505 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2507
2508 let child_low = task_mgr
2510 .add_task("Low priority", None, Some(parent.id))
2511 .await
2512 .unwrap();
2513 let _ = task_mgr
2514 .update_task(child_low.id, None, None, None, None, None, Some(10))
2515 .await
2516 .unwrap();
2517
2518 let child_high = task_mgr
2519 .add_task("High priority", None, Some(parent.id))
2520 .await
2521 .unwrap();
2522 let _ = task_mgr
2523 .update_task(child_high.id, None, None, None, None, None, Some(1))
2524 .await
2525 .unwrap();
2526
2527 let child_medium = task_mgr
2528 .add_task("Medium priority", None, Some(parent.id))
2529 .await
2530 .unwrap();
2531 let _ = task_mgr
2532 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2533 .await
2534 .unwrap();
2535
2536 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2537
2538 assert_eq!(context.children.len(), 3);
2540 assert_eq!(context.children[0].priority, Some(1));
2541 assert_eq!(context.children[1].priority, Some(5));
2542 assert_eq!(context.children[2].priority, Some(10));
2543 }
2544
2545 #[tokio::test]
2546 async fn test_get_task_context_nonexistent_task() {
2547 let ctx = TestContext::new().await;
2548 let task_mgr = TaskManager::new(ctx.pool());
2549
2550 let result = task_mgr.get_task_context(99999).await;
2551 assert!(result.is_err());
2552 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2553 }
2554
2555 #[tokio::test]
2556 async fn test_get_task_context_handles_null_priority() {
2557 let ctx = TestContext::new().await;
2558 let task_mgr = TaskManager::new(ctx.pool());
2559
2560 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2562 let _ = task_mgr
2563 .update_task(task1.id, None, None, None, None, None, Some(1))
2564 .await
2565 .unwrap();
2566
2567 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2568 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2571 let _ = task_mgr
2572 .update_task(task3.id, None, None, None, None, None, Some(5))
2573 .await
2574 .unwrap();
2575
2576 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2577
2578 assert_eq!(context.siblings.len(), 2);
2580 assert_eq!(context.siblings[0].id, task1.id);
2582 assert_eq!(context.siblings[0].priority, Some(1));
2583 assert_eq!(context.siblings[1].id, task3.id);
2585 assert_eq!(context.siblings[1].priority, Some(5));
2586 }
2587}