1use super::{Tool, ToolContext, ToolError, ToolResult};
4use async_trait::async_trait;
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum TaskStatus {
17 Pending,
18 InProgress,
19 Completed,
20 Cancelled,
21 Blocked,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum TaskPriority {
28 Low,
29 Medium,
30 High,
31 Critical,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum DependencyType {
38 BlocksStart,
40 BlocksCompletion,
42 Related,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TaskDependency {
49 pub task_id: String,
50 pub dependency_type: DependencyType,
51 pub description: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TaskMetadata {
57 pub estimated_duration: Option<chrono::Duration>,
58 pub actual_duration: Option<chrono::Duration>,
59 pub tags: Vec<String>,
60 pub assignee: Option<String>,
61 pub project: Option<String>,
62 pub milestone: Option<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Task {
68 pub id: String,
69 pub content: String,
70 pub status: TaskStatus,
71 pub priority: TaskPriority,
72 pub created_at: DateTime<Utc>,
73 pub updated_at: DateTime<Utc>,
74 pub completed_at: Option<DateTime<Utc>>,
75 pub due_date: Option<DateTime<Utc>>,
76 pub dependencies: Vec<TaskDependency>,
77 pub metadata: TaskMetadata,
78 pub progress: f32, pub notes: Vec<String>,
80}
81
82impl Task {
83 pub fn new(id: String, content: String, priority: TaskPriority) -> Self {
84 let now = Utc::now();
85 Self {
86 id,
87 content,
88 status: TaskStatus::Pending,
89 priority,
90 created_at: now,
91 updated_at: now,
92 completed_at: None,
93 due_date: None,
94 dependencies: Vec::new(),
95 metadata: TaskMetadata {
96 estimated_duration: None,
97 actual_duration: None,
98 tags: Vec::new(),
99 assignee: None,
100 project: None,
101 milestone: None,
102 },
103 progress: 0.0,
104 notes: Vec::new(),
105 }
106 }
107
108 pub fn update_status(&mut self, status: TaskStatus) {
109 self.status = status;
110 self.updated_at = Utc::now();
111
112 if self.status == TaskStatus::Completed {
113 self.completed_at = Some(Utc::now());
114 self.progress = 1.0;
115
116 self.metadata.actual_duration = Some(
118 self.updated_at.signed_duration_since(self.created_at)
119 );
120 }
121 }
122
123 pub fn add_note(&mut self, note: String) {
124 self.notes.push(note);
125 self.updated_at = Utc::now();
126 }
127
128 pub fn add_dependency(&mut self, dependency: TaskDependency) {
129 self.dependencies.push(dependency);
130 self.updated_at = Utc::now();
131 }
132
133 pub fn is_blocked_by(&self, other_task: &Task) -> bool {
134 self.dependencies.iter().any(|dep| {
135 dep.task_id == other_task.id &&
136 matches!(dep.dependency_type, DependencyType::BlocksStart) &&
137 other_task.status != TaskStatus::Completed
138 })
139 }
140
141 pub fn can_start(&self, all_tasks: &HashMap<String, Task>) -> bool {
142 for dep in &self.dependencies {
144 if matches!(dep.dependency_type, DependencyType::BlocksStart) {
145 if let Some(blocking_task) = all_tasks.get(&dep.task_id) {
146 if blocking_task.status != TaskStatus::Completed {
147 return false;
148 }
149 }
150 }
151 }
152 true
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct TaskList {
159 pub session_id: String,
160 pub tasks: HashMap<String, Task>,
161 pub created_at: DateTime<Utc>,
162 pub updated_at: DateTime<Utc>,
163}
164
165impl TaskList {
166 pub fn new(session_id: String) -> Self {
167 let now = Utc::now();
168 Self {
169 session_id,
170 tasks: HashMap::new(),
171 created_at: now,
172 updated_at: now,
173 }
174 }
175
176 pub fn add_task(&mut self, task: Task) {
177 self.tasks.insert(task.id.clone(), task);
178 self.updated_at = Utc::now();
179 }
180
181 pub fn get_task(&self, id: &str) -> Option<&Task> {
182 self.tasks.get(id)
183 }
184
185 pub fn get_task_mut(&mut self, id: &str) -> Option<&mut Task> {
186 if self.tasks.contains_key(id) {
187 self.updated_at = Utc::now();
188 }
189 self.tasks.get_mut(id)
190 }
191
192 pub fn remove_task(&mut self, id: &str) -> Option<Task> {
193 let task = self.tasks.remove(id);
194 if task.is_some() {
195 self.updated_at = Utc::now();
196 }
197 task
198 }
199
200 pub fn get_tasks_by_status(&self, status: &TaskStatus) -> Vec<&Task> {
201 self.tasks.values().filter(|t| &t.status == status).collect()
202 }
203
204 pub fn get_tasks_by_priority(&self, priority: &TaskPriority) -> Vec<&Task> {
205 self.tasks.values().filter(|t| &t.priority == priority).collect()
206 }
207
208 pub fn get_available_tasks(&self) -> Vec<&Task> {
209 self.tasks.values()
210 .filter(|t| t.status == TaskStatus::Pending && t.can_start(&self.tasks))
211 .collect()
212 }
213
214 pub fn get_blocked_tasks(&self) -> Vec<&Task> {
215 self.tasks.values()
216 .filter(|t| t.status == TaskStatus::Pending && !t.can_start(&self.tasks))
217 .collect()
218 }
219
220 pub fn get_completion_stats(&self) -> TaskStats {
221 let total = self.tasks.len();
222 let completed = self.get_tasks_by_status(&TaskStatus::Completed).len();
223 let in_progress = self.get_tasks_by_status(&TaskStatus::InProgress).len();
224 let pending = self.get_tasks_by_status(&TaskStatus::Pending).len();
225 let blocked = self.get_blocked_tasks().len();
226 let cancelled = self.get_tasks_by_status(&TaskStatus::Cancelled).len();
227
228 TaskStats {
229 total,
230 completed,
231 in_progress,
232 pending,
233 blocked,
234 cancelled,
235 completion_rate: if total > 0 { completed as f32 / total as f32 } else { 0.0 },
236 }
237 }
238}
239
240#[derive(Debug, Serialize, Deserialize)]
242pub struct TaskStats {
243 pub total: usize,
244 pub completed: usize,
245 pub in_progress: usize,
246 pub pending: usize,
247 pub blocked: usize,
248 pub cancelled: usize,
249 pub completion_rate: f32,
250}
251
252#[derive(Debug)]
254pub struct TaskStorage {
255 task_lists: Arc<RwLock<HashMap<String, TaskList>>>,
256}
257
258impl TaskStorage {
259 pub fn new() -> Self {
260 Self {
261 task_lists: Arc::new(RwLock::new(HashMap::new())),
262 }
263 }
264
265 pub async fn get_or_create_list(&self, session_id: &str) -> TaskList {
266 let mut lists = self.task_lists.write().await;
267 lists.entry(session_id.to_string())
268 .or_insert_with(|| TaskList::new(session_id.to_string()))
269 .clone()
270 }
271
272 pub async fn save_list(&self, list: TaskList) {
273 let mut lists = self.task_lists.write().await;
274 lists.insert(list.session_id.clone(), list);
275 }
276
277 pub async fn get_all_sessions(&self) -> Vec<String> {
278 let lists = self.task_lists.read().await;
279 lists.keys().cloned().collect()
280 }
281}
282
283pub struct TodoTool {
285 storage: TaskStorage,
286}
287
288impl TodoTool {
289 pub fn new() -> Self {
290 Self {
291 storage: TaskStorage::new(),
292 }
293 }
294}
295
296#[derive(Debug, Deserialize)]
297#[serde(tag = "action")]
298#[serde(rename_all = "snake_case")]
299enum TodoAction {
300 List,
301 Add { content: String, priority: Option<TaskPriority> },
302 Update {
303 id: String,
304 status: Option<TaskStatus>,
305 priority: Option<TaskPriority>,
306 content: Option<String>,
307 progress: Option<f32>,
308 },
309 Remove { id: String },
310 AddDependency {
311 task_id: String,
312 depends_on: String,
313 dependency_type: DependencyType,
314 description: Option<String>,
315 },
316 AddNote { id: String, note: String },
317 Stats,
318 Export { format: Option<String> },
319}
320
321#[async_trait]
322impl Tool for TodoTool {
323 fn id(&self) -> &str {
324 "todo"
325 }
326
327 fn description(&self) -> &str {
328 "Comprehensive task management tool with dependency tracking, progress reporting, and analytics"
329 }
330
331 fn parameters_schema(&self) -> Value {
332 json!({
333 "type": "object",
334 "properties": {
335 "action": {
336 "type": "string",
337 "enum": ["list", "add", "update", "remove", "add_dependency", "add_note", "stats", "export"],
338 "description": "The action to perform"
339 },
340 "content": {
341 "type": "string",
342 "description": "Task content (for add action)"
343 },
344 "priority": {
345 "type": "string",
346 "enum": ["low", "medium", "high", "critical"],
347 "description": "Task priority"
348 },
349 "id": {
350 "type": "string",
351 "description": "Task ID (for update, remove, add_note actions)"
352 },
353 "status": {
354 "type": "string",
355 "enum": ["pending", "in_progress", "completed", "cancelled", "blocked"],
356 "description": "Task status (for update action)"
357 },
358 "progress": {
359 "type": "number",
360 "minimum": 0.0,
361 "maximum": 1.0,
362 "description": "Task progress from 0.0 to 1.0 (for update action)"
363 },
364 "depends_on": {
365 "type": "string",
366 "description": "ID of task this depends on (for add_dependency action)"
367 },
368 "dependency_type": {
369 "type": "string",
370 "enum": ["blocks_start", "blocks_completion", "related"],
371 "description": "Type of dependency (for add_dependency action)"
372 },
373 "note": {
374 "type": "string",
375 "description": "Note to add to task (for add_note action)"
376 },
377 "format": {
378 "type": "string",
379 "enum": ["json", "markdown", "csv"],
380 "description": "Export format (for export action)"
381 },
382 "description": {
383 "type": "string",
384 "description": "Description for dependency (for add_dependency action)"
385 }
386 },
387 "required": ["action"]
388 })
389 }
390
391 async fn execute(&self, args: Value, ctx: ToolContext) -> Result<ToolResult, ToolError> {
392 let action: TodoAction = serde_json::from_value(args)
393 .map_err(|e| ToolError::InvalidParameters(e.to_string()))?;
394
395 let mut task_list = self.storage.get_or_create_list(&ctx.session_id).await;
396
397 match action {
398 TodoAction::List => {
399 let output = format_task_list(&task_list);
400 let stats = task_list.get_completion_stats();
401
402 Ok(ToolResult {
403 title: format!("Task List ({} tasks)", task_list.tasks.len()),
404 output,
405 metadata: json!({
406 "stats": stats,
407 "session_id": ctx.session_id,
408 "task_count": task_list.tasks.len()
409 }),
410 })
411 },
412
413 TodoAction::Add { content, priority } => {
414 let task_id = Uuid::new_v4().to_string();
415 let priority_value = priority.unwrap_or(TaskPriority::Medium);
416 let task = Task::new(task_id.clone(), content.clone(), priority_value.clone());
417
418 task_list.add_task(task);
419 self.storage.save_list(task_list.clone()).await;
420
421 Ok(ToolResult {
422 title: "Task Added".to_string(),
423 output: format!("Added task: {} (ID: {})", content, task_id),
424 metadata: json!({
425 "task_id": task_id,
426 "content": content,
427 "priority": priority_value
428 }),
429 })
430 },
431
432 TodoAction::Update { id, status, priority, content, progress } => {
433 if let Some(task) = task_list.get_task_mut(&id) {
434 if let Some(new_status) = status {
435 task.update_status(new_status);
436 }
437 if let Some(new_priority) = priority {
438 task.priority = new_priority;
439 task.updated_at = Utc::now();
440 }
441 if let Some(new_content) = content {
442 task.content = new_content;
443 task.updated_at = Utc::now();
444 }
445 if let Some(new_progress) = progress {
446 task.progress = new_progress.clamp(0.0, 1.0);
447 task.updated_at = Utc::now();
448 }
449
450 let task_content = task.content.clone();
451 let task_status = task.status.clone();
452 let task_priority = task.priority.clone();
453 let task_progress = task.progress;
454
455 self.storage.save_list(task_list).await;
456
457 Ok(ToolResult {
458 title: "Task Updated".to_string(),
459 output: format!("Updated task: {}", task_content),
460 metadata: json!({
461 "task_id": id,
462 "status": task_status,
463 "priority": task_priority,
464 "progress": task_progress
465 }),
466 })
467 } else {
468 Err(ToolError::InvalidParameters(format!("Task not found: {}", id)))
469 }
470 },
471
472 TodoAction::Remove { id } => {
473 if let Some(task) = task_list.remove_task(&id) {
474 self.storage.save_list(task_list).await;
475
476 Ok(ToolResult {
477 title: "Task Removed".to_string(),
478 output: format!("Removed task: {}", task.content),
479 metadata: json!({
480 "task_id": id,
481 "content": task.content
482 }),
483 })
484 } else {
485 Err(ToolError::InvalidParameters(format!("Task not found: {}", id)))
486 }
487 },
488
489 TodoAction::AddDependency { task_id, depends_on, dependency_type, description } => {
490 if !task_list.tasks.contains_key(&task_id) {
492 return Err(ToolError::InvalidParameters(format!("Task not found: {}", task_id)));
493 }
494 if !task_list.tasks.contains_key(&depends_on) {
495 return Err(ToolError::InvalidParameters(format!("Dependency task not found: {}", depends_on)));
496 }
497
498 if would_create_cycle(&task_list, &task_id, &depends_on) {
500 return Err(ToolError::InvalidParameters("Adding this dependency would create a circular dependency".to_string()));
501 }
502
503 if let Some(task) = task_list.get_task_mut(&task_id) {
504 let dependency = TaskDependency {
505 task_id: depends_on.clone(),
506 dependency_type: dependency_type.clone(),
507 description,
508 };
509 task.add_dependency(dependency);
510 self.storage.save_list(task_list).await;
511
512 Ok(ToolResult {
513 title: "Dependency Added".to_string(),
514 output: format!("Added dependency: {} depends on {}", task_id, depends_on),
515 metadata: json!({
516 "task_id": task_id,
517 "depends_on": depends_on,
518 "dependency_type": dependency_type
519 }),
520 })
521 } else {
522 Err(ToolError::InvalidParameters(format!("Task not found: {}", task_id)))
523 }
524 },
525
526 TodoAction::AddNote { id, note } => {
527 if let Some(task) = task_list.get_task_mut(&id) {
528 task.add_note(note.clone());
529 let task_content = task.content.clone();
530 self.storage.save_list(task_list).await;
531
532 Ok(ToolResult {
533 title: "Note Added".to_string(),
534 output: format!("Added note to task {}: {}", task_content, note),
535 metadata: json!({
536 "task_id": id,
537 "note": note
538 }),
539 })
540 } else {
541 Err(ToolError::InvalidParameters(format!("Task not found: {}", id)))
542 }
543 },
544
545 TodoAction::Stats => {
546 let stats = task_list.get_completion_stats();
547 let available_tasks = task_list.get_available_tasks();
548 let blocked_tasks = task_list.get_blocked_tasks();
549
550 let output = format!(
551 "Task Statistics:\n\
552 Total tasks: {}\n\
553 Completed: {}\n\
554 In progress: {}\n\
555 Pending: {}\n\
556 Blocked: {}\n\
557 Cancelled: {}\n\
558 Completion rate: {:.1}%\n\n\
559 Available tasks (can start now): {}\n\
560 Blocked tasks (waiting on dependencies): {}",
561 stats.total,
562 stats.completed,
563 stats.in_progress,
564 stats.pending,
565 stats.blocked,
566 stats.cancelled,
567 stats.completion_rate * 100.0,
568 available_tasks.len(),
569 blocked_tasks.len()
570 );
571
572 Ok(ToolResult {
573 title: "Task Statistics".to_string(),
574 output,
575 metadata: json!({
576 "stats": stats,
577 "available_tasks": available_tasks.len(),
578 "blocked_tasks": blocked_tasks.len()
579 }),
580 })
581 },
582
583 TodoAction::Export { format } => {
584 let format = format.as_deref().unwrap_or("json");
585 let output = match format {
586 "json" => serde_json::to_string_pretty(&task_list.tasks)
587 .map_err(|e| ToolError::ExecutionFailed(format!("JSON serialization failed: {}", e)))?,
588 "markdown" => export_to_markdown(&task_list),
589 "csv" => export_to_csv(&task_list)?,
590 _ => return Err(ToolError::InvalidParameters("Unsupported export format".to_string())),
591 };
592
593 Ok(ToolResult {
594 title: format!("Task Export ({})", format),
595 output,
596 metadata: json!({
597 "format": format,
598 "task_count": task_list.tasks.len(),
599 "exported_at": Utc::now()
600 }),
601 })
602 },
603 }
604 }
605}
606
607fn format_task_list(task_list: &TaskList) -> String {
609 if task_list.tasks.is_empty() {
610 return "No tasks found.".to_string();
611 }
612
613 let mut output = String::new();
614 let stats = task_list.get_completion_stats();
615
616 output.push_str(&format!(
617 "Task List ({}% complete)\n\n",
618 (stats.completion_rate * 100.0) as u32
619 ));
620
621 let statuses = [
623 TaskStatus::InProgress,
624 TaskStatus::Pending,
625 TaskStatus::Blocked,
626 TaskStatus::Completed,
627 TaskStatus::Cancelled,
628 ];
629
630 for status in &statuses {
631 let tasks = task_list.get_tasks_by_status(status);
632 if !tasks.is_empty() {
633 output.push_str(&format!("## {:?} ({})\n", status, tasks.len()));
634
635 for task in tasks {
636 let progress_bar = create_progress_bar(task.progress);
637 let dependencies = if task.dependencies.is_empty() {
638 String::new()
639 } else {
640 format!(" (depends on: {})",
641 task.dependencies.iter()
642 .map(|d| &d.task_id[..8])
643 .collect::<Vec<_>>()
644 .join(", ")
645 )
646 };
647
648 output.push_str(&format!(
649 "- [{}] {} {} {}{}\n",
650 if task.status == TaskStatus::Completed { "x" } else { " " },
651 task.content,
652 format!("({:?})", task.priority),
653 progress_bar,
654 dependencies
655 ));
656
657 if !task.notes.is_empty() {
658 for note in &task.notes {
659 output.push_str(&format!(" 📝 {}\n", note));
660 }
661 }
662 }
663 output.push('\n');
664 }
665 }
666
667 output
668}
669
670fn create_progress_bar(progress: f32) -> String {
672 let width = 10;
673 let filled = (progress * width as f32) as usize;
674 let empty = width - filled;
675 format!("[{}{}] {:.0}%", "█".repeat(filled), "░".repeat(empty), progress * 100.0)
676}
677
678fn export_to_markdown(task_list: &TaskList) -> String {
680 let mut output = format!("# Task List - {}\n\n", task_list.session_id);
681
682 for task in task_list.tasks.values() {
683 output.push_str(&format!(
684 "## {} ({})\n\n",
685 task.content,
686 task.id
687 ));
688
689 output.push_str(&format!("- **Status**: {:?}\n", task.status));
690 output.push_str(&format!("- **Priority**: {:?}\n", task.priority));
691 output.push_str(&format!("- **Progress**: {:.1}%\n", task.progress * 100.0));
692 output.push_str(&format!("- **Created**: {}\n", task.created_at.format("%Y-%m-%d %H:%M:%S")));
693
694 if let Some(completed_at) = task.completed_at {
695 output.push_str(&format!("- **Completed**: {}\n", completed_at.format("%Y-%m-%d %H:%M:%S")));
696 }
697
698 if !task.dependencies.is_empty() {
699 output.push_str("- **Dependencies**:\n");
700 for dep in &task.dependencies {
701 output.push_str(&format!(" - {} ({:?})\n", dep.task_id, dep.dependency_type));
702 }
703 }
704
705 if !task.notes.is_empty() {
706 output.push_str("- **Notes**:\n");
707 for note in &task.notes {
708 output.push_str(&format!(" - {}\n", note));
709 }
710 }
711
712 output.push('\n');
713 }
714
715 output
716}
717
718fn export_to_csv(task_list: &TaskList) -> Result<String, ToolError> {
720 let mut output = "ID,Content,Status,Priority,Progress,Created,Updated,Completed,Dependencies,Notes\n".to_string();
721
722 for task in task_list.tasks.values() {
723 let dependencies = task.dependencies.iter()
724 .map(|d| format!("{}:{:?}", d.task_id, d.dependency_type))
725 .collect::<Vec<_>>()
726 .join(";");
727
728 let notes = task.notes.join(";");
729
730 let completed = task.completed_at
731 .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string())
732 .unwrap_or_default();
733
734 output.push_str(&format!(
735 "{},{},{:?},{:?},{:.2},{},{},{},{},{}\n",
736 task.id,
737 task.content,
738 task.status,
739 task.priority,
740 task.progress,
741 task.created_at.format("%Y-%m-%d %H:%M:%S"),
742 task.updated_at.format("%Y-%m-%d %H:%M:%S"),
743 completed,
744 dependencies,
745 notes
746 ));
747 }
748
749 Ok(output)
750}
751
752fn would_create_cycle(task_list: &TaskList, from_task: &str, to_task: &str) -> bool {
754 let mut visited = HashSet::new();
755 let mut stack = vec![to_task];
756
757 while let Some(current) = stack.pop() {
758 if current == from_task {
759 return true; }
761
762 if visited.contains(current) {
763 continue;
764 }
765 visited.insert(current);
766
767 if let Some(task) = task_list.tasks.get(current) {
768 for dep in &task.dependencies {
769 if matches!(dep.dependency_type, DependencyType::BlocksStart | DependencyType::BlocksCompletion) {
770 stack.push(&dep.task_id);
771 }
772 }
773 }
774 }
775
776 false
777}
778
779impl Default for TodoTool {
780 fn default() -> Self {
781 Self::new()
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788
789 #[test]
790 fn test_task_creation() {
791 let task = Task::new("test-1".to_string(), "Test task".to_string(), TaskPriority::High);
792 assert_eq!(task.id, "test-1");
793 assert_eq!(task.content, "Test task");
794 assert_eq!(task.priority, TaskPriority::High);
795 assert_eq!(task.status, TaskStatus::Pending);
796 assert_eq!(task.progress, 0.0);
797 }
798
799 #[test]
800 fn test_task_status_update() {
801 let mut task = Task::new("test-1".to_string(), "Test task".to_string(), TaskPriority::Medium);
802 task.update_status(TaskStatus::Completed);
803
804 assert_eq!(task.status, TaskStatus::Completed);
805 assert_eq!(task.progress, 1.0);
806 assert!(task.completed_at.is_some());
807 assert!(task.metadata.actual_duration.is_some());
808 }
809
810 #[test]
811 fn test_task_dependencies() {
812 let mut task1 = Task::new("task-1".to_string(), "First task".to_string(), TaskPriority::High);
813 let task2 = Task::new("task-2".to_string(), "Second task".to_string(), TaskPriority::Medium);
814
815 let dependency = TaskDependency {
816 task_id: task2.id.clone(),
817 dependency_type: DependencyType::BlocksStart,
818 description: Some("Must complete first".to_string()),
819 };
820
821 task1.add_dependency(dependency);
822 assert_eq!(task1.dependencies.len(), 1);
823 assert!(task1.is_blocked_by(&task2));
824 }
825
826 #[test]
827 fn test_cycle_detection() {
828 let mut task_list = TaskList::new("test-session".to_string());
829
830 let task1 = Task::new("task-1".to_string(), "Task 1".to_string(), TaskPriority::Medium);
831 let mut task2 = Task::new("task-2".to_string(), "Task 2".to_string(), TaskPriority::Medium);
832
833 task2.add_dependency(TaskDependency {
835 task_id: "task-1".to_string(),
836 dependency_type: DependencyType::BlocksStart,
837 description: None,
838 });
839
840 task_list.add_task(task1);
841 task_list.add_task(task2);
842
843 assert!(would_create_cycle(&task_list, "task-1", "task-2"));
845 assert!(!would_create_cycle(&task_list, "task-2", "task-1"));
846 }
847}