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) ASC,
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) ASC,
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.as_ref() {
1140 if let Ok(current_id) = current_id_str.parse::<i64>() {
1141 let doing_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 = 'doing'
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) = doing_subtasks {
1164 return Ok(PickNextResponse::focused_subtask(task));
1165 }
1166
1167 let todo_subtasks = sqlx::query_as::<_, Task>(
1169 r#"
1170 SELECT id, parent_id, name, spec, status, complexity, priority,
1171 first_todo_at, first_doing_at, first_done_at, active_form
1172 FROM tasks
1173 WHERE parent_id = ? AND status = 'todo'
1174 AND NOT EXISTS (
1175 SELECT 1 FROM dependencies d
1176 JOIN tasks bt ON d.blocking_task_id = bt.id
1177 WHERE d.blocked_task_id = tasks.id
1178 AND bt.status != 'done'
1179 )
1180 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1181 LIMIT 1
1182 "#,
1183 )
1184 .bind(current_id)
1185 .fetch_optional(self.pool)
1186 .await?;
1187
1188 if let Some(task) = todo_subtasks {
1189 return Ok(PickNextResponse::focused_subtask(task));
1190 }
1191 }
1192 }
1193
1194 let doing_top_level = if let Some(current_id_str) = current_task_id.as_ref() {
1197 if let Ok(current_id) = current_id_str.parse::<i64>() {
1198 sqlx::query_as::<_, Task>(
1199 r#"
1200 SELECT id, parent_id, name, spec, status, complexity, priority,
1201 first_todo_at, first_doing_at, first_done_at, active_form
1202 FROM tasks
1203 WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1204 AND NOT EXISTS (
1205 SELECT 1 FROM dependencies d
1206 JOIN tasks bt ON d.blocking_task_id = bt.id
1207 WHERE d.blocked_task_id = tasks.id
1208 AND bt.status != 'done'
1209 )
1210 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1211 LIMIT 1
1212 "#,
1213 )
1214 .bind(current_id)
1215 .fetch_optional(self.pool)
1216 .await?
1217 } else {
1218 None
1219 }
1220 } else {
1221 sqlx::query_as::<_, Task>(
1222 r#"
1223 SELECT id, parent_id, name, spec, status, complexity, priority,
1224 first_todo_at, first_doing_at, first_done_at, active_form
1225 FROM tasks
1226 WHERE parent_id IS NULL AND status = 'doing'
1227 AND NOT EXISTS (
1228 SELECT 1 FROM dependencies d
1229 JOIN tasks bt ON d.blocking_task_id = bt.id
1230 WHERE d.blocked_task_id = tasks.id
1231 AND bt.status != 'done'
1232 )
1233 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1234 LIMIT 1
1235 "#,
1236 )
1237 .fetch_optional(self.pool)
1238 .await?
1239 };
1240
1241 if let Some(task) = doing_top_level {
1242 return Ok(PickNextResponse::top_level_task(task));
1243 }
1244
1245 let todo_top_level = sqlx::query_as::<_, Task>(
1248 r#"
1249 SELECT id, parent_id, name, spec, status, complexity, priority,
1250 first_todo_at, first_doing_at, first_done_at, active_form
1251 FROM tasks
1252 WHERE parent_id IS NULL AND status = 'todo'
1253 AND NOT EXISTS (
1254 SELECT 1 FROM dependencies d
1255 JOIN tasks bt ON d.blocking_task_id = bt.id
1256 WHERE d.blocked_task_id = tasks.id
1257 AND bt.status != 'done'
1258 )
1259 ORDER BY COALESCE(priority, 999999) ASC, id ASC
1260 LIMIT 1
1261 "#,
1262 )
1263 .fetch_optional(self.pool)
1264 .await?;
1265
1266 if let Some(task) = todo_top_level {
1267 return Ok(PickNextResponse::top_level_task(task));
1268 }
1269
1270 let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
1273 .fetch_one(self.pool)
1274 .await?;
1275
1276 if total_tasks == 0 {
1277 return Ok(PickNextResponse::no_tasks_in_project());
1278 }
1279
1280 let todo_or_doing_count: i64 =
1282 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1283 .fetch_one(self.pool)
1284 .await?;
1285
1286 if todo_or_doing_count == 0 {
1287 return Ok(PickNextResponse::all_tasks_completed());
1288 }
1289
1290 Ok(PickNextResponse::no_available_todos())
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::*;
1298 use crate::events::EventManager;
1299 use crate::test_utils::test_helpers::TestContext;
1300 use crate::workspace::WorkspaceManager;
1301
1302 #[tokio::test]
1303 async fn test_add_task() {
1304 let ctx = TestContext::new().await;
1305 let manager = TaskManager::new(ctx.pool());
1306
1307 let task = manager.add_task("Test task", None, None).await.unwrap();
1308
1309 assert_eq!(task.name, "Test task");
1310 assert_eq!(task.status, "todo");
1311 assert!(task.first_todo_at.is_some());
1312 assert!(task.first_doing_at.is_none());
1313 assert!(task.first_done_at.is_none());
1314 }
1315
1316 #[tokio::test]
1317 async fn test_add_task_with_spec() {
1318 let ctx = TestContext::new().await;
1319 let manager = TaskManager::new(ctx.pool());
1320
1321 let spec = "This is a task specification";
1322 let task = manager
1323 .add_task("Test task", Some(spec), None)
1324 .await
1325 .unwrap();
1326
1327 assert_eq!(task.name, "Test task");
1328 assert_eq!(task.spec.as_deref(), Some(spec));
1329 }
1330
1331 #[tokio::test]
1332 async fn test_add_task_with_parent() {
1333 let ctx = TestContext::new().await;
1334 let manager = TaskManager::new(ctx.pool());
1335
1336 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1337 let child = manager
1338 .add_task("Child task", None, Some(parent.id))
1339 .await
1340 .unwrap();
1341
1342 assert_eq!(child.parent_id, Some(parent.id));
1343 }
1344
1345 #[tokio::test]
1346 async fn test_get_task() {
1347 let ctx = TestContext::new().await;
1348 let manager = TaskManager::new(ctx.pool());
1349
1350 let created = manager.add_task("Test task", None, None).await.unwrap();
1351 let retrieved = manager.get_task(created.id).await.unwrap();
1352
1353 assert_eq!(created.id, retrieved.id);
1354 assert_eq!(created.name, retrieved.name);
1355 }
1356
1357 #[tokio::test]
1358 async fn test_get_task_not_found() {
1359 let ctx = TestContext::new().await;
1360 let manager = TaskManager::new(ctx.pool());
1361
1362 let result = manager.get_task(999).await;
1363 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1364 }
1365
1366 #[tokio::test]
1367 async fn test_update_task_name() {
1368 let ctx = TestContext::new().await;
1369 let manager = TaskManager::new(ctx.pool());
1370
1371 let task = manager.add_task("Original name", None, None).await.unwrap();
1372 let updated = manager
1373 .update_task(task.id, Some("New name"), None, None, None, None, None)
1374 .await
1375 .unwrap();
1376
1377 assert_eq!(updated.name, "New name");
1378 }
1379
1380 #[tokio::test]
1381 async fn test_update_task_status() {
1382 let ctx = TestContext::new().await;
1383 let manager = TaskManager::new(ctx.pool());
1384
1385 let task = manager.add_task("Test task", None, None).await.unwrap();
1386 let updated = manager
1387 .update_task(task.id, None, None, None, Some("doing"), None, None)
1388 .await
1389 .unwrap();
1390
1391 assert_eq!(updated.status, "doing");
1392 assert!(updated.first_doing_at.is_some());
1393 }
1394
1395 #[tokio::test]
1396 async fn test_delete_task() {
1397 let ctx = TestContext::new().await;
1398 let manager = TaskManager::new(ctx.pool());
1399
1400 let task = manager.add_task("Test task", None, None).await.unwrap();
1401 manager.delete_task(task.id).await.unwrap();
1402
1403 let result = manager.get_task(task.id).await;
1404 assert!(result.is_err());
1405 }
1406
1407 #[tokio::test]
1408 async fn test_find_tasks_by_status() {
1409 let ctx = TestContext::new().await;
1410 let manager = TaskManager::new(ctx.pool());
1411
1412 manager.add_task("Todo task", None, None).await.unwrap();
1413 let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1414 manager
1415 .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1416 .await
1417 .unwrap();
1418
1419 let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1420 let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
1421
1422 assert_eq!(todo_tasks.len(), 1);
1423 assert_eq!(doing_tasks.len(), 1);
1424 assert_eq!(doing_tasks[0].status, "doing");
1425 }
1426
1427 #[tokio::test]
1428 async fn test_find_tasks_by_parent() {
1429 let ctx = TestContext::new().await;
1430 let manager = TaskManager::new(ctx.pool());
1431
1432 let parent = manager.add_task("Parent", None, None).await.unwrap();
1433 manager
1434 .add_task("Child 1", None, Some(parent.id))
1435 .await
1436 .unwrap();
1437 manager
1438 .add_task("Child 2", None, Some(parent.id))
1439 .await
1440 .unwrap();
1441
1442 let children = manager
1443 .find_tasks(None, Some(Some(parent.id)))
1444 .await
1445 .unwrap();
1446
1447 assert_eq!(children.len(), 2);
1448 }
1449
1450 #[tokio::test]
1451 async fn test_start_task() {
1452 let ctx = TestContext::new().await;
1453 let manager = TaskManager::new(ctx.pool());
1454
1455 let task = manager.add_task("Test task", None, None).await.unwrap();
1456 let started = manager.start_task(task.id, false).await.unwrap();
1457
1458 assert_eq!(started.task.status, "doing");
1459 assert!(started.task.first_doing_at.is_some());
1460
1461 let current: Option<String> =
1463 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1464 .fetch_optional(ctx.pool())
1465 .await
1466 .unwrap();
1467
1468 assert_eq!(current, Some(task.id.to_string()));
1469 }
1470
1471 #[tokio::test]
1472 async fn test_start_task_with_events() {
1473 let ctx = TestContext::new().await;
1474 let manager = TaskManager::new(ctx.pool());
1475
1476 let task = manager.add_task("Test task", None, None).await.unwrap();
1477
1478 sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1480 .bind(task.id)
1481 .bind("test")
1482 .bind("test event")
1483 .execute(ctx.pool())
1484 .await
1485 .unwrap();
1486
1487 let started = manager.start_task(task.id, true).await.unwrap();
1488
1489 assert!(started.events_summary.is_some());
1490 let summary = started.events_summary.unwrap();
1491 assert_eq!(summary.total_count, 1);
1492 }
1493
1494 #[tokio::test]
1495 async fn test_done_task() {
1496 let ctx = TestContext::new().await;
1497 let manager = TaskManager::new(ctx.pool());
1498
1499 let task = manager.add_task("Test task", None, None).await.unwrap();
1500 manager.start_task(task.id, false).await.unwrap();
1501 let response = manager.done_task().await.unwrap();
1502
1503 assert_eq!(response.completed_task.status, "done");
1504 assert!(response.completed_task.first_done_at.is_some());
1505 assert_eq!(response.workspace_status.current_task_id, None);
1506
1507 match response.next_step_suggestion {
1509 NextStepSuggestion::WorkspaceIsClear { .. } => {},
1510 _ => panic!("Expected WorkspaceIsClear suggestion"),
1511 }
1512
1513 let current: Option<String> =
1515 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1516 .fetch_optional(ctx.pool())
1517 .await
1518 .unwrap();
1519
1520 assert!(current.is_none());
1521 }
1522
1523 #[tokio::test]
1524 async fn test_done_task_with_uncompleted_children() {
1525 let ctx = TestContext::new().await;
1526 let manager = TaskManager::new(ctx.pool());
1527
1528 let parent = manager.add_task("Parent", None, None).await.unwrap();
1529 manager
1530 .add_task("Child", None, Some(parent.id))
1531 .await
1532 .unwrap();
1533
1534 manager.start_task(parent.id, false).await.unwrap();
1536
1537 let result = manager.done_task().await;
1538 assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1539 }
1540
1541 #[tokio::test]
1542 async fn test_done_task_with_completed_children() {
1543 let ctx = TestContext::new().await;
1544 let manager = TaskManager::new(ctx.pool());
1545
1546 let parent = manager.add_task("Parent", None, None).await.unwrap();
1547 let child = manager
1548 .add_task("Child", None, Some(parent.id))
1549 .await
1550 .unwrap();
1551
1552 manager.start_task(child.id, false).await.unwrap();
1554 let child_response = manager.done_task().await.unwrap();
1555
1556 match child_response.next_step_suggestion {
1558 NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1559 assert_eq!(parent_task_id, parent.id);
1560 },
1561 _ => panic!("Expected ParentIsReady suggestion"),
1562 }
1563
1564 manager.start_task(parent.id, false).await.unwrap();
1566 let parent_response = manager.done_task().await.unwrap();
1567 assert_eq!(parent_response.completed_task.status, "done");
1568
1569 match parent_response.next_step_suggestion {
1571 NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1572 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1573 }
1574 }
1575
1576 #[tokio::test]
1577 async fn test_circular_dependency() {
1578 let ctx = TestContext::new().await;
1579 let manager = TaskManager::new(ctx.pool());
1580
1581 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1582 let task2 = manager
1583 .add_task("Task 2", None, Some(task1.id))
1584 .await
1585 .unwrap();
1586
1587 let result = manager
1589 .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1590 .await;
1591
1592 assert!(matches!(
1593 result,
1594 Err(IntentError::CircularDependency { .. })
1595 ));
1596 }
1597
1598 #[tokio::test]
1599 async fn test_invalid_parent_id() {
1600 let ctx = TestContext::new().await;
1601 let manager = TaskManager::new(ctx.pool());
1602
1603 let result = manager.add_task("Test", None, Some(999)).await;
1604 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1605 }
1606
1607 #[tokio::test]
1608 async fn test_update_task_complexity_and_priority() {
1609 let ctx = TestContext::new().await;
1610 let manager = TaskManager::new(ctx.pool());
1611
1612 let task = manager.add_task("Test task", None, None).await.unwrap();
1613 let updated = manager
1614 .update_task(task.id, None, None, None, None, Some(8), Some(10))
1615 .await
1616 .unwrap();
1617
1618 assert_eq!(updated.complexity, Some(8));
1619 assert_eq!(updated.priority, Some(10));
1620 }
1621
1622 #[tokio::test]
1623 async fn test_spawn_subtask() {
1624 let ctx = TestContext::new().await;
1625 let manager = TaskManager::new(ctx.pool());
1626
1627 let parent = manager.add_task("Parent task", None, None).await.unwrap();
1629 manager.start_task(parent.id, false).await.unwrap();
1630
1631 let response = manager
1633 .spawn_subtask("Child task", Some("Details"))
1634 .await
1635 .unwrap();
1636
1637 assert_eq!(response.subtask.parent_id, parent.id);
1638 assert_eq!(response.subtask.name, "Child task");
1639 assert_eq!(response.subtask.status, "doing");
1640 assert_eq!(response.parent_task.id, parent.id);
1641 assert_eq!(response.parent_task.name, "Parent task");
1642
1643 let current: Option<String> =
1645 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1646 .fetch_optional(ctx.pool())
1647 .await
1648 .unwrap();
1649
1650 assert_eq!(current, Some(response.subtask.id.to_string()));
1651
1652 let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1654 assert_eq!(retrieved.status, "doing");
1655 }
1656
1657 #[tokio::test]
1658 async fn test_spawn_subtask_no_current_task() {
1659 let ctx = TestContext::new().await;
1660 let manager = TaskManager::new(ctx.pool());
1661
1662 let result = manager.spawn_subtask("Child", None).await;
1664 assert!(result.is_err());
1665 }
1666
1667 #[tokio::test]
1668 async fn test_pick_next_tasks_basic() {
1669 let ctx = TestContext::new().await;
1670 let manager = TaskManager::new(ctx.pool());
1671
1672 for i in 1..=10 {
1674 manager
1675 .add_task(&format!("Task {}", i), None, None)
1676 .await
1677 .unwrap();
1678 }
1679
1680 let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1682
1683 assert_eq!(picked.len(), 5);
1684 for task in &picked {
1685 assert_eq!(task.status, "doing");
1686 assert!(task.first_doing_at.is_some());
1687 }
1688
1689 let doing_count: i64 =
1691 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1692 .fetch_one(ctx.pool())
1693 .await
1694 .unwrap();
1695
1696 assert_eq!(doing_count, 5);
1697 }
1698
1699 #[tokio::test]
1700 async fn test_pick_next_tasks_with_existing_doing() {
1701 let ctx = TestContext::new().await;
1702 let manager = TaskManager::new(ctx.pool());
1703
1704 for i in 1..=10 {
1706 manager
1707 .add_task(&format!("Task {}", i), None, None)
1708 .await
1709 .unwrap();
1710 }
1711
1712 let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1714 manager.start_task(tasks[0].id, false).await.unwrap();
1715 manager.start_task(tasks[1].id, false).await.unwrap();
1716
1717 let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1719
1720 assert_eq!(picked.len(), 3);
1722
1723 let doing_count: i64 =
1725 sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1726 .fetch_one(ctx.pool())
1727 .await
1728 .unwrap();
1729
1730 assert_eq!(doing_count, 5);
1731 }
1732
1733 #[tokio::test]
1734 async fn test_pick_next_tasks_at_capacity() {
1735 let ctx = TestContext::new().await;
1736 let manager = TaskManager::new(ctx.pool());
1737
1738 for i in 1..=10 {
1740 manager
1741 .add_task(&format!("Task {}", i), None, None)
1742 .await
1743 .unwrap();
1744 }
1745
1746 let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1748 assert_eq!(first_batch.len(), 5);
1749
1750 let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1752 assert_eq!(second_batch.len(), 0);
1753 }
1754
1755 #[tokio::test]
1756 async fn test_pick_next_tasks_priority_ordering() {
1757 let ctx = TestContext::new().await;
1758 let manager = TaskManager::new(ctx.pool());
1759
1760 let low = manager.add_task("Low priority", None, None).await.unwrap();
1762 manager
1763 .update_task(low.id, None, None, None, None, None, Some(1))
1764 .await
1765 .unwrap();
1766
1767 let high = manager.add_task("High priority", None, None).await.unwrap();
1768 manager
1769 .update_task(high.id, None, None, None, None, None, Some(10))
1770 .await
1771 .unwrap();
1772
1773 let medium = manager
1774 .add_task("Medium priority", None, None)
1775 .await
1776 .unwrap();
1777 manager
1778 .update_task(medium.id, None, None, None, None, None, Some(5))
1779 .await
1780 .unwrap();
1781
1782 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1784
1785 assert_eq!(picked.len(), 3);
1787 assert_eq!(picked[0].priority, Some(1)); assert_eq!(picked[1].priority, Some(5)); assert_eq!(picked[2].priority, Some(10)); }
1791
1792 #[tokio::test]
1793 async fn test_pick_next_tasks_complexity_ordering() {
1794 let ctx = TestContext::new().await;
1795 let manager = TaskManager::new(ctx.pool());
1796
1797 let complex = manager.add_task("Complex", None, None).await.unwrap();
1799 manager
1800 .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1801 .await
1802 .unwrap();
1803
1804 let simple = manager.add_task("Simple", None, None).await.unwrap();
1805 manager
1806 .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1807 .await
1808 .unwrap();
1809
1810 let medium = manager.add_task("Medium", None, None).await.unwrap();
1811 manager
1812 .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1813 .await
1814 .unwrap();
1815
1816 let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1818
1819 assert_eq!(picked.len(), 3);
1821 assert_eq!(picked[0].complexity, Some(1)); assert_eq!(picked[1].complexity, Some(5)); assert_eq!(picked[2].complexity, Some(9)); }
1825
1826 #[tokio::test]
1827 async fn test_done_task_sibling_tasks_remain() {
1828 let ctx = TestContext::new().await;
1829 let manager = TaskManager::new(ctx.pool());
1830
1831 let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1833 let child1 = manager
1834 .add_task("Child 1", None, Some(parent.id))
1835 .await
1836 .unwrap();
1837 let child2 = manager
1838 .add_task("Child 2", None, Some(parent.id))
1839 .await
1840 .unwrap();
1841 let _child3 = manager
1842 .add_task("Child 3", None, Some(parent.id))
1843 .await
1844 .unwrap();
1845
1846 manager.start_task(child1.id, false).await.unwrap();
1848 let response = manager.done_task().await.unwrap();
1849
1850 match response.next_step_suggestion {
1852 NextStepSuggestion::SiblingTasksRemain {
1853 parent_task_id,
1854 remaining_siblings_count,
1855 ..
1856 } => {
1857 assert_eq!(parent_task_id, parent.id);
1858 assert_eq!(remaining_siblings_count, 2); },
1860 _ => panic!("Expected SiblingTasksRemain suggestion"),
1861 }
1862
1863 manager.start_task(child2.id, false).await.unwrap();
1865 let response2 = manager.done_task().await.unwrap();
1866
1867 match response2.next_step_suggestion {
1869 NextStepSuggestion::SiblingTasksRemain {
1870 remaining_siblings_count,
1871 ..
1872 } => {
1873 assert_eq!(remaining_siblings_count, 1); },
1875 _ => panic!("Expected SiblingTasksRemain suggestion"),
1876 }
1877 }
1878
1879 #[tokio::test]
1880 async fn test_done_task_top_level_with_children() {
1881 let ctx = TestContext::new().await;
1882 let manager = TaskManager::new(ctx.pool());
1883
1884 let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1886 let child = manager
1887 .add_task("Sub Task", None, Some(parent.id))
1888 .await
1889 .unwrap();
1890
1891 manager.start_task(child.id, false).await.unwrap();
1893 manager.done_task().await.unwrap();
1894
1895 manager.start_task(parent.id, false).await.unwrap();
1897 let response = manager.done_task().await.unwrap();
1898
1899 match response.next_step_suggestion {
1901 NextStepSuggestion::TopLevelTaskCompleted {
1902 completed_task_id,
1903 completed_task_name,
1904 ..
1905 } => {
1906 assert_eq!(completed_task_id, parent.id);
1907 assert_eq!(completed_task_name, "Epic Task");
1908 },
1909 _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1910 }
1911 }
1912
1913 #[tokio::test]
1914 async fn test_done_task_no_parent_context() {
1915 let ctx = TestContext::new().await;
1916 let manager = TaskManager::new(ctx.pool());
1917
1918 let task1 = manager
1920 .add_task("Standalone Task 1", None, None)
1921 .await
1922 .unwrap();
1923 let _task2 = manager
1924 .add_task("Standalone Task 2", None, None)
1925 .await
1926 .unwrap();
1927
1928 manager.start_task(task1.id, false).await.unwrap();
1930 let response = manager.done_task().await.unwrap();
1931
1932 match response.next_step_suggestion {
1934 NextStepSuggestion::NoParentContext {
1935 completed_task_id,
1936 completed_task_name,
1937 ..
1938 } => {
1939 assert_eq!(completed_task_id, task1.id);
1940 assert_eq!(completed_task_name, "Standalone Task 1");
1941 },
1942 _ => panic!("Expected NoParentContext suggestion"),
1943 }
1944 }
1945
1946 #[tokio::test]
1947 async fn test_search_tasks_by_name() {
1948 let ctx = TestContext::new().await;
1949 let manager = TaskManager::new(ctx.pool());
1950
1951 manager
1953 .add_task("Authentication bug fix", Some("Fix login issue"), None)
1954 .await
1955 .unwrap();
1956 manager
1957 .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1958 .await
1959 .unwrap();
1960 manager
1961 .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1962 .await
1963 .unwrap();
1964
1965 let results = manager.search_tasks("authentication").await.unwrap();
1967
1968 assert_eq!(results.len(), 2);
1969 assert!(results[0]
1970 .task
1971 .name
1972 .to_lowercase()
1973 .contains("authentication"));
1974 assert!(results[1]
1975 .task
1976 .name
1977 .to_lowercase()
1978 .contains("authentication"));
1979
1980 assert!(!results[0].match_snippet.is_empty());
1982 }
1983
1984 #[tokio::test]
1985 async fn test_search_tasks_by_spec() {
1986 let ctx = TestContext::new().await;
1987 let manager = TaskManager::new(ctx.pool());
1988
1989 manager
1991 .add_task("Task 1", Some("Implement JWT authentication"), None)
1992 .await
1993 .unwrap();
1994 manager
1995 .add_task("Task 2", Some("Add user registration"), None)
1996 .await
1997 .unwrap();
1998 manager
1999 .add_task("Task 3", Some("JWT token refresh"), None)
2000 .await
2001 .unwrap();
2002
2003 let results = manager.search_tasks("JWT").await.unwrap();
2005
2006 assert_eq!(results.len(), 2);
2007 for result in &results {
2008 assert!(result
2009 .task
2010 .spec
2011 .as_ref()
2012 .unwrap()
2013 .to_uppercase()
2014 .contains("JWT"));
2015 }
2016 }
2017
2018 #[tokio::test]
2019 async fn test_search_tasks_with_advanced_query() {
2020 let ctx = TestContext::new().await;
2021 let manager = TaskManager::new(ctx.pool());
2022
2023 manager
2025 .add_task("Bug fix", Some("Fix critical authentication bug"), None)
2026 .await
2027 .unwrap();
2028 manager
2029 .add_task("Feature", Some("Add authentication feature"), None)
2030 .await
2031 .unwrap();
2032 manager
2033 .add_task("Bug report", Some("Report critical database bug"), None)
2034 .await
2035 .unwrap();
2036
2037 let results = manager
2039 .search_tasks("authentication AND bug")
2040 .await
2041 .unwrap();
2042
2043 assert_eq!(results.len(), 1);
2044 assert!(results[0]
2045 .task
2046 .spec
2047 .as_ref()
2048 .unwrap()
2049 .contains("authentication"));
2050 assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
2051 }
2052
2053 #[tokio::test]
2054 async fn test_search_tasks_no_results() {
2055 let ctx = TestContext::new().await;
2056 let manager = TaskManager::new(ctx.pool());
2057
2058 manager
2060 .add_task("Task 1", Some("Some description"), None)
2061 .await
2062 .unwrap();
2063
2064 let results = manager.search_tasks("nonexistent").await.unwrap();
2066
2067 assert_eq!(results.len(), 0);
2068 }
2069
2070 #[tokio::test]
2071 async fn test_search_tasks_snippet_highlighting() {
2072 let ctx = TestContext::new().await;
2073 let manager = TaskManager::new(ctx.pool());
2074
2075 manager
2077 .add_task(
2078 "Test task",
2079 Some("This is a description with the keyword authentication in the middle"),
2080 None,
2081 )
2082 .await
2083 .unwrap();
2084
2085 let results = manager.search_tasks("authentication").await.unwrap();
2087
2088 assert_eq!(results.len(), 1);
2089 assert!(results[0].match_snippet.contains("**authentication**"));
2091 }
2092
2093 #[tokio::test]
2094 async fn test_pick_next_focused_subtask() {
2095 let ctx = TestContext::new().await;
2096 let manager = TaskManager::new(ctx.pool());
2097
2098 let parent = manager.add_task("Parent task", None, None).await.unwrap();
2100 manager.start_task(parent.id, false).await.unwrap();
2101
2102 let subtask1 = manager
2104 .add_task("Subtask 1", None, Some(parent.id))
2105 .await
2106 .unwrap();
2107 let subtask2 = manager
2108 .add_task("Subtask 2", None, Some(parent.id))
2109 .await
2110 .unwrap();
2111
2112 manager
2114 .update_task(subtask1.id, None, None, None, None, None, Some(2))
2115 .await
2116 .unwrap();
2117 manager
2118 .update_task(subtask2.id, None, None, None, None, None, Some(1))
2119 .await
2120 .unwrap();
2121
2122 let response = manager.pick_next().await.unwrap();
2124
2125 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2126 assert!(response.task.is_some());
2127 assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2128 assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2129 }
2130
2131 #[tokio::test]
2132 async fn test_pick_next_top_level_task() {
2133 let ctx = TestContext::new().await;
2134 let manager = TaskManager::new(ctx.pool());
2135
2136 let task1 = manager.add_task("Task 1", None, None).await.unwrap();
2138 let task2 = manager.add_task("Task 2", None, None).await.unwrap();
2139
2140 manager
2142 .update_task(task1.id, None, None, None, None, None, Some(5))
2143 .await
2144 .unwrap();
2145 manager
2146 .update_task(task2.id, None, None, None, None, None, Some(3))
2147 .await
2148 .unwrap();
2149
2150 let response = manager.pick_next().await.unwrap();
2152
2153 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2154 assert!(response.task.is_some());
2155 assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2156 assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2157 }
2158
2159 #[tokio::test]
2160 async fn test_pick_next_no_tasks() {
2161 let ctx = TestContext::new().await;
2162 let manager = TaskManager::new(ctx.pool());
2163
2164 let response = manager.pick_next().await.unwrap();
2166
2167 assert_eq!(response.suggestion_type, "NONE");
2168 assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2169 assert!(response.message.is_some());
2170 }
2171
2172 #[tokio::test]
2173 async fn test_pick_next_all_completed() {
2174 let ctx = TestContext::new().await;
2175 let manager = TaskManager::new(ctx.pool());
2176
2177 let task = manager.add_task("Task 1", None, None).await.unwrap();
2179 manager.start_task(task.id, false).await.unwrap();
2180 manager.done_task().await.unwrap();
2181
2182 let response = manager.pick_next().await.unwrap();
2184
2185 assert_eq!(response.suggestion_type, "NONE");
2186 assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2187 assert!(response.message.is_some());
2188 }
2189
2190 #[tokio::test]
2191 async fn test_pick_next_no_available_todos() {
2192 let ctx = TestContext::new().await;
2193 let manager = TaskManager::new(ctx.pool());
2194
2195 let parent = manager.add_task("Parent task", None, None).await.unwrap();
2197 manager.start_task(parent.id, false).await.unwrap();
2198
2199 let subtask = manager
2201 .add_task("Subtask", None, Some(parent.id))
2202 .await
2203 .unwrap();
2204 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2206 .bind(subtask.id)
2207 .execute(ctx.pool())
2208 .await
2209 .unwrap();
2210
2211 sqlx::query(
2213 "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
2214 )
2215 .bind(subtask.id.to_string())
2216 .execute(ctx.pool())
2217 .await
2218 .unwrap();
2219
2220 sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2222 .bind(parent.id)
2223 .execute(ctx.pool())
2224 .await
2225 .unwrap();
2226
2227 let response = manager.pick_next().await.unwrap();
2230
2231 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2232 assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2233 assert_eq!(response.task.as_ref().unwrap().status, "doing");
2234 }
2235
2236 #[tokio::test]
2237 async fn test_pick_next_priority_ordering() {
2238 let ctx = TestContext::new().await;
2239 let manager = TaskManager::new(ctx.pool());
2240
2241 let parent = manager.add_task("Parent", None, None).await.unwrap();
2243 manager.start_task(parent.id, false).await.unwrap();
2244
2245 let sub1 = manager
2247 .add_task("Priority 10", None, Some(parent.id))
2248 .await
2249 .unwrap();
2250 manager
2251 .update_task(sub1.id, None, None, None, None, None, Some(10))
2252 .await
2253 .unwrap();
2254
2255 let sub2 = manager
2256 .add_task("Priority 1", None, Some(parent.id))
2257 .await
2258 .unwrap();
2259 manager
2260 .update_task(sub2.id, None, None, None, None, None, Some(1))
2261 .await
2262 .unwrap();
2263
2264 let sub3 = manager
2265 .add_task("Priority 5", None, Some(parent.id))
2266 .await
2267 .unwrap();
2268 manager
2269 .update_task(sub3.id, None, None, None, None, None, Some(5))
2270 .await
2271 .unwrap();
2272
2273 let response = manager.pick_next().await.unwrap();
2275
2276 assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2277 assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2278 assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2279 }
2280
2281 #[tokio::test]
2282 async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2283 let ctx = TestContext::new().await;
2284 let manager = TaskManager::new(ctx.pool());
2285
2286 let parent = manager.add_task("Parent", None, None).await.unwrap();
2288 manager.start_task(parent.id, false).await.unwrap();
2289
2290 let top_level = manager
2292 .add_task("Top level task", None, None)
2293 .await
2294 .unwrap();
2295
2296 let response = manager.pick_next().await.unwrap();
2298
2299 assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2300 assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2301 }
2302
2303 #[tokio::test]
2306 async fn test_get_task_with_events() {
2307 let ctx = TestContext::new().await;
2308 let task_mgr = TaskManager::new(ctx.pool());
2309 let event_mgr = EventManager::new(ctx.pool());
2310
2311 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2312
2313 event_mgr
2315 .add_event(task.id, "progress", "Event 1")
2316 .await
2317 .unwrap();
2318 event_mgr
2319 .add_event(task.id, "decision", "Event 2")
2320 .await
2321 .unwrap();
2322
2323 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2324
2325 assert_eq!(result.task.id, task.id);
2326 assert!(result.events_summary.is_some());
2327
2328 let summary = result.events_summary.unwrap();
2329 assert_eq!(summary.total_count, 2);
2330 assert_eq!(summary.recent_events.len(), 2);
2331 assert_eq!(summary.recent_events[0].log_type, "decision"); assert_eq!(summary.recent_events[1].log_type, "progress");
2333 }
2334
2335 #[tokio::test]
2336 async fn test_get_task_with_events_nonexistent() {
2337 let ctx = TestContext::new().await;
2338 let task_mgr = TaskManager::new(ctx.pool());
2339
2340 let result = task_mgr.get_task_with_events(999).await;
2341 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2342 }
2343
2344 #[tokio::test]
2345 async fn test_get_task_with_many_events() {
2346 let ctx = TestContext::new().await;
2347 let task_mgr = TaskManager::new(ctx.pool());
2348 let event_mgr = EventManager::new(ctx.pool());
2349
2350 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2351
2352 for i in 0..20 {
2354 event_mgr
2355 .add_event(task.id, "test", &format!("Event {}", i))
2356 .await
2357 .unwrap();
2358 }
2359
2360 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2361 let summary = result.events_summary.unwrap();
2362
2363 assert_eq!(summary.total_count, 20);
2364 assert_eq!(summary.recent_events.len(), 10); }
2366
2367 #[tokio::test]
2368 async fn test_get_task_with_no_events() {
2369 let ctx = TestContext::new().await;
2370 let task_mgr = TaskManager::new(ctx.pool());
2371
2372 let task = task_mgr.add_task("Test", None, None).await.unwrap();
2373
2374 let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2375 let summary = result.events_summary.unwrap();
2376
2377 assert_eq!(summary.total_count, 0);
2378 assert_eq!(summary.recent_events.len(), 0);
2379 }
2380
2381 #[tokio::test]
2382 async fn test_pick_next_tasks_zero_capacity() {
2383 let ctx = TestContext::new().await;
2384 let task_mgr = TaskManager::new(ctx.pool());
2385
2386 task_mgr.add_task("Task 1", None, None).await.unwrap();
2387
2388 let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2390 assert_eq!(results.len(), 0);
2391 }
2392
2393 #[tokio::test]
2394 async fn test_pick_next_tasks_capacity_exceeds_available() {
2395 let ctx = TestContext::new().await;
2396 let task_mgr = TaskManager::new(ctx.pool());
2397
2398 task_mgr.add_task("Task 1", None, None).await.unwrap();
2399 task_mgr.add_task("Task 2", None, None).await.unwrap();
2400
2401 let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2403 assert_eq!(results.len(), 2); }
2405
2406 #[tokio::test]
2409 async fn test_get_task_context_root_task_no_relations() {
2410 let ctx = TestContext::new().await;
2411 let task_mgr = TaskManager::new(ctx.pool());
2412
2413 let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2415
2416 let context = task_mgr.get_task_context(task.id).await.unwrap();
2417
2418 assert_eq!(context.task.id, task.id);
2420 assert_eq!(context.task.name, "Root task");
2421
2422 assert_eq!(context.ancestors.len(), 0);
2424
2425 assert_eq!(context.siblings.len(), 0);
2427
2428 assert_eq!(context.children.len(), 0);
2430 }
2431
2432 #[tokio::test]
2433 async fn test_get_task_context_with_siblings() {
2434 let ctx = TestContext::new().await;
2435 let task_mgr = TaskManager::new(ctx.pool());
2436
2437 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2439 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2440 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2441
2442 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2443
2444 assert_eq!(context.task.id, task2.id);
2446
2447 assert_eq!(context.ancestors.len(), 0);
2449
2450 assert_eq!(context.siblings.len(), 2);
2452 let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2453 assert!(sibling_ids.contains(&task1.id));
2454 assert!(sibling_ids.contains(&task3.id));
2455 assert!(!sibling_ids.contains(&task2.id)); assert_eq!(context.children.len(), 0);
2459 }
2460
2461 #[tokio::test]
2462 async fn test_get_task_context_with_parent() {
2463 let ctx = TestContext::new().await;
2464 let task_mgr = TaskManager::new(ctx.pool());
2465
2466 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2468 let child = task_mgr
2469 .add_task("Child task", None, Some(parent.id))
2470 .await
2471 .unwrap();
2472
2473 let context = task_mgr.get_task_context(child.id).await.unwrap();
2474
2475 assert_eq!(context.task.id, child.id);
2477 assert_eq!(context.task.parent_id, Some(parent.id));
2478
2479 assert_eq!(context.ancestors.len(), 1);
2481 assert_eq!(context.ancestors[0].id, parent.id);
2482 assert_eq!(context.ancestors[0].name, "Parent task");
2483
2484 assert_eq!(context.siblings.len(), 0);
2486
2487 assert_eq!(context.children.len(), 0);
2489 }
2490
2491 #[tokio::test]
2492 async fn test_get_task_context_with_children() {
2493 let ctx = TestContext::new().await;
2494 let task_mgr = TaskManager::new(ctx.pool());
2495
2496 let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2498 let child1 = task_mgr
2499 .add_task("Child 1", None, Some(parent.id))
2500 .await
2501 .unwrap();
2502 let child2 = task_mgr
2503 .add_task("Child 2", None, Some(parent.id))
2504 .await
2505 .unwrap();
2506 let child3 = task_mgr
2507 .add_task("Child 3", None, Some(parent.id))
2508 .await
2509 .unwrap();
2510
2511 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2512
2513 assert_eq!(context.task.id, parent.id);
2515
2516 assert_eq!(context.ancestors.len(), 0);
2518
2519 assert_eq!(context.siblings.len(), 0);
2521
2522 assert_eq!(context.children.len(), 3);
2524 let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2525 assert!(child_ids.contains(&child1.id));
2526 assert!(child_ids.contains(&child2.id));
2527 assert!(child_ids.contains(&child3.id));
2528 }
2529
2530 #[tokio::test]
2531 async fn test_get_task_context_multi_level_hierarchy() {
2532 let ctx = TestContext::new().await;
2533 let task_mgr = TaskManager::new(ctx.pool());
2534
2535 let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2537 let parent = task_mgr
2538 .add_task("Parent", None, Some(grandparent.id))
2539 .await
2540 .unwrap();
2541 let child = task_mgr
2542 .add_task("Child", None, Some(parent.id))
2543 .await
2544 .unwrap();
2545
2546 let context = task_mgr.get_task_context(child.id).await.unwrap();
2547
2548 assert_eq!(context.task.id, child.id);
2550
2551 assert_eq!(context.ancestors.len(), 2);
2553 assert_eq!(context.ancestors[0].id, parent.id);
2554 assert_eq!(context.ancestors[0].name, "Parent");
2555 assert_eq!(context.ancestors[1].id, grandparent.id);
2556 assert_eq!(context.ancestors[1].name, "Grandparent");
2557
2558 assert_eq!(context.siblings.len(), 0);
2560
2561 assert_eq!(context.children.len(), 0);
2563 }
2564
2565 #[tokio::test]
2566 async fn test_get_task_context_complex_family_tree() {
2567 let ctx = TestContext::new().await;
2568 let task_mgr = TaskManager::new(ctx.pool());
2569
2570 let root = task_mgr.add_task("Root", None, None).await.unwrap();
2578 let child1 = task_mgr
2579 .add_task("Child1", None, Some(root.id))
2580 .await
2581 .unwrap();
2582 let child2 = task_mgr
2583 .add_task("Child2", None, Some(root.id))
2584 .await
2585 .unwrap();
2586 let grandchild1 = task_mgr
2587 .add_task("Grandchild1", None, Some(child1.id))
2588 .await
2589 .unwrap();
2590 let grandchild2 = task_mgr
2591 .add_task("Grandchild2", None, Some(child1.id))
2592 .await
2593 .unwrap();
2594
2595 let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2597
2598 assert_eq!(context.task.id, grandchild2.id);
2600
2601 assert_eq!(context.ancestors.len(), 2);
2603 assert_eq!(context.ancestors[0].id, child1.id);
2604 assert_eq!(context.ancestors[1].id, root.id);
2605
2606 assert_eq!(context.siblings.len(), 1);
2608 assert_eq!(context.siblings[0].id, grandchild1.id);
2609
2610 assert_eq!(context.children.len(), 0);
2612
2613 let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2615 assert_eq!(context_child1.ancestors.len(), 1);
2616 assert_eq!(context_child1.ancestors[0].id, root.id);
2617 assert_eq!(context_child1.siblings.len(), 1);
2618 assert_eq!(context_child1.siblings[0].id, child2.id);
2619 assert_eq!(context_child1.children.len(), 2);
2620 }
2621
2622 #[tokio::test]
2623 async fn test_get_task_context_respects_priority_ordering() {
2624 let ctx = TestContext::new().await;
2625 let task_mgr = TaskManager::new(ctx.pool());
2626
2627 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2629
2630 let child_low = task_mgr
2632 .add_task("Low priority", None, Some(parent.id))
2633 .await
2634 .unwrap();
2635 let _ = task_mgr
2636 .update_task(child_low.id, None, None, None, None, None, Some(10))
2637 .await
2638 .unwrap();
2639
2640 let child_high = task_mgr
2641 .add_task("High priority", None, Some(parent.id))
2642 .await
2643 .unwrap();
2644 let _ = task_mgr
2645 .update_task(child_high.id, None, None, None, None, None, Some(1))
2646 .await
2647 .unwrap();
2648
2649 let child_medium = task_mgr
2650 .add_task("Medium priority", None, Some(parent.id))
2651 .await
2652 .unwrap();
2653 let _ = task_mgr
2654 .update_task(child_medium.id, None, None, None, None, None, Some(5))
2655 .await
2656 .unwrap();
2657
2658 let context = task_mgr.get_task_context(parent.id).await.unwrap();
2659
2660 assert_eq!(context.children.len(), 3);
2662 assert_eq!(context.children[0].priority, Some(1));
2663 assert_eq!(context.children[1].priority, Some(5));
2664 assert_eq!(context.children[2].priority, Some(10));
2665 }
2666
2667 #[tokio::test]
2668 async fn test_get_task_context_nonexistent_task() {
2669 let ctx = TestContext::new().await;
2670 let task_mgr = TaskManager::new(ctx.pool());
2671
2672 let result = task_mgr.get_task_context(99999).await;
2673 assert!(result.is_err());
2674 assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2675 }
2676
2677 #[tokio::test]
2678 async fn test_get_task_context_handles_null_priority() {
2679 let ctx = TestContext::new().await;
2680 let task_mgr = TaskManager::new(ctx.pool());
2681
2682 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2684 let _ = task_mgr
2685 .update_task(task1.id, None, None, None, None, None, Some(1))
2686 .await
2687 .unwrap();
2688
2689 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2690 let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2693 let _ = task_mgr
2694 .update_task(task3.id, None, None, None, None, None, Some(5))
2695 .await
2696 .unwrap();
2697
2698 let context = task_mgr.get_task_context(task2.id).await.unwrap();
2699
2700 assert_eq!(context.siblings.len(), 2);
2702 assert_eq!(context.siblings[0].id, task1.id);
2704 assert_eq!(context.siblings[0].priority, Some(1));
2705 assert_eq!(context.siblings[1].id, task3.id);
2707 assert_eq!(context.siblings[1].priority, Some(5));
2708 }
2709
2710 #[tokio::test]
2711 async fn test_pick_next_tasks_priority_order() {
2712 let ctx = TestContext::new().await;
2713 let task_mgr = TaskManager::new(ctx.pool());
2714
2715 let critical = task_mgr
2717 .add_task("Critical Task", None, None)
2718 .await
2719 .unwrap();
2720 task_mgr
2721 .update_task(critical.id, None, None, None, None, None, Some(1))
2722 .await
2723 .unwrap();
2724
2725 let low = task_mgr.add_task("Low Task", None, None).await.unwrap();
2726 task_mgr
2727 .update_task(low.id, None, None, None, None, None, Some(4))
2728 .await
2729 .unwrap();
2730
2731 let high = task_mgr.add_task("High Task", None, None).await.unwrap();
2732 task_mgr
2733 .update_task(high.id, None, None, None, None, None, Some(2))
2734 .await
2735 .unwrap();
2736
2737 let medium = task_mgr.add_task("Medium Task", None, None).await.unwrap();
2738 task_mgr
2739 .update_task(medium.id, None, None, None, None, None, Some(3))
2740 .await
2741 .unwrap();
2742
2743 let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2745
2746 assert_eq!(tasks.len(), 4);
2747 assert_eq!(tasks[0].id, critical.id); assert_eq!(tasks[1].id, high.id); assert_eq!(tasks[2].id, medium.id); assert_eq!(tasks[3].id, low.id); }
2752
2753 #[tokio::test]
2754 async fn test_pick_next_prefers_doing_over_todo() {
2755 let ctx = TestContext::new().await;
2756 let task_mgr = TaskManager::new(ctx.pool());
2757 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2758
2759 let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2761 let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
2762 workspace_mgr
2763 .set_current_task(parent_started.task.id)
2764 .await
2765 .unwrap();
2766
2767 let doing_subtask = task_mgr
2769 .add_task("Doing Subtask", None, Some(parent.id))
2770 .await
2771 .unwrap();
2772 task_mgr.start_task(doing_subtask.id, false).await.unwrap();
2773 workspace_mgr.set_current_task(parent.id).await.unwrap();
2775
2776 let _todo_subtask = task_mgr
2777 .add_task("Todo Subtask", None, Some(parent.id))
2778 .await
2779 .unwrap();
2780
2781 let result = task_mgr.pick_next().await.unwrap();
2783
2784 if let Some(task) = result.task {
2785 assert_eq!(
2786 task.id, doing_subtask.id,
2787 "Should recommend doing subtask over todo subtask"
2788 );
2789 assert_eq!(task.status, "doing");
2790 } else {
2791 panic!("Expected a task recommendation");
2792 }
2793 }
2794
2795 #[tokio::test]
2796 async fn test_multiple_doing_tasks_allowed() {
2797 let ctx = TestContext::new().await;
2798 let task_mgr = TaskManager::new(ctx.pool());
2799 let workspace_mgr = WorkspaceManager::new(ctx.pool());
2800
2801 let task_a = task_mgr.add_task("Task A", None, None).await.unwrap();
2803 let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
2804 assert_eq!(task_a_started.task.status, "doing");
2805
2806 let current = workspace_mgr.get_current_task().await.unwrap();
2808 assert_eq!(current.current_task_id, Some(task_a.id));
2809
2810 let task_b = task_mgr.add_task("Task B", None, None).await.unwrap();
2812 let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
2813 assert_eq!(task_b_started.task.status, "doing");
2814
2815 let current = workspace_mgr.get_current_task().await.unwrap();
2817 assert_eq!(current.current_task_id, Some(task_b.id));
2818
2819 let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
2821 assert_eq!(
2822 task_a_after.status, "doing",
2823 "Task A should remain doing even though it is not current"
2824 );
2825
2826 let doing_tasks: Vec<Task> = sqlx::query_as(
2828 r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
2829 FROM tasks WHERE status = 'doing' ORDER BY id"#
2830 )
2831 .fetch_all(ctx.pool())
2832 .await
2833 .unwrap();
2834
2835 assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
2836 assert_eq!(doing_tasks[0].id, task_a.id);
2837 assert_eq!(doing_tasks[1].id, task_b.id);
2838 }
2839}