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