From e05e9cfd90388209bb569fa0ebf34d17ae43ed23 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Thu, 2 Apr 2026 18:28:13 +0200 Subject: [PATCH] test(todo): add comprehensive unit tests for todo features Add tests for: - @Done insertion at various line positions - Future task filtering with show_future flag - Task sorting by moment ascending - Error message formatting for all new error variants - Trailing newline preservation --- src/cli/commands/todo.rs | 127 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/cli/commands/todo.rs b/src/cli/commands/todo.rs index 2102050..d443203 100644 --- a/src/cli/commands/todo.rs +++ b/src/cli/commands/todo.rs @@ -170,6 +170,26 @@ pub fn run() -> Result<(), StreamdError> { #[cfg(test)] mod tests { + use super::*; + use chrono::{Duration, TimeZone}; + use indexmap::IndexMap; + + fn make_task_shard(moment: chrono::DateTime, file: &str) -> LocalizedShard { + let mut location = IndexMap::new(); + location.insert("file".to_string(), file.to_string()); + location.insert("task".to_string(), "open".to_string()); + + LocalizedShard { + markers: vec!["Task".to_string()], + tags: vec![], + start_line: 1, + end_line: 1, + moment, + location, + children: vec![], + } + } + #[test] fn test_insert_done_after_task() { let line = "Some content @Task with more text"; @@ -184,6 +204,13 @@ mod tests { assert_eq!(result, "Some content @Task @Done"); } + #[test] + fn test_insert_done_only_first_task() { + let line = "Some @Task content @Task again"; + let result = line.replacen("@Task", "@Task @Done", 1); + assert_eq!(result, "Some @Task @Done content @Task again"); + } + #[test] fn test_task_count_single() { let line = "Some content @Task with more text"; @@ -201,4 +228,104 @@ mod tests { let line = "Some content without task marker"; assert_eq!(line.matches("@Task").count(), 0); } + + #[test] + fn test_filter_future_tasks_excludes_future_when_show_future_false() { + let now = Utc::now(); + let past = now - Duration::hours(1); + let future = now + Duration::hours(1); + + let tasks = vec![ + make_task_shard(past, "past.md"), + make_task_shard(future, "future.md"), + ]; + + let filtered: Vec<_> = tasks + .into_iter() + .filter(|shard| shard.moment <= now) + .collect(); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].location.get("file").unwrap(), "past.md"); + } + + #[test] + fn test_filter_future_tasks_includes_all_when_show_future_true() { + let now = Utc::now(); + let past = now - Duration::hours(1); + let future = now + Duration::hours(1); + + let tasks = vec![ + make_task_shard(past, "past.md"), + make_task_shard(future, "future.md"), + ]; + + let filtered: Vec<_> = tasks.into_iter().filter(|_| true).collect(); + + assert_eq!(filtered.len(), 2); + } + + #[test] + fn test_sort_tasks_by_moment_ascending() { + let oldest = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); + let middle = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(); + let newest = Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(); + + let mut tasks = vec![ + make_task_shard(newest, "newest.md"), + make_task_shard(oldest, "oldest.md"), + make_task_shard(middle, "middle.md"), + ]; + + tasks.sort_by(|a, b| a.moment.cmp(&b.moment)); + + assert_eq!(tasks[0].location.get("file").unwrap(), "oldest.md"); + assert_eq!(tasks[1].location.get("file").unwrap(), "middle.md"); + assert_eq!(tasks[2].location.get("file").unwrap(), "newest.md"); + } + + #[test] + fn test_preserve_trailing_newline() { + let content_with_newline = "line1\nline2\n"; + let content_without_newline = "line1\nline2"; + + assert!(content_with_newline.ends_with('\n')); + assert!(!content_without_newline.ends_with('\n')); + } + + #[test] + fn test_invalid_task_number_zero() { + let result = StreamdError::InvalidTaskNumber(0, 5); + assert_eq!( + result.to_string(), + "Invalid task number 0: only 5 tasks available" + ); + } + + #[test] + fn test_invalid_task_number_exceeds_count() { + let result = StreamdError::InvalidTaskNumber(10, 3); + assert_eq!( + result.to_string(), + "Invalid task number 10: only 3 tasks available" + ); + } + + #[test] + fn test_multiple_task_markers_error_message() { + let result = StreamdError::MultipleTaskMarkers("/path/file.md".to_string(), 42); + assert_eq!( + result.to_string(), + "Multiple @Task markers found in /path/file.md:42 - cannot auto-insert @Done" + ); + } + + #[test] + fn test_no_task_marker_error_message() { + let result = StreamdError::NoTaskMarker("/path/file.md".to_string(), 42); + assert_eq!( + result.to_string(), + "No @Task marker found in /path/file.md:42" + ); + } }