ricecoder_tools/
todo.rs

1//! Todo tools for managing task lists
2//!
3//! Provides functionality to create, read, and update todos with persistent storage.
4
5use crate::error::ToolError;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use tracing::{debug, error, info};
10
11/// Todo status enumeration
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum TodoStatus {
15    /// Todo is pending
16    Pending,
17    /// Todo is in progress
18    InProgress,
19    /// Todo is completed
20    Completed,
21    /// Todo is blocked
22    Blocked,
23}
24
25impl std::fmt::Display for TodoStatus {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            TodoStatus::Pending => write!(f, "pending"),
29            TodoStatus::InProgress => write!(f, "in_progress"),
30            TodoStatus::Completed => write!(f, "completed"),
31            TodoStatus::Blocked => write!(f, "blocked"),
32        }
33    }
34}
35
36impl std::str::FromStr for TodoStatus {
37    type Err = ToolError;
38
39    fn from_str(s: &str) -> Result<Self, Self::Err> {
40        match s.to_lowercase().as_str() {
41            "pending" => Ok(TodoStatus::Pending),
42            "in_progress" => Ok(TodoStatus::InProgress),
43            "completed" => Ok(TodoStatus::Completed),
44            "blocked" => Ok(TodoStatus::Blocked),
45            _ => Err(ToolError::new(
46                "INVALID_STATUS",
47                format!("Invalid todo status: {}", s),
48            )
49            .with_suggestion("Use one of: pending, in_progress, completed, blocked")),
50        }
51    }
52}
53
54/// Todo priority enumeration
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum TodoPriority {
58    /// Low priority
59    Low,
60    /// Medium priority
61    Medium,
62    /// High priority
63    High,
64    /// Critical priority
65    Critical,
66}
67
68impl std::fmt::Display for TodoPriority {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            TodoPriority::Low => write!(f, "low"),
72            TodoPriority::Medium => write!(f, "medium"),
73            TodoPriority::High => write!(f, "high"),
74            TodoPriority::Critical => write!(f, "critical"),
75        }
76    }
77}
78
79impl std::str::FromStr for TodoPriority {
80    type Err = ToolError;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        match s.to_lowercase().as_str() {
84            "low" => Ok(TodoPriority::Low),
85            "medium" => Ok(TodoPriority::Medium),
86            "high" => Ok(TodoPriority::High),
87            "critical" => Ok(TodoPriority::Critical),
88            _ => Err(ToolError::new(
89                "INVALID_PRIORITY",
90                format!("Invalid todo priority: {}", s),
91            )
92            .with_suggestion("Use one of: low, medium, high, critical")),
93        }
94    }
95}
96
97/// A single todo item
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99pub struct Todo {
100    /// Unique identifier for the todo
101    pub id: String,
102    /// Todo title (required, non-empty)
103    pub title: String,
104    /// Todo description (optional)
105    pub description: Option<String>,
106    /// Current status of the todo
107    pub status: TodoStatus,
108    /// Priority level of the todo
109    pub priority: TodoPriority,
110}
111
112impl Todo {
113    /// Create a new todo with validation
114    pub fn new(
115        id: impl Into<String>,
116        title: impl Into<String>,
117        status: TodoStatus,
118        priority: TodoPriority,
119    ) -> Result<Self, ToolError> {
120        let title = title.into();
121
122        // Validate title is non-empty
123        if title.trim().is_empty() {
124            return Err(ToolError::new("INVALID_TITLE", "Todo title cannot be empty")
125                .with_suggestion("Provide a non-empty title for the todo"));
126        }
127
128        Ok(Self {
129            id: id.into(),
130            title,
131            description: None,
132            status,
133            priority,
134        })
135    }
136
137    /// Set the description
138    pub fn with_description(mut self, description: impl Into<String>) -> Self {
139        self.description = Some(description.into());
140        self
141    }
142}
143
144/// Input for todowrite operation
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct TodowriteInput {
147    /// List of todos to create or update
148    pub todos: Vec<Todo>,
149}
150
151/// Output from todowrite operation
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct TodowriteOutput {
154    /// Number of todos created
155    pub created: usize,
156    /// Number of todos updated
157    pub updated: usize,
158}
159
160/// Input for todoread operation
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TodoreadInput {
163    /// Optional filter by status
164    pub status_filter: Option<TodoStatus>,
165    /// Optional filter by priority
166    pub priority_filter: Option<TodoPriority>,
167}
168
169/// Output from todoread operation
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct TodoreadOutput {
172    /// List of todos matching the filters
173    pub todos: Vec<Todo>,
174}
175
176/// Todo storage manager
177pub struct TodoStorage {
178    storage_path: PathBuf,
179}
180
181impl TodoStorage {
182    /// Create a new todo storage manager
183    pub fn new(storage_path: impl Into<PathBuf>) -> Self {
184        Self {
185            storage_path: storage_path.into(),
186        }
187    }
188
189    /// Get the default storage path (~/.ricecoder/todos.json)
190    pub fn default_path() -> Result<PathBuf, ToolError> {
191        if let Some(home_dir) = dirs::home_dir() {
192            Ok(home_dir.join(".ricecoder").join("todos.json"))
193        } else {
194            Err(ToolError::new(
195                "HOME_DIR_NOT_FOUND",
196                "Could not determine home directory",
197            )
198            .with_suggestion("Set the HOME environment variable"))
199        }
200    }
201
202    /// Load todos from storage
203    pub fn load_todos(&self) -> Result<HashMap<String, Todo>, ToolError> {
204        debug!("Loading todos from: {:?}", self.storage_path);
205
206        // If file doesn't exist, return empty map
207        if !self.storage_path.exists() {
208            debug!("Todo storage file does not exist, returning empty todos");
209            return Ok(HashMap::new());
210        }
211
212        // Read file
213        let content = std::fs::read_to_string(&self.storage_path).map_err(|e| {
214            error!("Failed to read todo storage file: {}", e);
215            ToolError::from(e)
216                .with_details(format!("Failed to read: {:?}", self.storage_path))
217                .with_suggestion("Check file permissions and ensure the file is readable")
218        })?;
219
220        // Parse JSON
221        let todos: Vec<Todo> = serde_json::from_str(&content).map_err(|e| {
222            error!("Failed to parse todo storage file: {}", e);
223            ToolError::from(e)
224                .with_details("Todo storage file contains invalid JSON")
225                .with_suggestion("Check the file format or restore from backup")
226        })?;
227
228        // Convert to HashMap
229        let mut map = HashMap::new();
230        for todo in todos {
231            map.insert(todo.id.clone(), todo);
232        }
233
234        info!("Loaded {} todos from storage", map.len());
235        Ok(map)
236    }
237
238    /// Save todos to storage (atomic write)
239    pub fn save_todos(&self, todos: &HashMap<String, Todo>) -> Result<(), ToolError> {
240        debug!("Saving {} todos to: {:?}", todos.len(), self.storage_path);
241
242        // Create parent directory if it doesn't exist
243        if let Some(parent) = self.storage_path.parent() {
244            std::fs::create_dir_all(parent).map_err(|e| {
245                error!("Failed to create storage directory: {}", e);
246                ToolError::from(e)
247                    .with_details(format!("Failed to create: {:?}", parent))
248                    .with_suggestion("Check directory permissions")
249            })?;
250        }
251
252        // Convert HashMap to Vec for serialization
253        let mut todos_vec: Vec<Todo> = todos.values().cloned().collect();
254        todos_vec.sort_by(|a, b| a.id.cmp(&b.id));
255
256        // Serialize to JSON
257        let json = serde_json::to_string_pretty(&todos_vec).map_err(|e| {
258            error!("Failed to serialize todos: {}", e);
259            ToolError::from(e)
260                .with_details("Failed to serialize todos to JSON")
261                .with_suggestion("Check for circular references or invalid data")
262        })?;
263
264        // Write to temporary file first (atomic write)
265        let temp_path = self.storage_path.with_extension("json.tmp");
266        std::fs::write(&temp_path, &json).map_err(|e| {
267            error!("Failed to write temporary todo file: {}", e);
268            ToolError::from(e)
269                .with_details(format!("Failed to write: {:?}", temp_path))
270                .with_suggestion("Check disk space and file permissions")
271        })?;
272
273        // Rename temporary file to actual file (atomic on most filesystems)
274        std::fs::rename(&temp_path, &self.storage_path).map_err(|e| {
275            error!("Failed to finalize todo storage: {}", e);
276            // Clean up temp file on error
277            let _ = std::fs::remove_file(&temp_path);
278            ToolError::from(e)
279                .with_details(format!(
280                    "Failed to rename: {:?} to {:?}",
281                    temp_path, self.storage_path
282                ))
283                .with_suggestion("Check file permissions and disk space")
284        })?;
285
286        info!("Saved {} todos to storage", todos.len());
287        Ok(())
288    }
289}
290
291/// Todo tools for managing task lists
292pub struct TodoTools {
293    storage: TodoStorage,
294    mcp_provider: Option<std::sync::Arc<dyn crate::Provider>>,
295}
296
297impl TodoTools {
298    /// Create new todo tools with default storage path
299    pub fn new() -> Result<Self, ToolError> {
300        let storage_path = TodoStorage::default_path()?;
301        Ok(Self {
302            storage: TodoStorage::new(storage_path),
303            mcp_provider: None,
304        })
305    }
306
307    /// Create new todo tools with custom storage path
308    pub fn with_storage_path(storage_path: impl Into<PathBuf>) -> Self {
309        Self {
310            storage: TodoStorage::new(storage_path),
311            mcp_provider: None,
312        }
313    }
314
315    /// Set the MCP provider for todo operations
316    pub fn with_mcp_provider(mut self, provider: std::sync::Arc<dyn crate::Provider>) -> Self {
317        self.mcp_provider = Some(provider);
318        self
319    }
320
321    /// Write todos with timeout enforcement (500ms)
322    ///
323    /// Attempts to use MCP provider if available, falls back to built-in implementation.
324    pub async fn write_todos_with_timeout(&self, input: TodowriteInput) -> Result<TodowriteOutput, ToolError> {
325        let timeout_duration = std::time::Duration::from_millis(500);
326        
327        match tokio::time::timeout(timeout_duration, async {
328            self.write_todos_internal(input)
329        }).await {
330            Ok(result) => result,
331            Err(_) => {
332                Err(ToolError::new("TIMEOUT", "Todo write operation exceeded 500ms timeout")
333                    .with_details("Operation took too long to complete")
334                    .with_suggestion("Try again or check system performance"))
335            }
336        }
337    }
338
339    /// Write todos (create or update)
340    ///
341    /// Attempts to use MCP provider if available, falls back to built-in implementation.
342    pub fn write_todos(&self, input: TodowriteInput) -> Result<TodowriteOutput, ToolError> {
343        self.write_todos_internal(input)
344    }
345
346    /// Internal write todos implementation
347    fn write_todos_internal(&self, input: TodowriteInput) -> Result<TodowriteOutput, ToolError> {
348        debug!("Writing {} todos", input.todos.len());
349
350        // Try MCP provider first
351        if let Some(_provider) = &self.mcp_provider {
352            debug!("Attempting to use MCP provider for todowrite");
353            let _input_json = serde_json::to_string(&input).map_err(|e| {
354                error!("Failed to serialize todowrite input: {}", e);
355                ToolError::from(e)
356                    .with_details("Failed to serialize input for MCP provider")
357                    .with_suggestion("Check the input data format")
358            })?;
359
360            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
361                // Note: In a real async context, this would be async
362                // For now, we'll use a synchronous fallback
363                None::<String>
364            })) {
365                Ok(_) => {
366                    debug!("MCP provider not available, falling back to built-in");
367                }
368                Err(_) => {
369                    debug!("MCP provider error, falling back to built-in");
370                }
371            }
372        }
373
374        // Fall back to built-in implementation
375        debug!("Using built-in todowrite implementation");
376
377        // Load existing todos
378        let mut todos = self.storage.load_todos()?;
379
380        let mut created = 0;
381        let mut updated = 0;
382
383        // Process each todo
384        for todo in input.todos {
385            let id = todo.id.clone();
386            if todos.contains_key(&id) {
387                updated += 1;
388            } else {
389                created += 1;
390            }
391            todos.insert(id, todo);
392        }
393
394        // Save todos
395        self.storage.save_todos(&todos)?;
396
397        info!("Wrote todos: {} created, {} updated", created, updated);
398        Ok(TodowriteOutput { created, updated })
399    }
400
401    /// Read todos with timeout enforcement (500ms)
402    ///
403    /// Attempts to use MCP provider if available, falls back to built-in implementation.
404    pub async fn read_todos_with_timeout(&self, input: TodoreadInput) -> Result<TodoreadOutput, ToolError> {
405        let timeout_duration = std::time::Duration::from_millis(500);
406        
407        match tokio::time::timeout(timeout_duration, async {
408            self.read_todos_internal(input)
409        }).await {
410            Ok(result) => result,
411            Err(_) => {
412                Err(ToolError::new("TIMEOUT", "Todo read operation exceeded 500ms timeout")
413                    .with_details("Operation took too long to complete")
414                    .with_suggestion("Try again or check system performance"))
415            }
416        }
417    }
418
419    /// Read todos with optional filtering
420    ///
421    /// Attempts to use MCP provider if available, falls back to built-in implementation.
422    pub fn read_todos(&self, input: TodoreadInput) -> Result<TodoreadOutput, ToolError> {
423        self.read_todos_internal(input)
424    }
425
426    /// Internal read todos implementation
427    fn read_todos_internal(&self, input: TodoreadInput) -> Result<TodoreadOutput, ToolError> {
428        debug!("Reading todos with filters: status={:?}, priority={:?}", 
429               input.status_filter, input.priority_filter);
430
431        // Try MCP provider first
432        if let Some(_provider) = &self.mcp_provider {
433            debug!("Attempting to use MCP provider for todoread");
434            let _input_json = serde_json::to_string(&input).map_err(|e| {
435                error!("Failed to serialize todoread input: {}", e);
436                ToolError::from(e)
437                    .with_details("Failed to serialize input for MCP provider")
438                    .with_suggestion("Check the input data format")
439            })?;
440
441            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
442                // Note: In a real async context, this would be async
443                // For now, we'll use a synchronous fallback
444                None::<String>
445            })) {
446                Ok(_) => {
447                    debug!("MCP provider not available, falling back to built-in");
448                }
449                Err(_) => {
450                    debug!("MCP provider error, falling back to built-in");
451                }
452            }
453        }
454
455        // Fall back to built-in implementation
456        debug!("Using built-in todoread implementation");
457
458        // Load todos
459        let todos = self.storage.load_todos()?;
460
461        // Filter todos
462        let mut filtered: Vec<Todo> = todos
463            .into_values()
464            .filter(|todo| {
465                // Apply status filter
466                if let Some(status) = input.status_filter {
467                    if todo.status != status {
468                        return false;
469                    }
470                }
471
472                // Apply priority filter
473                if let Some(priority) = input.priority_filter {
474                    if todo.priority != priority {
475                        return false;
476                    }
477                }
478
479                true
480            })
481            .collect();
482
483        // Sort by priority (descending) then by id
484        filtered.sort_by(|a, b| {
485            match b.priority.cmp(&a.priority) {
486                std::cmp::Ordering::Equal => a.id.cmp(&b.id),
487                other => other,
488            }
489        });
490
491        info!("Read {} todos (filtered from total)", filtered.len());
492        Ok(TodoreadOutput { todos: filtered })
493    }
494}
495
496impl Default for TodoTools {
497    fn default() -> Self {
498        Self::new().unwrap_or_else(|_| {
499            // Fallback to in-memory storage if default path fails
500            Self::with_storage_path("/tmp/ricecoder-todos.json")
501        })
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use tempfile::TempDir;
509
510    #[test]
511    fn test_todo_creation() {
512        let todo = Todo::new("1", "Test todo", TodoStatus::Pending, TodoPriority::High);
513        assert!(todo.is_ok());
514        let todo = todo.unwrap();
515        assert_eq!(todo.id, "1");
516        assert_eq!(todo.title, "Test todo");
517        assert_eq!(todo.status, TodoStatus::Pending);
518        assert_eq!(todo.priority, TodoPriority::High);
519    }
520
521    #[test]
522    fn test_todo_empty_title_validation() {
523        let result = Todo::new("1", "   ", TodoStatus::Pending, TodoPriority::High);
524        assert!(result.is_err());
525        if let Err(err) = result {
526            assert_eq!(err.code, "INVALID_TITLE");
527        }
528    }
529
530    #[test]
531    fn test_todo_with_description() {
532        let todo = Todo::new("1", "Test", TodoStatus::Pending, TodoPriority::High)
533            .unwrap()
534            .with_description("A test todo");
535        assert_eq!(todo.description, Some("A test todo".to_string()));
536    }
537
538    #[test]
539    fn test_todo_status_parsing() {
540        assert_eq!("pending".parse::<TodoStatus>().unwrap(), TodoStatus::Pending);
541        assert_eq!(
542            "in_progress".parse::<TodoStatus>().unwrap(),
543            TodoStatus::InProgress
544        );
545        assert_eq!(
546            "completed".parse::<TodoStatus>().unwrap(),
547            TodoStatus::Completed
548        );
549        assert_eq!("blocked".parse::<TodoStatus>().unwrap(), TodoStatus::Blocked);
550        assert!("invalid".parse::<TodoStatus>().is_err());
551    }
552
553    #[test]
554    fn test_todo_priority_parsing() {
555        assert_eq!("low".parse::<TodoPriority>().unwrap(), TodoPriority::Low);
556        assert_eq!(
557            "medium".parse::<TodoPriority>().unwrap(),
558            TodoPriority::Medium
559        );
560        assert_eq!("high".parse::<TodoPriority>().unwrap(), TodoPriority::High);
561        assert_eq!(
562            "critical".parse::<TodoPriority>().unwrap(),
563            TodoPriority::Critical
564        );
565        assert!("invalid".parse::<TodoPriority>().is_err());
566    }
567
568    #[test]
569    fn test_todo_storage_load_empty() {
570        let temp_dir = TempDir::new().unwrap();
571        let storage_path = temp_dir.path().join("todos.json");
572        let storage = TodoStorage::new(&storage_path);
573
574        let todos = storage.load_todos().unwrap();
575        assert!(todos.is_empty());
576    }
577
578    #[test]
579    fn test_todo_storage_save_and_load() {
580        let temp_dir = TempDir::new().unwrap();
581        let storage_path = temp_dir.path().join("todos.json");
582        let storage = TodoStorage::new(&storage_path);
583
584        // Create and save todos
585        let mut todos = HashMap::new();
586        let todo1 = Todo::new("1", "First todo", TodoStatus::Pending, TodoPriority::High)
587            .unwrap()
588            .with_description("First description");
589        let todo2 = Todo::new("2", "Second todo", TodoStatus::Completed, TodoPriority::Low).unwrap();
590
591        todos.insert(todo1.id.clone(), todo1);
592        todos.insert(todo2.id.clone(), todo2);
593
594        storage.save_todos(&todos).unwrap();
595
596        // Load and verify
597        let loaded = storage.load_todos().unwrap();
598        assert_eq!(loaded.len(), 2);
599        assert!(loaded.contains_key("1"));
600        assert!(loaded.contains_key("2"));
601    }
602
603    #[test]
604    fn test_todo_tools_write_and_read() {
605        let temp_dir = TempDir::new().unwrap();
606        let storage_path = temp_dir.path().join("todos.json");
607        let tools = TodoTools::with_storage_path(&storage_path);
608
609        // Write todos
610        let todo1 = Todo::new("1", "First", TodoStatus::Pending, TodoPriority::High).unwrap();
611        let todo2 = Todo::new("2", "Second", TodoStatus::InProgress, TodoPriority::Medium).unwrap();
612
613        let write_result = tools
614            .write_todos(TodowriteInput {
615                todos: vec![todo1, todo2],
616            })
617            .unwrap();
618
619        assert_eq!(write_result.created, 2);
620        assert_eq!(write_result.updated, 0);
621
622        // Read todos
623        let read_result = tools
624            .read_todos(TodoreadInput {
625                status_filter: None,
626                priority_filter: None,
627            })
628            .unwrap();
629
630        assert_eq!(read_result.todos.len(), 2);
631    }
632
633    #[test]
634    fn test_todo_tools_update() {
635        let temp_dir = TempDir::new().unwrap();
636        let storage_path = temp_dir.path().join("todos.json");
637        let tools = TodoTools::with_storage_path(&storage_path);
638
639        // Write initial todos
640        let todo1 = Todo::new("1", "First", TodoStatus::Pending, TodoPriority::High).unwrap();
641        tools
642            .write_todos(TodowriteInput {
643                todos: vec![todo1],
644            })
645            .unwrap();
646
647        // Update the todo
648        let updated_todo =
649            Todo::new("1", "First (updated)", TodoStatus::Completed, TodoPriority::Low).unwrap();
650        let write_result = tools
651            .write_todos(TodowriteInput {
652                todos: vec![updated_todo],
653            })
654            .unwrap();
655
656        assert_eq!(write_result.created, 0);
657        assert_eq!(write_result.updated, 1);
658
659        // Verify update
660        let read_result = tools
661            .read_todos(TodoreadInput {
662                status_filter: None,
663                priority_filter: None,
664            })
665            .unwrap();
666
667        assert_eq!(read_result.todos.len(), 1);
668        assert_eq!(read_result.todos[0].title, "First (updated)");
669        assert_eq!(read_result.todos[0].status, TodoStatus::Completed);
670    }
671
672    #[test]
673    fn test_todo_tools_filter_by_status() {
674        let temp_dir = TempDir::new().unwrap();
675        let storage_path = temp_dir.path().join("todos.json");
676        let tools = TodoTools::with_storage_path(&storage_path);
677
678        // Write todos with different statuses
679        let todo1 = Todo::new("1", "Pending", TodoStatus::Pending, TodoPriority::High).unwrap();
680        let todo2 =
681            Todo::new("2", "Completed", TodoStatus::Completed, TodoPriority::Medium).unwrap();
682
683        tools
684            .write_todos(TodowriteInput {
685                todos: vec![todo1, todo2],
686            })
687            .unwrap();
688
689        // Filter by status
690        let read_result = tools
691            .read_todos(TodoreadInput {
692                status_filter: Some(TodoStatus::Completed),
693                priority_filter: None,
694            })
695            .unwrap();
696
697        assert_eq!(read_result.todos.len(), 1);
698        assert_eq!(read_result.todos[0].title, "Completed");
699    }
700
701    #[test]
702    fn test_todo_tools_filter_by_priority() {
703        let temp_dir = TempDir::new().unwrap();
704        let storage_path = temp_dir.path().join("todos.json");
705        let tools = TodoTools::with_storage_path(&storage_path);
706
707        // Write todos with different priorities
708        let todo1 = Todo::new("1", "High", TodoStatus::Pending, TodoPriority::High).unwrap();
709        let todo2 = Todo::new("2", "Low", TodoStatus::Pending, TodoPriority::Low).unwrap();
710
711        tools
712            .write_todos(TodowriteInput {
713                todos: vec![todo1, todo2],
714            })
715            .unwrap();
716
717        // Filter by priority
718        let read_result = tools
719            .read_todos(TodoreadInput {
720                status_filter: None,
721                priority_filter: Some(TodoPriority::High),
722            })
723            .unwrap();
724
725        assert_eq!(read_result.todos.len(), 1);
726        assert_eq!(read_result.todos[0].title, "High");
727    }
728
729    #[tokio::test]
730    async fn test_todo_write_timeout_enforcement() {
731        let temp_dir = TempDir::new().unwrap();
732        let storage_path = temp_dir.path().join("todos.json");
733        let tools = TodoTools::with_storage_path(&storage_path);
734
735        // Write todos with timeout enforcement (should complete well within 500ms)
736        let todo = Todo::new("1", "Test", TodoStatus::Pending, TodoPriority::High).unwrap();
737        let result = tools
738            .write_todos_with_timeout(TodowriteInput {
739                todos: vec![todo],
740            })
741            .await;
742
743        assert!(result.is_ok());
744        let output = result.unwrap();
745        assert_eq!(output.created, 1);
746    }
747
748    #[tokio::test]
749    async fn test_todo_read_timeout_enforcement() {
750        let temp_dir = TempDir::new().unwrap();
751        let storage_path = temp_dir.path().join("todos.json");
752        let tools = TodoTools::with_storage_path(&storage_path);
753
754        // Write a todo first
755        let todo = Todo::new("1", "Test", TodoStatus::Pending, TodoPriority::High).unwrap();
756        tools
757            .write_todos(TodowriteInput {
758                todos: vec![todo],
759            })
760            .unwrap();
761
762        // Read todos with timeout enforcement (should complete well within 500ms)
763        let result = tools
764            .read_todos_with_timeout(TodoreadInput {
765                status_filter: None,
766                priority_filter: None,
767            })
768            .await;
769
770        assert!(result.is_ok());
771        let output = result.unwrap();
772        assert_eq!(output.todos.len(), 1);
773    }
774}