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