1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
4#[serde(rename_all = "kebab-case")]
5pub enum TaskStatus {
6 #[default]
7 Pending,
8 InProgress,
9 Done,
10 Review,
11 Blocked,
12 Deferred,
13 Cancelled,
14 Expanded, Failed, }
17
18impl TaskStatus {
19 pub fn as_str(&self) -> &'static str {
20 match self {
21 TaskStatus::Pending => "pending",
22 TaskStatus::InProgress => "in-progress",
23 TaskStatus::Done => "done",
24 TaskStatus::Review => "review",
25 TaskStatus::Blocked => "blocked",
26 TaskStatus::Deferred => "deferred",
27 TaskStatus::Cancelled => "cancelled",
28 TaskStatus::Expanded => "expanded",
29 TaskStatus::Failed => "failed",
30 }
31 }
32
33 #[allow(clippy::should_implement_trait)]
34 pub fn from_str(s: &str) -> Option<Self> {
35 match s {
36 "pending" => Some(TaskStatus::Pending),
37 "in-progress" => Some(TaskStatus::InProgress),
38 "done" => Some(TaskStatus::Done),
39 "review" => Some(TaskStatus::Review),
40 "blocked" => Some(TaskStatus::Blocked),
41 "deferred" => Some(TaskStatus::Deferred),
42 "cancelled" => Some(TaskStatus::Cancelled),
43 "expanded" => Some(TaskStatus::Expanded),
44 "failed" => Some(TaskStatus::Failed),
45 _ => None,
46 }
47 }
48
49 pub fn all() -> Vec<&'static str> {
50 vec![
51 "pending",
52 "in-progress",
53 "done",
54 "review",
55 "blocked",
56 "deferred",
57 "cancelled",
58 "expanded",
59 "failed",
60 ]
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
65#[serde(rename_all = "lowercase")]
66pub enum Priority {
67 Critical,
68 High,
69 #[default]
70 Medium,
71 Low,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Task {
76 pub id: String,
77 pub title: String,
78 pub description: String,
79
80 #[serde(default)]
81 pub status: TaskStatus,
82
83 #[serde(default)]
84 pub complexity: u32,
85
86 #[serde(default)]
87 pub priority: Priority,
88
89 #[serde(default)]
90 pub dependencies: Vec<String>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub parent_id: Option<String>,
95
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub subtasks: Vec<String>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub details: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub test_strategy: Option<String>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub created_at: Option<String>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub updated_at: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub assigned_to: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub agent_type: Option<String>,
118}
119
120impl Task {
121 const MAX_TITLE_LENGTH: usize = 200;
123 const MAX_DESCRIPTION_LENGTH: usize = 5000;
124 const VALID_FIBONACCI_NUMBERS: &'static [u32] = &[0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
125
126 pub const ID_SEPARATOR: char = ':';
128
129 pub fn new(id: String, title: String, description: String) -> Self {
130 let now = chrono::Utc::now().to_rfc3339();
131 Task {
132 id,
133 title,
134 description,
135 status: TaskStatus::Pending,
136 complexity: 0,
137 priority: Priority::Medium,
138 dependencies: Vec::new(),
139 parent_id: None,
140 subtasks: Vec::new(),
141 details: None,
142 test_strategy: None,
143 created_at: Some(now.clone()),
144 updated_at: Some(now),
145 assigned_to: None,
146 agent_type: None,
147 }
148 }
149
150 pub fn parse_id(id: &str) -> Option<(&str, &str)> {
154 id.split_once(Self::ID_SEPARATOR)
155 }
156
157 pub fn make_id(epic_tag: &str, local_id: &str) -> String {
159 format!("{}{}{}", epic_tag, Self::ID_SEPARATOR, local_id)
160 }
161
162 pub fn local_id(&self) -> &str {
164 Self::parse_id(&self.id)
165 .map(|(_, local)| local)
166 .unwrap_or(&self.id)
167 }
168
169 pub fn epic_tag(&self) -> Option<&str> {
171 Self::parse_id(&self.id).map(|(tag, _)| tag)
172 }
173
174 pub fn is_subtask(&self) -> bool {
176 self.parent_id.is_some()
177 }
178
179 pub fn is_expanded(&self) -> bool {
181 self.status == TaskStatus::Expanded || !self.subtasks.is_empty()
182 }
183
184 pub fn validate_id(id: &str) -> Result<(), String> {
187 if id.is_empty() {
188 return Err("Task ID cannot be empty".to_string());
189 }
190
191 if id.len() > 100 {
192 return Err("Task ID too long (max 100 characters)".to_string());
193 }
194
195 let valid_chars = id
197 .chars()
198 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ':' || c == '.');
199
200 if !valid_chars {
201 return Err(
202 "Task ID can only contain alphanumeric characters, hyphens, underscores, colons, and dots"
203 .to_string(),
204 );
205 }
206
207 Ok(())
208 }
209
210 pub fn validate_title(title: &str) -> Result<(), String> {
212 if title.trim().is_empty() {
213 return Err("Task title cannot be empty".to_string());
214 }
215
216 if title.len() > Self::MAX_TITLE_LENGTH {
217 return Err(format!(
218 "Task title too long (max {} characters)",
219 Self::MAX_TITLE_LENGTH
220 ));
221 }
222
223 Ok(())
224 }
225
226 pub fn validate_description(description: &str) -> Result<(), String> {
228 if description.len() > Self::MAX_DESCRIPTION_LENGTH {
229 return Err(format!(
230 "Task description too long (max {} characters)",
231 Self::MAX_DESCRIPTION_LENGTH
232 ));
233 }
234
235 Ok(())
236 }
237
238 pub fn validate_complexity(complexity: u32) -> Result<(), String> {
240 if !Self::VALID_FIBONACCI_NUMBERS.contains(&complexity) {
241 return Err(format!(
242 "Complexity must be a Fibonacci number: {:?}",
243 Self::VALID_FIBONACCI_NUMBERS
244 ));
245 }
246
247 Ok(())
248 }
249
250 pub fn sanitize_text(text: &str) -> String {
252 text.replace('<', "<")
253 .replace('>', ">")
254 .replace('"', """)
255 .replace('\'', "'")
256 }
257
258 pub fn validate(&self) -> Result<(), Vec<String>> {
260 let mut errors = Vec::new();
261
262 if let Err(e) = Self::validate_id(&self.id) {
263 errors.push(e);
264 }
265
266 if let Err(e) = Self::validate_title(&self.title) {
267 errors.push(e);
268 }
269
270 if let Err(e) = Self::validate_description(&self.description) {
271 errors.push(e);
272 }
273
274 if self.complexity > 0 {
275 if let Err(e) = Self::validate_complexity(self.complexity) {
276 errors.push(e);
277 }
278 }
279
280 if errors.is_empty() {
281 Ok(())
282 } else {
283 Err(errors)
284 }
285 }
286
287 pub fn set_status(&mut self, status: TaskStatus) {
288 self.status = status;
289 self.updated_at = Some(chrono::Utc::now().to_rfc3339());
290 }
291
292 pub fn update(&mut self) {
293 self.updated_at = Some(chrono::Utc::now().to_rfc3339());
294 }
295
296 pub fn has_dependencies_met(&self, all_tasks: &[Task]) -> bool {
297 self.dependencies.iter().all(|dep_id| {
298 all_tasks
299 .iter()
300 .find(|t| &t.id == dep_id)
301 .map(|t| t.status == TaskStatus::Done)
302 .unwrap_or(false)
303 })
304 }
305
306 pub fn get_effective_dependencies(&self, all_tasks: &[&Task]) -> Vec<String> {
309 let mut deps = self.dependencies.clone();
310
311 if let Some(ref parent_id) = self.parent_id {
313 if let Some(parent) = all_tasks.iter().find(|t| &t.id == parent_id) {
314 let parent_deps = parent.get_effective_dependencies(all_tasks);
316 deps.extend(parent_deps);
317 }
318 }
319
320 deps.sort();
322 deps.dedup();
323 deps
324 }
325
326 pub fn has_dependencies_met_refs(&self, all_tasks: &[&Task]) -> bool {
330 self.get_effective_dependencies(all_tasks)
331 .iter()
332 .all(|dep_id| {
333 all_tasks
334 .iter()
335 .find(|t| &t.id == dep_id)
336 .map(|t| t.status == TaskStatus::Done)
337 .unwrap_or(false)
338 })
339 }
340
341 pub fn needs_expansion(&self) -> bool {
345 self.complexity >= 5 && !self.is_expanded() && !self.is_subtask()
346 }
347
348 pub fn recommended_subtasks(&self) -> usize {
353 Self::recommended_subtasks_for_complexity(self.complexity)
354 }
355
356 pub fn recommended_subtasks_for_complexity(complexity: u32) -> usize {
358 match complexity {
359 0..=3 => 0, 5 => 2, 8 => 2, 13 => 3, _ => 3, }
365 }
366
367 pub fn assign(&mut self, assignee: &str) {
369 self.assigned_to = Some(assignee.to_string());
370 self.update();
371 }
372
373 pub fn is_assigned_to(&self, assignee: &str) -> bool {
374 self.assigned_to
375 .as_ref()
376 .map(|s| s == assignee)
377 .unwrap_or(false)
378 }
379
380 pub fn would_create_cycle(&self, new_dep_id: &str, all_tasks: &[Task]) -> Result<(), String> {
383 if self.id == new_dep_id {
384 return Err(format!("Self-reference: {} -> {}", self.id, new_dep_id));
385 }
386
387 let mut visited = std::collections::HashSet::new();
388 let mut path = Vec::new();
389
390 Self::detect_cycle_recursive(new_dep_id, &self.id, all_tasks, &mut visited, &mut path)
391 }
392
393 fn detect_cycle_recursive(
394 current_id: &str,
395 target_id: &str,
396 all_tasks: &[Task],
397 visited: &mut std::collections::HashSet<String>,
398 path: &mut Vec<String>,
399 ) -> Result<(), String> {
400 if current_id == target_id {
401 path.push(current_id.to_string());
402 return Err(format!("Circular dependency: {}", path.join(" -> ")));
403 }
404
405 if visited.contains(current_id) {
406 return Ok(());
407 }
408
409 visited.insert(current_id.to_string());
410 path.push(current_id.to_string());
411
412 if let Some(task) = all_tasks.iter().find(|t| t.id == current_id) {
413 for dep_id in &task.dependencies {
414 Self::detect_cycle_recursive(dep_id, target_id, all_tasks, visited, path)?;
415 }
416 }
417
418 path.pop();
419 Ok(())
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_task_creation() {
429 let task = Task::new(
430 "TASK-1".to_string(),
431 "Test Task".to_string(),
432 "Description".to_string(),
433 );
434
435 assert_eq!(task.id, "TASK-1");
436 assert_eq!(task.title, "Test Task");
437 assert_eq!(task.description, "Description");
438 assert_eq!(task.status, TaskStatus::Pending);
439 assert_eq!(task.complexity, 0);
440 assert_eq!(task.priority, Priority::Medium);
441 assert!(task.dependencies.is_empty());
442 assert!(task.created_at.is_some());
443 assert!(task.updated_at.is_some());
444 assert!(task.assigned_to.is_none());
445 }
446
447 #[test]
448 fn test_status_conversion() {
449 assert_eq!(TaskStatus::Pending.as_str(), "pending");
450 assert_eq!(TaskStatus::InProgress.as_str(), "in-progress");
451 assert_eq!(TaskStatus::Done.as_str(), "done");
452 assert_eq!(TaskStatus::Review.as_str(), "review");
453 assert_eq!(TaskStatus::Blocked.as_str(), "blocked");
454 assert_eq!(TaskStatus::Deferred.as_str(), "deferred");
455 assert_eq!(TaskStatus::Cancelled.as_str(), "cancelled");
456 }
457
458 #[test]
459 fn test_status_from_string() {
460 assert_eq!(TaskStatus::from_str("pending"), Some(TaskStatus::Pending));
461 assert_eq!(
462 TaskStatus::from_str("in-progress"),
463 Some(TaskStatus::InProgress)
464 );
465 assert_eq!(TaskStatus::from_str("done"), Some(TaskStatus::Done));
466 assert_eq!(TaskStatus::from_str("invalid"), None);
467 }
468
469 #[test]
470 fn test_set_status_updates_timestamp() {
471 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
472 let initial_updated = task.updated_at.clone();
473
474 std::thread::sleep(std::time::Duration::from_millis(10));
475 task.set_status(TaskStatus::InProgress);
476
477 assert_eq!(task.status, TaskStatus::InProgress);
478 assert!(task.updated_at > initial_updated);
479 }
480
481 #[test]
482 fn test_task_assignment() {
483 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
484
485 task.assign("alice");
486 assert_eq!(task.assigned_to, Some("alice".to_string()));
487 assert!(task.is_assigned_to("alice"));
488 assert!(!task.is_assigned_to("bob"));
489 }
490
491 #[test]
492 fn test_has_dependencies_met_all_done() {
493 let mut task = Task::new("TASK-3".to_string(), "Test".to_string(), "Desc".to_string());
494 task.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
495
496 let mut task1 = Task::new(
497 "TASK-1".to_string(),
498 "Dep 1".to_string(),
499 "Desc".to_string(),
500 );
501 task1.set_status(TaskStatus::Done);
502
503 let mut task2 = Task::new(
504 "TASK-2".to_string(),
505 "Dep 2".to_string(),
506 "Desc".to_string(),
507 );
508 task2.set_status(TaskStatus::Done);
509
510 let all_tasks = vec![task1, task2];
511 assert!(task.has_dependencies_met(&all_tasks));
512 }
513
514 #[test]
515 fn test_has_dependencies_met_some_pending() {
516 let mut task = Task::new("TASK-3".to_string(), "Test".to_string(), "Desc".to_string());
517 task.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
518
519 let mut task1 = Task::new(
520 "TASK-1".to_string(),
521 "Dep 1".to_string(),
522 "Desc".to_string(),
523 );
524 task1.set_status(TaskStatus::Done);
525
526 let task2 = Task::new(
527 "TASK-2".to_string(),
528 "Dep 2".to_string(),
529 "Desc".to_string(),
530 );
531 let all_tasks = vec![task1, task2];
534 assert!(!task.has_dependencies_met(&all_tasks));
535 }
536
537 #[test]
538 fn test_has_dependencies_met_missing_dependency() {
539 let mut task = Task::new("TASK-3".to_string(), "Test".to_string(), "Desc".to_string());
540 task.dependencies = vec!["TASK-1".to_string(), "TASK-MISSING".to_string()];
541
542 let mut task1 = Task::new(
543 "TASK-1".to_string(),
544 "Dep 1".to_string(),
545 "Desc".to_string(),
546 );
547 task1.set_status(TaskStatus::Done);
548
549 let all_tasks = vec![task1];
550 assert!(!task.has_dependencies_met(&all_tasks));
551 }
552
553 #[test]
554 fn test_needs_expansion() {
555 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
556
557 task.complexity = 1;
559 assert!(!task.needs_expansion());
560
561 task.complexity = 2;
562 assert!(!task.needs_expansion());
563
564 task.complexity = 3;
565 assert!(!task.needs_expansion());
566
567 task.complexity = 5;
569 assert!(task.needs_expansion());
570
571 task.complexity = 8;
572 assert!(task.needs_expansion());
573
574 task.complexity = 13;
575 assert!(task.needs_expansion());
576
577 task.complexity = 21;
578 assert!(task.needs_expansion());
579
580 task.status = TaskStatus::Expanded;
582 assert!(!task.needs_expansion());
583
584 task.status = TaskStatus::Pending;
586 task.parent_id = Some("parent:1".to_string());
587 assert!(!task.needs_expansion()); task.parent_id = None;
591 task.subtasks = vec!["TASK-1.1".to_string()];
592 assert!(!task.needs_expansion()); }
594
595 #[test]
596 fn test_task_serialization() {
597 let task = Task::new(
598 "TASK-1".to_string(),
599 "Test Task".to_string(),
600 "Description".to_string(),
601 );
602
603 let json = serde_json::to_string(&task).unwrap();
604 let deserialized: Task = serde_json::from_str(&json).unwrap();
605
606 assert_eq!(task.id, deserialized.id);
607 assert_eq!(task.title, deserialized.title);
608 assert_eq!(task.description, deserialized.description);
609 }
610
611 #[test]
612 fn test_task_serialization_with_optional_fields() {
613 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
614 task.details = Some("Detailed info".to_string());
615 task.test_strategy = Some("Test plan".to_string());
616 task.assign("alice");
617
618 let json = serde_json::to_string(&task).unwrap();
619 let deserialized: Task = serde_json::from_str(&json).unwrap();
620
621 assert_eq!(task.details, deserialized.details);
622 assert_eq!(task.test_strategy, deserialized.test_strategy);
623 assert_eq!(task.assigned_to, deserialized.assigned_to);
624 }
625
626 #[test]
627 fn test_priority_default() {
628 let default_priority = Priority::default();
629 assert_eq!(default_priority, Priority::Medium);
630 }
631
632 #[test]
633 fn test_status_all() {
634 let all_statuses = TaskStatus::all();
635 assert_eq!(all_statuses.len(), 9);
636 assert!(all_statuses.contains(&"pending"));
637 assert!(all_statuses.contains(&"in-progress"));
638 assert!(all_statuses.contains(&"done"));
639 assert!(all_statuses.contains(&"review"));
640 assert!(all_statuses.contains(&"blocked"));
641 assert!(all_statuses.contains(&"deferred"));
642 assert!(all_statuses.contains(&"cancelled"));
643 assert!(all_statuses.contains(&"expanded"));
644 assert!(all_statuses.contains(&"failed"));
645 }
646
647 #[test]
648 fn test_circular_dependency_self_reference() {
649 let task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
650 let all_tasks = vec![task.clone()];
651
652 let result = task.would_create_cycle("TASK-1", &all_tasks);
653 assert!(result.is_err());
654 assert!(result.unwrap_err().contains("Self-reference"));
655 }
656
657 #[test]
658 fn test_circular_dependency_direct_cycle() {
659 let mut task1 = Task::new(
660 "TASK-1".to_string(),
661 "Task 1".to_string(),
662 "Desc".to_string(),
663 );
664 task1.dependencies = vec!["TASK-2".to_string()];
665
666 let task2 = Task::new(
667 "TASK-2".to_string(),
668 "Task 2".to_string(),
669 "Desc".to_string(),
670 );
671
672 let all_tasks = vec![task1.clone(), task2.clone()];
673
674 let result = task2.would_create_cycle("TASK-1", &all_tasks);
676 assert!(result.is_err());
677 assert!(result.unwrap_err().contains("Circular dependency"));
678 }
679
680 #[test]
681 fn test_circular_dependency_indirect_cycle() {
682 let mut task1 = Task::new(
683 "TASK-1".to_string(),
684 "Task 1".to_string(),
685 "Desc".to_string(),
686 );
687 task1.dependencies = vec!["TASK-2".to_string()];
688
689 let mut task2 = Task::new(
690 "TASK-2".to_string(),
691 "Task 2".to_string(),
692 "Desc".to_string(),
693 );
694 task2.dependencies = vec!["TASK-3".to_string()];
695
696 let task3 = Task::new(
697 "TASK-3".to_string(),
698 "Task 3".to_string(),
699 "Desc".to_string(),
700 );
701
702 let all_tasks = vec![task1.clone(), task2, task3.clone()];
703
704 let result = task3.would_create_cycle("TASK-1", &all_tasks);
707 assert!(result.is_err());
708 assert!(result.unwrap_err().contains("Circular dependency"));
709 }
710
711 #[test]
712 fn test_circular_dependency_no_cycle() {
713 let mut task1 = Task::new(
714 "TASK-1".to_string(),
715 "Task 1".to_string(),
716 "Desc".to_string(),
717 );
718 task1.dependencies = vec!["TASK-3".to_string()];
719
720 let task2 = Task::new(
721 "TASK-2".to_string(),
722 "Task 2".to_string(),
723 "Desc".to_string(),
724 );
725
726 let task3 = Task::new(
727 "TASK-3".to_string(),
728 "Task 3".to_string(),
729 "Desc".to_string(),
730 );
731
732 let all_tasks = vec![task1.clone(), task2.clone(), task3];
733
734 let result = task1.would_create_cycle("TASK-2", &all_tasks);
736 assert!(result.is_ok());
737 }
738
739 #[test]
740 fn test_circular_dependency_complex_graph() {
741 let mut task1 = Task::new(
742 "TASK-1".to_string(),
743 "Task 1".to_string(),
744 "Desc".to_string(),
745 );
746 task1.dependencies = vec!["TASK-2".to_string(), "TASK-3".to_string()];
747
748 let mut task2 = Task::new(
749 "TASK-2".to_string(),
750 "Task 2".to_string(),
751 "Desc".to_string(),
752 );
753 task2.dependencies = vec!["TASK-4".to_string()];
754
755 let mut task3 = Task::new(
756 "TASK-3".to_string(),
757 "Task 3".to_string(),
758 "Desc".to_string(),
759 );
760 task3.dependencies = vec!["TASK-4".to_string()];
761
762 let task4 = Task::new(
763 "TASK-4".to_string(),
764 "Task 4".to_string(),
765 "Desc".to_string(),
766 );
767
768 let all_tasks = vec![task1.clone(), task2, task3, task4.clone()];
769
770 let result = task4.would_create_cycle("TASK-1", &all_tasks);
772 assert!(result.is_err());
773 assert!(result.unwrap_err().contains("Circular dependency"));
774 }
775
776 #[test]
778 fn test_validate_id_success() {
779 assert!(Task::validate_id("TASK-123").is_ok());
780 assert!(Task::validate_id("task_456").is_ok());
781 assert!(Task::validate_id("Feature-789").is_ok());
782 assert!(Task::validate_id("phase1:10").is_ok());
784 assert!(Task::validate_id("phase1:10.1").is_ok());
785 assert!(Task::validate_id("my-epic:subtask-1.2.3").is_ok());
786 }
787
788 #[test]
789 fn test_validate_id_empty() {
790 let result = Task::validate_id("");
791 assert!(result.is_err());
792 assert_eq!(result.unwrap_err(), "Task ID cannot be empty");
793 }
794
795 #[test]
796 fn test_validate_id_too_long() {
797 let long_id = "A".repeat(101);
798 let result = Task::validate_id(&long_id);
799 assert!(result.is_err());
800 assert!(result.unwrap_err().contains("too long"));
801 }
802
803 #[test]
804 fn test_validate_id_invalid_characters() {
805 assert!(Task::validate_id("TASK@123").is_err());
806 assert!(Task::validate_id("TASK 123").is_err());
807 assert!(Task::validate_id("TASK#123").is_err());
808 assert!(Task::validate_id("TASK.123").is_ok()); assert!(Task::validate_id("epic:TASK-1").is_ok()); }
812
813 #[test]
814 fn test_validate_title_success() {
815 assert!(Task::validate_title("Valid title").is_ok());
816 assert!(Task::validate_title("A").is_ok());
817 }
818
819 #[test]
820 fn test_validate_title_empty() {
821 let result = Task::validate_title("");
822 assert!(result.is_err());
823 assert_eq!(result.unwrap_err(), "Task title cannot be empty");
824
825 let result = Task::validate_title(" ");
826 assert!(result.is_err());
827 assert_eq!(result.unwrap_err(), "Task title cannot be empty");
828 }
829
830 #[test]
831 fn test_validate_title_too_long() {
832 let long_title = "A".repeat(201);
833 let result = Task::validate_title(&long_title);
834 assert!(result.is_err());
835 assert!(result.unwrap_err().contains("too long"));
836 }
837
838 #[test]
839 fn test_validate_description_success() {
840 assert!(Task::validate_description("Valid description").is_ok());
841 assert!(Task::validate_description("").is_ok());
842 }
843
844 #[test]
845 fn test_validate_description_too_long() {
846 let long_desc = "A".repeat(5001);
847 let result = Task::validate_description(&long_desc);
848 assert!(result.is_err());
849 assert!(result.unwrap_err().contains("too long"));
850 }
851
852 #[test]
853 fn test_validate_complexity_success() {
854 assert!(Task::validate_complexity(0).is_ok());
855 assert!(Task::validate_complexity(1).is_ok());
856 assert!(Task::validate_complexity(2).is_ok());
857 assert!(Task::validate_complexity(3).is_ok());
858 assert!(Task::validate_complexity(5).is_ok());
859 assert!(Task::validate_complexity(8).is_ok());
860 assert!(Task::validate_complexity(13).is_ok());
861 assert!(Task::validate_complexity(21).is_ok());
862 }
863
864 #[test]
865 fn test_validate_complexity_invalid() {
866 assert!(Task::validate_complexity(4).is_err());
867 assert!(Task::validate_complexity(6).is_err());
868 assert!(Task::validate_complexity(7).is_err());
869 assert!(Task::validate_complexity(100).is_err());
870 }
871
872 #[test]
873 fn test_sanitize_text() {
874 assert_eq!(
875 Task::sanitize_text("<script>alert('xss')</script>"),
876 "<script>alert('xss')</script>"
877 );
878 assert_eq!(Task::sanitize_text("Normal text"), "Normal text");
879 assert_eq!(
880 Task::sanitize_text("<div>Content</div>"),
881 "<div>Content</div>"
882 );
883 }
884
885 #[test]
886 fn test_validate_success() {
887 let task = Task::new(
888 "TASK-1".to_string(),
889 "Valid title".to_string(),
890 "Valid description".to_string(),
891 );
892 assert!(task.validate().is_ok());
893 }
894
895 #[test]
896 fn test_validate_multiple_errors() {
897 let mut task = Task::new("TASK@INVALID".to_string(), "".to_string(), "A".repeat(5001));
898 task.complexity = 100; let result = task.validate();
901 assert!(result.is_err());
902 let errors = result.unwrap_err();
903 assert_eq!(errors.len(), 4);
904 assert!(errors.iter().any(|e| e.contains("ID")));
905 assert!(errors.iter().any(|e| e.contains("title")));
906 assert!(errors.iter().any(|e| e.contains("description")));
907 assert!(errors.iter().any(|e| e.contains("Complexity")));
908 }
909
910 #[test]
912 fn test_cross_tag_dependency_met() {
913 let mut task_a = Task::new(
914 "auth:1".to_string(),
915 "Auth task".to_string(),
916 "Desc".to_string(),
917 );
918 task_a.set_status(TaskStatus::Done);
919
920 let mut task_b = Task::new(
921 "api:1".to_string(),
922 "API task".to_string(),
923 "Desc".to_string(),
924 );
925 task_b.dependencies = vec!["auth:1".to_string()];
926
927 let all_tasks = vec![&task_a, &task_b];
928 assert!(task_b.has_dependencies_met_refs(&all_tasks));
929 }
930
931 #[test]
932 fn test_cross_tag_dependency_not_met() {
933 let task_a = Task::new(
934 "auth:1".to_string(),
935 "Auth task".to_string(),
936 "Desc".to_string(),
937 );
938 let mut task_b = Task::new(
941 "api:1".to_string(),
942 "API task".to_string(),
943 "Desc".to_string(),
944 );
945 task_b.dependencies = vec!["auth:1".to_string()];
946
947 let all_tasks = vec![&task_a, &task_b];
948 assert!(!task_b.has_dependencies_met_refs(&all_tasks));
949 }
950
951 #[test]
952 fn test_local_dependency_still_works_with_refs() {
953 let mut task_a = Task::new("1".to_string(), "First".to_string(), "Desc".to_string());
954 task_a.set_status(TaskStatus::Done);
955
956 let mut task_b = Task::new("2".to_string(), "Second".to_string(), "Desc".to_string());
957 task_b.dependencies = vec!["1".to_string()];
958
959 let all_tasks = vec![&task_a, &task_b];
960 assert!(task_b.has_dependencies_met_refs(&all_tasks));
961 }
962
963 #[test]
964 fn test_has_dependencies_met_refs_missing_dependency() {
965 let mut task = Task::new("api:1".to_string(), "Test".to_string(), "Desc".to_string());
966 task.dependencies = vec!["auth:1".to_string(), "auth:MISSING".to_string()];
967
968 let mut dep1 = Task::new(
969 "auth:1".to_string(),
970 "Dep 1".to_string(),
971 "Desc".to_string(),
972 );
973 dep1.set_status(TaskStatus::Done);
974
975 let all_tasks = vec![&dep1];
976 assert!(!task.has_dependencies_met_refs(&all_tasks));
977 }
978
979 #[test]
980 fn test_subtask_inherits_parent_dependencies() {
981 let mut parent = Task::new(
983 "main:9".to_string(),
984 "Parent Task".to_string(),
985 "Desc".to_string(),
986 );
987 parent.dependencies = vec!["terminal:4".to_string()];
988 parent.status = TaskStatus::Expanded;
989 parent.subtasks = vec!["main:9.1".to_string()];
990
991 let mut subtask = Task::new(
993 "main:9.1".to_string(),
994 "Subtask".to_string(),
995 "Desc".to_string(),
996 );
997 subtask.parent_id = Some("main:9".to_string());
998 let terminal_task = Task::new(
1002 "terminal:4".to_string(),
1003 "Terminal Task".to_string(),
1004 "Desc".to_string(),
1005 );
1006
1007 let all_tasks = vec![&parent, &subtask, &terminal_task];
1008
1009 let effective_deps = subtask.get_effective_dependencies(&all_tasks);
1011 assert!(
1012 effective_deps.contains(&"terminal:4".to_string()),
1013 "Subtask should inherit parent's cross-tag dependency"
1014 );
1015
1016 assert!(
1018 !subtask.has_dependencies_met_refs(&all_tasks),
1019 "Subtask should be blocked when inherited dependency is not met"
1020 );
1021 }
1022
1023 #[test]
1024 fn test_subtask_inherits_parent_dependencies_met() {
1025 let mut parent = Task::new(
1027 "main:9".to_string(),
1028 "Parent Task".to_string(),
1029 "Desc".to_string(),
1030 );
1031 parent.dependencies = vec!["terminal:4".to_string()];
1032 parent.status = TaskStatus::Expanded;
1033 parent.subtasks = vec!["main:9.1".to_string()];
1034
1035 let mut subtask = Task::new(
1037 "main:9.1".to_string(),
1038 "Subtask".to_string(),
1039 "Desc".to_string(),
1040 );
1041 subtask.parent_id = Some("main:9".to_string());
1042
1043 let mut terminal_task = Task::new(
1045 "terminal:4".to_string(),
1046 "Terminal Task".to_string(),
1047 "Desc".to_string(),
1048 );
1049 terminal_task.set_status(TaskStatus::Done);
1050
1051 let all_tasks = vec![&parent, &subtask, &terminal_task];
1052
1053 assert!(
1055 subtask.has_dependencies_met_refs(&all_tasks),
1056 "Subtask should be available when inherited dependency is met"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_get_effective_dependencies_no_parent() {
1062 let mut task = Task::new("1".to_string(), "Task".to_string(), "Desc".to_string());
1063 task.dependencies = vec!["2".to_string(), "3".to_string()];
1064
1065 let all_tasks: Vec<&Task> = vec![&task];
1066 let effective = task.get_effective_dependencies(&all_tasks);
1067
1068 assert_eq!(effective, vec!["2".to_string(), "3".to_string()]);
1069 }
1070
1071 #[test]
1072 fn test_get_effective_dependencies_deduplication() {
1073 let mut parent = Task::new(
1075 "parent".to_string(),
1076 "Parent".to_string(),
1077 "Desc".to_string(),
1078 );
1079 parent.dependencies = vec!["A".to_string(), "B".to_string()];
1080 parent.subtasks = vec!["child".to_string()];
1081
1082 let mut child = Task::new("child".to_string(), "Child".to_string(), "Desc".to_string());
1084 child.parent_id = Some("parent".to_string());
1085 child.dependencies = vec!["B".to_string(), "C".to_string()];
1086
1087 let all_tasks = vec![&parent, &child];
1088 let effective = child.get_effective_dependencies(&all_tasks);
1089
1090 assert_eq!(effective.len(), 3);
1092 assert!(effective.contains(&"A".to_string()));
1093 assert!(effective.contains(&"B".to_string()));
1094 assert!(effective.contains(&"C".to_string()));
1095 }
1096}