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