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