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 has_dependencies_met_refs(&self, all_tasks: &[&Task]) -> bool {
300 self.dependencies.iter().all(|dep_id| {
301 all_tasks
302 .iter()
303 .find(|t| &t.id == dep_id)
304 .map(|t| t.status == TaskStatus::Done)
305 .unwrap_or(false)
306 })
307 }
308
309 pub fn needs_expansion(&self) -> bool {
313 self.complexity >= 3 && !self.is_expanded() && !self.is_subtask()
314 }
315
316 pub fn recommended_subtasks(&self) -> usize {
324 Self::recommended_subtasks_for_complexity(self.complexity)
325 }
326
327 pub fn recommended_subtasks_for_complexity(complexity: u32) -> usize {
329 match complexity {
330 0..=2 => 2,
331 3 => 3,
332 5 => 4,
333 8 => 5,
334 13 => 6,
335 _ => 8, }
337 }
338
339 pub fn assign(&mut self, assignee: &str) {
341 self.assigned_to = Some(assignee.to_string());
342 self.update();
343 }
344
345 pub fn is_assigned_to(&self, assignee: &str) -> bool {
346 self.assigned_to
347 .as_ref()
348 .map(|s| s == assignee)
349 .unwrap_or(false)
350 }
351
352 pub fn would_create_cycle(&self, new_dep_id: &str, all_tasks: &[Task]) -> Result<(), String> {
355 if self.id == new_dep_id {
356 return Err(format!("Self-reference: {} -> {}", self.id, new_dep_id));
357 }
358
359 let mut visited = std::collections::HashSet::new();
360 let mut path = Vec::new();
361
362 Self::detect_cycle_recursive(new_dep_id, &self.id, all_tasks, &mut visited, &mut path)
363 }
364
365 fn detect_cycle_recursive(
366 current_id: &str,
367 target_id: &str,
368 all_tasks: &[Task],
369 visited: &mut std::collections::HashSet<String>,
370 path: &mut Vec<String>,
371 ) -> Result<(), String> {
372 if current_id == target_id {
373 path.push(current_id.to_string());
374 return Err(format!("Circular dependency: {}", path.join(" -> ")));
375 }
376
377 if visited.contains(current_id) {
378 return Ok(());
379 }
380
381 visited.insert(current_id.to_string());
382 path.push(current_id.to_string());
383
384 if let Some(task) = all_tasks.iter().find(|t| t.id == current_id) {
385 for dep_id in &task.dependencies {
386 Self::detect_cycle_recursive(dep_id, target_id, all_tasks, visited, path)?;
387 }
388 }
389
390 path.pop();
391 Ok(())
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_task_creation() {
401 let task = Task::new(
402 "TASK-1".to_string(),
403 "Test Task".to_string(),
404 "Description".to_string(),
405 );
406
407 assert_eq!(task.id, "TASK-1");
408 assert_eq!(task.title, "Test Task");
409 assert_eq!(task.description, "Description");
410 assert_eq!(task.status, TaskStatus::Pending);
411 assert_eq!(task.complexity, 0);
412 assert_eq!(task.priority, Priority::Medium);
413 assert!(task.dependencies.is_empty());
414 assert!(task.created_at.is_some());
415 assert!(task.updated_at.is_some());
416 assert!(task.assigned_to.is_none());
417 }
418
419 #[test]
420 fn test_status_conversion() {
421 assert_eq!(TaskStatus::Pending.as_str(), "pending");
422 assert_eq!(TaskStatus::InProgress.as_str(), "in-progress");
423 assert_eq!(TaskStatus::Done.as_str(), "done");
424 assert_eq!(TaskStatus::Review.as_str(), "review");
425 assert_eq!(TaskStatus::Blocked.as_str(), "blocked");
426 assert_eq!(TaskStatus::Deferred.as_str(), "deferred");
427 assert_eq!(TaskStatus::Cancelled.as_str(), "cancelled");
428 }
429
430 #[test]
431 fn test_status_from_string() {
432 assert_eq!(TaskStatus::from_str("pending"), Some(TaskStatus::Pending));
433 assert_eq!(
434 TaskStatus::from_str("in-progress"),
435 Some(TaskStatus::InProgress)
436 );
437 assert_eq!(TaskStatus::from_str("done"), Some(TaskStatus::Done));
438 assert_eq!(TaskStatus::from_str("invalid"), None);
439 }
440
441 #[test]
442 fn test_set_status_updates_timestamp() {
443 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
444 let initial_updated = task.updated_at.clone();
445
446 std::thread::sleep(std::time::Duration::from_millis(10));
447 task.set_status(TaskStatus::InProgress);
448
449 assert_eq!(task.status, TaskStatus::InProgress);
450 assert!(task.updated_at > initial_updated);
451 }
452
453 #[test]
454 fn test_task_assignment() {
455 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
456
457 task.assign("alice");
458 assert_eq!(task.assigned_to, Some("alice".to_string()));
459 assert!(task.is_assigned_to("alice"));
460 assert!(!task.is_assigned_to("bob"));
461 }
462
463 #[test]
464 fn test_has_dependencies_met_all_done() {
465 let mut task = Task::new("TASK-3".to_string(), "Test".to_string(), "Desc".to_string());
466 task.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
467
468 let mut task1 = Task::new(
469 "TASK-1".to_string(),
470 "Dep 1".to_string(),
471 "Desc".to_string(),
472 );
473 task1.set_status(TaskStatus::Done);
474
475 let mut task2 = Task::new(
476 "TASK-2".to_string(),
477 "Dep 2".to_string(),
478 "Desc".to_string(),
479 );
480 task2.set_status(TaskStatus::Done);
481
482 let all_tasks = vec![task1, task2];
483 assert!(task.has_dependencies_met(&all_tasks));
484 }
485
486 #[test]
487 fn test_has_dependencies_met_some_pending() {
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 task2 = Task::new(
499 "TASK-2".to_string(),
500 "Dep 2".to_string(),
501 "Desc".to_string(),
502 );
503 let all_tasks = vec![task1, task2];
506 assert!(!task.has_dependencies_met(&all_tasks));
507 }
508
509 #[test]
510 fn test_has_dependencies_met_missing_dependency() {
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-MISSING".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 all_tasks = vec![task1];
522 assert!(!task.has_dependencies_met(&all_tasks));
523 }
524
525 #[test]
526 fn test_needs_expansion() {
527 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
528
529 task.complexity = 1;
531 assert!(!task.needs_expansion());
532
533 task.complexity = 2;
534 assert!(!task.needs_expansion());
535
536 task.complexity = 3;
538 assert!(task.needs_expansion());
539
540 task.complexity = 8;
541 assert!(task.needs_expansion());
542
543 task.complexity = 13;
544 assert!(task.needs_expansion());
545
546 task.complexity = 21;
547 assert!(task.needs_expansion());
548
549 task.status = TaskStatus::Expanded;
551 assert!(!task.needs_expansion());
552
553 task.status = TaskStatus::Pending;
555 task.parent_id = Some("parent:1".to_string());
556 assert!(!task.needs_expansion()); task.parent_id = None;
560 task.subtasks = vec!["TASK-1.1".to_string()];
561 assert!(!task.needs_expansion()); }
563
564 #[test]
565 fn test_task_serialization() {
566 let task = Task::new(
567 "TASK-1".to_string(),
568 "Test Task".to_string(),
569 "Description".to_string(),
570 );
571
572 let json = serde_json::to_string(&task).unwrap();
573 let deserialized: Task = serde_json::from_str(&json).unwrap();
574
575 assert_eq!(task.id, deserialized.id);
576 assert_eq!(task.title, deserialized.title);
577 assert_eq!(task.description, deserialized.description);
578 }
579
580 #[test]
581 fn test_task_serialization_with_optional_fields() {
582 let mut task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
583 task.details = Some("Detailed info".to_string());
584 task.test_strategy = Some("Test plan".to_string());
585 task.assign("alice");
586
587 let json = serde_json::to_string(&task).unwrap();
588 let deserialized: Task = serde_json::from_str(&json).unwrap();
589
590 assert_eq!(task.details, deserialized.details);
591 assert_eq!(task.test_strategy, deserialized.test_strategy);
592 assert_eq!(task.assigned_to, deserialized.assigned_to);
593 }
594
595 #[test]
596 fn test_priority_default() {
597 let default_priority = Priority::default();
598 assert_eq!(default_priority, Priority::Medium);
599 }
600
601 #[test]
602 fn test_status_all() {
603 let all_statuses = TaskStatus::all();
604 assert_eq!(all_statuses.len(), 8);
605 assert!(all_statuses.contains(&"pending"));
606 assert!(all_statuses.contains(&"in-progress"));
607 assert!(all_statuses.contains(&"done"));
608 assert!(all_statuses.contains(&"review"));
609 assert!(all_statuses.contains(&"blocked"));
610 assert!(all_statuses.contains(&"deferred"));
611 assert!(all_statuses.contains(&"cancelled"));
612 assert!(all_statuses.contains(&"expanded"));
613 }
614
615 #[test]
616 fn test_circular_dependency_self_reference() {
617 let task = Task::new("TASK-1".to_string(), "Test".to_string(), "Desc".to_string());
618 let all_tasks = vec![task.clone()];
619
620 let result = task.would_create_cycle("TASK-1", &all_tasks);
621 assert!(result.is_err());
622 assert!(result.unwrap_err().contains("Self-reference"));
623 }
624
625 #[test]
626 fn test_circular_dependency_direct_cycle() {
627 let mut task1 = Task::new(
628 "TASK-1".to_string(),
629 "Task 1".to_string(),
630 "Desc".to_string(),
631 );
632 task1.dependencies = vec!["TASK-2".to_string()];
633
634 let task2 = Task::new(
635 "TASK-2".to_string(),
636 "Task 2".to_string(),
637 "Desc".to_string(),
638 );
639
640 let all_tasks = vec![task1.clone(), task2.clone()];
641
642 let result = task2.would_create_cycle("TASK-1", &all_tasks);
644 assert!(result.is_err());
645 assert!(result.unwrap_err().contains("Circular dependency"));
646 }
647
648 #[test]
649 fn test_circular_dependency_indirect_cycle() {
650 let mut task1 = Task::new(
651 "TASK-1".to_string(),
652 "Task 1".to_string(),
653 "Desc".to_string(),
654 );
655 task1.dependencies = vec!["TASK-2".to_string()];
656
657 let mut task2 = Task::new(
658 "TASK-2".to_string(),
659 "Task 2".to_string(),
660 "Desc".to_string(),
661 );
662 task2.dependencies = vec!["TASK-3".to_string()];
663
664 let task3 = Task::new(
665 "TASK-3".to_string(),
666 "Task 3".to_string(),
667 "Desc".to_string(),
668 );
669
670 let all_tasks = vec![task1.clone(), task2, task3.clone()];
671
672 let result = task3.would_create_cycle("TASK-1", &all_tasks);
675 assert!(result.is_err());
676 assert!(result.unwrap_err().contains("Circular dependency"));
677 }
678
679 #[test]
680 fn test_circular_dependency_no_cycle() {
681 let mut task1 = Task::new(
682 "TASK-1".to_string(),
683 "Task 1".to_string(),
684 "Desc".to_string(),
685 );
686 task1.dependencies = vec!["TASK-3".to_string()];
687
688 let task2 = Task::new(
689 "TASK-2".to_string(),
690 "Task 2".to_string(),
691 "Desc".to_string(),
692 );
693
694 let task3 = Task::new(
695 "TASK-3".to_string(),
696 "Task 3".to_string(),
697 "Desc".to_string(),
698 );
699
700 let all_tasks = vec![task1.clone(), task2.clone(), task3];
701
702 let result = task1.would_create_cycle("TASK-2", &all_tasks);
704 assert!(result.is_ok());
705 }
706
707 #[test]
708 fn test_circular_dependency_complex_graph() {
709 let mut task1 = Task::new(
710 "TASK-1".to_string(),
711 "Task 1".to_string(),
712 "Desc".to_string(),
713 );
714 task1.dependencies = vec!["TASK-2".to_string(), "TASK-3".to_string()];
715
716 let mut task2 = Task::new(
717 "TASK-2".to_string(),
718 "Task 2".to_string(),
719 "Desc".to_string(),
720 );
721 task2.dependencies = vec!["TASK-4".to_string()];
722
723 let mut task3 = Task::new(
724 "TASK-3".to_string(),
725 "Task 3".to_string(),
726 "Desc".to_string(),
727 );
728 task3.dependencies = vec!["TASK-4".to_string()];
729
730 let task4 = Task::new(
731 "TASK-4".to_string(),
732 "Task 4".to_string(),
733 "Desc".to_string(),
734 );
735
736 let all_tasks = vec![task1.clone(), task2, task3, task4.clone()];
737
738 let result = task4.would_create_cycle("TASK-1", &all_tasks);
740 assert!(result.is_err());
741 assert!(result.unwrap_err().contains("Circular dependency"));
742 }
743
744 #[test]
746 fn test_validate_id_success() {
747 assert!(Task::validate_id("TASK-123").is_ok());
748 assert!(Task::validate_id("task_456").is_ok());
749 assert!(Task::validate_id("Feature-789").is_ok());
750 assert!(Task::validate_id("phase1:10").is_ok());
752 assert!(Task::validate_id("phase1:10.1").is_ok());
753 assert!(Task::validate_id("my-epic:subtask-1.2.3").is_ok());
754 }
755
756 #[test]
757 fn test_validate_id_empty() {
758 let result = Task::validate_id("");
759 assert!(result.is_err());
760 assert_eq!(result.unwrap_err(), "Task ID cannot be empty");
761 }
762
763 #[test]
764 fn test_validate_id_too_long() {
765 let long_id = "A".repeat(101);
766 let result = Task::validate_id(&long_id);
767 assert!(result.is_err());
768 assert!(result.unwrap_err().contains("too long"));
769 }
770
771 #[test]
772 fn test_validate_id_invalid_characters() {
773 assert!(Task::validate_id("TASK@123").is_err());
774 assert!(Task::validate_id("TASK 123").is_err());
775 assert!(Task::validate_id("TASK#123").is_err());
776 assert!(Task::validate_id("TASK.123").is_ok()); assert!(Task::validate_id("epic:TASK-1").is_ok()); }
780
781 #[test]
782 fn test_validate_title_success() {
783 assert!(Task::validate_title("Valid title").is_ok());
784 assert!(Task::validate_title("A").is_ok());
785 }
786
787 #[test]
788 fn test_validate_title_empty() {
789 let result = Task::validate_title("");
790 assert!(result.is_err());
791 assert_eq!(result.unwrap_err(), "Task title cannot be empty");
792
793 let result = Task::validate_title(" ");
794 assert!(result.is_err());
795 assert_eq!(result.unwrap_err(), "Task title cannot be empty");
796 }
797
798 #[test]
799 fn test_validate_title_too_long() {
800 let long_title = "A".repeat(201);
801 let result = Task::validate_title(&long_title);
802 assert!(result.is_err());
803 assert!(result.unwrap_err().contains("too long"));
804 }
805
806 #[test]
807 fn test_validate_description_success() {
808 assert!(Task::validate_description("Valid description").is_ok());
809 assert!(Task::validate_description("").is_ok());
810 }
811
812 #[test]
813 fn test_validate_description_too_long() {
814 let long_desc = "A".repeat(5001);
815 let result = Task::validate_description(&long_desc);
816 assert!(result.is_err());
817 assert!(result.unwrap_err().contains("too long"));
818 }
819
820 #[test]
821 fn test_validate_complexity_success() {
822 assert!(Task::validate_complexity(0).is_ok());
823 assert!(Task::validate_complexity(1).is_ok());
824 assert!(Task::validate_complexity(2).is_ok());
825 assert!(Task::validate_complexity(3).is_ok());
826 assert!(Task::validate_complexity(5).is_ok());
827 assert!(Task::validate_complexity(8).is_ok());
828 assert!(Task::validate_complexity(13).is_ok());
829 assert!(Task::validate_complexity(21).is_ok());
830 }
831
832 #[test]
833 fn test_validate_complexity_invalid() {
834 assert!(Task::validate_complexity(4).is_err());
835 assert!(Task::validate_complexity(6).is_err());
836 assert!(Task::validate_complexity(7).is_err());
837 assert!(Task::validate_complexity(100).is_err());
838 }
839
840 #[test]
841 fn test_sanitize_text() {
842 assert_eq!(
843 Task::sanitize_text("<script>alert('xss')</script>"),
844 "<script>alert('xss')</script>"
845 );
846 assert_eq!(Task::sanitize_text("Normal text"), "Normal text");
847 assert_eq!(
848 Task::sanitize_text("<div>Content</div>"),
849 "<div>Content</div>"
850 );
851 }
852
853 #[test]
854 fn test_validate_success() {
855 let task = Task::new(
856 "TASK-1".to_string(),
857 "Valid title".to_string(),
858 "Valid description".to_string(),
859 );
860 assert!(task.validate().is_ok());
861 }
862
863 #[test]
864 fn test_validate_multiple_errors() {
865 let mut task = Task::new("TASK@INVALID".to_string(), "".to_string(), "A".repeat(5001));
866 task.complexity = 100; let result = task.validate();
869 assert!(result.is_err());
870 let errors = result.unwrap_err();
871 assert_eq!(errors.len(), 4);
872 assert!(errors.iter().any(|e| e.contains("ID")));
873 assert!(errors.iter().any(|e| e.contains("title")));
874 assert!(errors.iter().any(|e| e.contains("description")));
875 assert!(errors.iter().any(|e| e.contains("Complexity")));
876 }
877
878 #[test]
880 fn test_cross_tag_dependency_met() {
881 let mut task_a = Task::new(
882 "auth:1".to_string(),
883 "Auth task".to_string(),
884 "Desc".to_string(),
885 );
886 task_a.set_status(TaskStatus::Done);
887
888 let mut task_b = Task::new(
889 "api:1".to_string(),
890 "API task".to_string(),
891 "Desc".to_string(),
892 );
893 task_b.dependencies = vec!["auth:1".to_string()];
894
895 let all_tasks = vec![&task_a, &task_b];
896 assert!(task_b.has_dependencies_met_refs(&all_tasks));
897 }
898
899 #[test]
900 fn test_cross_tag_dependency_not_met() {
901 let task_a = Task::new(
902 "auth:1".to_string(),
903 "Auth task".to_string(),
904 "Desc".to_string(),
905 );
906 let mut task_b = Task::new(
909 "api:1".to_string(),
910 "API task".to_string(),
911 "Desc".to_string(),
912 );
913 task_b.dependencies = vec!["auth:1".to_string()];
914
915 let all_tasks = vec![&task_a, &task_b];
916 assert!(!task_b.has_dependencies_met_refs(&all_tasks));
917 }
918
919 #[test]
920 fn test_local_dependency_still_works_with_refs() {
921 let mut task_a = Task::new("1".to_string(), "First".to_string(), "Desc".to_string());
922 task_a.set_status(TaskStatus::Done);
923
924 let mut task_b = Task::new("2".to_string(), "Second".to_string(), "Desc".to_string());
925 task_b.dependencies = vec!["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_has_dependencies_met_refs_missing_dependency() {
933 let mut task = Task::new("api:1".to_string(), "Test".to_string(), "Desc".to_string());
934 task.dependencies = vec!["auth:1".to_string(), "auth:MISSING".to_string()];
935
936 let mut dep1 = Task::new(
937 "auth:1".to_string(),
938 "Dep 1".to_string(),
939 "Desc".to_string(),
940 );
941 dep1.set_status(TaskStatus::Done);
942
943 let all_tasks = vec![&dep1];
944 assert!(!task.has_dependencies_met_refs(&all_tasks));
945 }
946}