Skip to main content

intent_engine/
session_restore.rs

1use crate::db::models::{TaskSortBy, WorkspaceStats};
2use crate::error::Result;
3use crate::events::EventManager;
4use crate::tasks::TaskManager;
5use crate::workspace::WorkspaceManager;
6use serde::{Deserialize, Serialize};
7use sqlx::SqlitePool;
8
9/// Session restoration status
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum SessionStatus {
13    /// Successfully restored focus
14    Success,
15    /// No active focus (workspace exists but no current task)
16    NoFocus,
17    /// Error occurred
18    Error,
19}
20
21/// Error types for session restoration
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ErrorType {
25    WorkspaceNotFound,
26    DatabaseCorrupted,
27    PermissionDenied,
28}
29
30/// Simplified task info for session restoration
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TaskInfo {
33    pub id: i64,
34    pub name: String,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub status: Option<String>,
37}
38
39/// Detailed current task info
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CurrentTaskInfo {
42    pub id: i64,
43    pub name: String,
44    pub status: String,
45    /// Full spec
46    pub spec: Option<String>,
47    /// Truncated spec (first 100 chars)
48    pub spec_preview: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub created_at: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub first_doing_at: Option<String>,
53}
54
55/// Siblings information
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SiblingsInfo {
58    pub total: usize,
59    pub done: usize,
60    pub doing: usize,
61    pub todo: usize,
62    pub done_list: Vec<TaskInfo>,
63}
64
65/// Children information
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ChildrenInfo {
68    pub total: usize,
69    pub todo: usize,
70    pub list: Vec<TaskInfo>,
71}
72
73/// Event information
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EventInfo {
76    #[serde(rename = "type")]
77    pub event_type: String,
78    pub data: String,
79    pub timestamp: String,
80}
81
82// WorkspaceStats is imported from crate::db::models
83
84/// Complete session restoration result
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SessionRestoreResult {
87    pub status: SessionStatus,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub workspace_path: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub current_task: Option<CurrentTaskInfo>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub parent_task: Option<TaskInfo>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub siblings: Option<SiblingsInfo>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub children: Option<ChildrenInfo>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub recent_events: Option<Vec<EventInfo>>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub suggested_commands: Option<Vec<String>>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub stats: Option<WorkspaceStats>,
104    /// Recommended next task (from pick_next) - used when no focus
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub recommended_task: Option<TaskInfo>,
107    /// Top pending tasks by priority - used when no focus
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub top_pending_tasks: Option<Vec<TaskInfo>>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub error_type: Option<ErrorType>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub message: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub recovery_suggestion: Option<String>,
116}
117
118/// Session restoration manager
119pub struct SessionRestoreManager<'a> {
120    pool: &'a SqlitePool,
121}
122
123impl<'a> SessionRestoreManager<'a> {
124    pub fn new(pool: &'a SqlitePool) -> Self {
125        Self { pool }
126    }
127
128    /// Restore session with full context
129    pub async fn restore(&self, include_events: usize) -> Result<SessionRestoreResult> {
130        let workspace_mgr = WorkspaceManager::new(self.pool);
131        let task_mgr = TaskManager::new(self.pool);
132        let event_mgr = EventManager::new(self.pool);
133
134        // Get current workspace path (for display purposes)
135        let workspace_path = std::env::current_dir()
136            .ok()
137            .and_then(|p| p.to_str().map(String::from));
138
139        // Get current task
140        let current_task_id = match workspace_mgr.get_current_task(None).await {
141            Ok(response) => {
142                if let Some(id) = response.current_task_id {
143                    id
144                } else {
145                    // No focus - return stats
146                    return self.restore_no_focus(workspace_path).await;
147                }
148            },
149            Err(_) => {
150                // Error getting current task
151                return Ok(Self::error_result(
152                    workspace_path,
153                    ErrorType::DatabaseCorrupted,
154                    "Unable to query workspace state",
155                    "Run 'ie workspace init' or check database integrity",
156                ));
157            },
158        };
159
160        // Get current task details
161        let task = match task_mgr.get_task(current_task_id).await {
162            Ok(t) => t,
163            Err(_) => {
164                return Ok(Self::error_result(
165                    workspace_path,
166                    ErrorType::DatabaseCorrupted,
167                    "Current task not found in database",
168                    "Run 'ie current --set <task_id>' to set a valid task",
169                ));
170            },
171        };
172
173        let spec_preview = task.spec.as_ref().map(|s| Self::truncate_spec(s, 100));
174
175        let current_task = CurrentTaskInfo {
176            id: task.id,
177            name: task.name.clone(),
178            status: task.status.clone(),
179            spec: task.spec.clone(),
180            spec_preview,
181            created_at: task.first_todo_at.map(|dt| dt.to_rfc3339()),
182            first_doing_at: task.first_doing_at.map(|dt| dt.to_rfc3339()),
183        };
184
185        // Get parent task
186        let parent_task = if let Some(parent_id) = task.parent_id {
187            task_mgr.get_task(parent_id).await.ok().map(|p| TaskInfo {
188                id: p.id,
189                name: p.name,
190                status: Some(p.status),
191            })
192        } else {
193            None
194        };
195
196        // Get siblings info
197        let siblings = if let Some(parent_id) = task.parent_id {
198            let result = task_mgr
199                .find_tasks(None, Some(Some(parent_id)), None, None, None)
200                .await?;
201            Self::build_siblings_info(&result.tasks)
202        } else {
203            None
204        };
205
206        // Get children info
207        let children = {
208            let result = task_mgr
209                .find_tasks(None, Some(Some(current_task_id)), None, None, None)
210                .await?;
211            Self::build_children_info(&result.tasks)
212        };
213
214        // Get recent events
215        let events = event_mgr
216            .list_events(Some(current_task_id), None, None, None)
217            .await?;
218        let recent_events: Vec<EventInfo> = events
219            .into_iter()
220            .take(include_events)
221            .map(|e| EventInfo {
222                event_type: e.log_type,
223                data: e.discussion_data,
224                timestamp: e.timestamp.to_rfc3339(),
225            })
226            .collect();
227
228        // Suggest next commands based on context
229        let suggested_commands = Self::suggest_commands(&current_task, children.as_ref());
230
231        Ok(SessionRestoreResult {
232            status: SessionStatus::Success,
233            workspace_path,
234            current_task: Some(current_task),
235            parent_task,
236            siblings,
237            children,
238            recent_events: Some(recent_events),
239            suggested_commands: Some(suggested_commands),
240            stats: None,
241            recommended_task: None,
242            top_pending_tasks: None,
243            error_type: None,
244            message: None,
245            recovery_suggestion: None,
246        })
247    }
248
249    /// Restore when no focus exists
250    ///
251    /// Uses efficient SQL aggregation for stats and limited queries for task lists.
252    /// Does NOT load all tasks into memory.
253    async fn restore_no_focus(
254        &self,
255        workspace_path: Option<String>,
256    ) -> Result<SessionRestoreResult> {
257        let task_mgr = TaskManager::new(self.pool);
258
259        // 1. Get stats using SQL aggregation (no data loading)
260        let stats = task_mgr.get_stats().await?;
261
262        // 2. Get recommended next task via pick_next
263        let recommendation = task_mgr.pick_next().await?;
264        let recommended_task = recommendation.task.map(|t| TaskInfo {
265            id: t.id,
266            name: t.name,
267            status: Some(t.status),
268        });
269
270        // 3. Get top 5 pending tasks by priority (limited query)
271        let top_pending_result = task_mgr
272            .find_tasks(
273                Some("todo".to_string()),
274                None,
275                Some(TaskSortBy::Priority),
276                Some(5),
277                None,
278            )
279            .await?;
280        let top_pending_tasks: Vec<TaskInfo> = top_pending_result
281            .tasks
282            .into_iter()
283            .map(|t| TaskInfo {
284                id: t.id,
285                name: t.name,
286                status: Some(t.status),
287            })
288            .collect();
289
290        let suggested_commands = vec![
291            "ie pick-next".to_string(),
292            "ie task list --status todo".to_string(),
293        ];
294
295        Ok(SessionRestoreResult {
296            status: SessionStatus::NoFocus,
297            workspace_path,
298            current_task: None,
299            parent_task: None,
300            siblings: None,
301            children: None,
302            recent_events: None,
303            suggested_commands: Some(suggested_commands),
304            stats: Some(stats),
305            recommended_task,
306            top_pending_tasks: if top_pending_tasks.is_empty() {
307                None
308            } else {
309                Some(top_pending_tasks)
310            },
311            error_type: None,
312            message: None,
313            recovery_suggestion: None,
314        })
315    }
316
317    /// Build siblings information
318    fn build_siblings_info(siblings: &[crate::db::models::Task]) -> Option<SiblingsInfo> {
319        if siblings.is_empty() {
320            return None;
321        }
322
323        let done_list: Vec<TaskInfo> = siblings
324            .iter()
325            .filter(|s| s.status == "done")
326            .map(|s| TaskInfo {
327                id: s.id,
328                name: s.name.clone(),
329                status: Some(s.status.clone()),
330            })
331            .collect();
332
333        Some(SiblingsInfo {
334            total: siblings.len(),
335            done: siblings.iter().filter(|s| s.status == "done").count(),
336            doing: siblings.iter().filter(|s| s.status == "doing").count(),
337            todo: siblings.iter().filter(|s| s.status == "todo").count(),
338            done_list,
339        })
340    }
341
342    /// Build children information
343    fn build_children_info(children: &[crate::db::models::Task]) -> Option<ChildrenInfo> {
344        if children.is_empty() {
345            return None;
346        }
347
348        let list: Vec<TaskInfo> = children
349            .iter()
350            .map(|c| TaskInfo {
351                id: c.id,
352                name: c.name.clone(),
353                status: Some(c.status.clone()),
354            })
355            .collect();
356
357        Some(ChildrenInfo {
358            total: children.len(),
359            todo: children.iter().filter(|c| c.status == "todo").count(),
360            list,
361        })
362    }
363
364    /// Suggest next commands based on context
365    fn suggest_commands(
366        _current_task: &CurrentTaskInfo,
367        children: Option<&ChildrenInfo>,
368    ) -> Vec<String> {
369        let mut commands = vec![];
370
371        // If there are blockers, suggest viewing them
372        commands.push("ie event list --type blocker".to_string());
373
374        // Suggest completion or spawning subtasks
375        if let Some(children) = children {
376            if children.todo > 0 {
377                commands.push("ie task done".to_string());
378            }
379        } else {
380            commands.push("ie task done".to_string());
381        }
382
383        commands.push("ie task spawn-subtask".to_string());
384
385        commands
386    }
387
388    /// Truncate spec to specified length
389    fn truncate_spec(spec: &str, max_len: usize) -> String {
390        if spec.len() <= max_len {
391            spec.to_string()
392        } else {
393            format!("{}...", &spec[..max_len])
394        }
395    }
396
397    /// Create error result
398    fn error_result(
399        workspace_path: Option<String>,
400        error_type: ErrorType,
401        message: &str,
402        recovery: &str,
403    ) -> SessionRestoreResult {
404        let suggested_commands = match error_type {
405            ErrorType::WorkspaceNotFound => {
406                vec!["ie workspace init".to_string(), "ie help".to_string()]
407            },
408            ErrorType::DatabaseCorrupted => {
409                vec!["ie workspace init".to_string(), "ie doctor".to_string()]
410            },
411            ErrorType::PermissionDenied => {
412                vec!["chmod 644 .intent-engine/workspace.db".to_string()]
413            },
414        };
415
416        SessionRestoreResult {
417            status: SessionStatus::Error,
418            workspace_path,
419            current_task: None,
420            parent_task: None,
421            siblings: None,
422            children: None,
423            recent_events: None,
424            suggested_commands: Some(suggested_commands),
425            stats: None,
426            recommended_task: None,
427            top_pending_tasks: None,
428            error_type: Some(error_type),
429            message: Some(message.to_string()),
430            recovery_suggestion: Some(recovery.to_string()),
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::events::EventManager;
439    use crate::tasks::{TaskManager, TaskUpdate};
440    use crate::test_utils::test_helpers::TestContext;
441    use crate::workspace::WorkspaceManager;
442
443    #[test]
444    fn test_truncate_spec() {
445        let spec = "a".repeat(200);
446        let truncated = SessionRestoreManager::truncate_spec(&spec, 100);
447        assert_eq!(truncated.len(), 103); // 100 + "..."
448        assert!(truncated.ends_with("..."));
449    }
450
451    #[test]
452    fn test_truncate_spec_short() {
453        let spec = "Short spec";
454        let truncated = SessionRestoreManager::truncate_spec(spec, 100);
455        assert_eq!(truncated, spec);
456        assert!(!truncated.ends_with("..."));
457    }
458
459    #[tokio::test]
460    async fn test_restore_with_focus_minimal() {
461        let ctx = TestContext::new().await;
462        let pool = ctx.pool();
463
464        // Create a task and set it as current
465        let task_mgr = TaskManager::new(pool);
466        let task = task_mgr
467            .add_task("Test task".to_string(), None, None, None, None, None)
468            .await
469            .unwrap();
470
471        let workspace_mgr = WorkspaceManager::new(pool);
472        workspace_mgr.set_current_task(task.id, None).await.unwrap();
473
474        // Restore session
475        let restore_mgr = SessionRestoreManager::new(pool);
476        let result = restore_mgr.restore(3).await.unwrap();
477
478        // Verify
479        assert_eq!(result.status, SessionStatus::Success);
480        assert!(result.current_task.is_some());
481        let current_task = result.current_task.unwrap();
482        assert_eq!(current_task.id, task.id);
483        assert_eq!(current_task.name, "Test task");
484    }
485
486    #[tokio::test]
487    async fn test_restore_with_focus_rich_context() {
488        let ctx = TestContext::new().await;
489        let pool = ctx.pool();
490
491        let task_mgr = TaskManager::new(pool);
492        let event_mgr = EventManager::new(pool);
493        let workspace_mgr = WorkspaceManager::new(pool);
494
495        // Create parent task
496        let parent = task_mgr
497            .add_task(
498                "Parent task".to_string(),
499                Some("Parent spec".to_string()),
500                None,
501                None,
502                None,
503                None,
504            )
505            .await
506            .unwrap();
507
508        // Create 3 siblings (1 done, 1 doing, 1 todo)
509        let sibling1 = task_mgr
510            .add_task(
511                "Sibling 1".to_string(),
512                None,
513                Some(parent.id),
514                None,
515                None,
516                None,
517            )
518            .await
519            .unwrap();
520        task_mgr
521            .update_task(
522                sibling1.id,
523                TaskUpdate {
524                    status: Some("done"),
525                    ..Default::default()
526                },
527            )
528            .await
529            .unwrap();
530
531        let current = task_mgr
532            .add_task(
533                "Current task".to_string(),
534                Some("Current spec".to_string()),
535                Some(parent.id),
536                None,
537                None,
538                None,
539            )
540            .await
541            .unwrap();
542        task_mgr
543            .update_task(
544                current.id,
545                TaskUpdate {
546                    status: Some("doing"),
547                    ..Default::default()
548                },
549            )
550            .await
551            .unwrap();
552        workspace_mgr
553            .set_current_task(current.id, None)
554            .await
555            .unwrap();
556
557        let _sibling3 = task_mgr
558            .add_task(
559                "Sibling 3".to_string(),
560                None,
561                Some(parent.id),
562                None,
563                None,
564                None,
565            )
566            .await
567            .unwrap();
568
569        // Add children to current task
570        let _child1 = task_mgr
571            .add_task(
572                "Child 1".to_string(),
573                None,
574                Some(current.id),
575                None,
576                None,
577                None,
578            )
579            .await
580            .unwrap();
581        let _child2 = task_mgr
582            .add_task(
583                "Child 2".to_string(),
584                None,
585                Some(current.id),
586                None,
587                None,
588                None,
589            )
590            .await
591            .unwrap();
592
593        // Add events
594        event_mgr
595            .add_event(current.id, "decision".to_string(), "Decision 1".to_string())
596            .await
597            .unwrap();
598        event_mgr
599            .add_event(current.id, "blocker".to_string(), "Blocker 1".to_string())
600            .await
601            .unwrap();
602        event_mgr
603            .add_event(current.id, "note".to_string(), "Note 1".to_string())
604            .await
605            .unwrap();
606
607        // Restore session
608        let restore_mgr = SessionRestoreManager::new(pool);
609        let result = restore_mgr.restore(3).await.unwrap();
610
611        // Verify complete context
612        assert_eq!(result.status, SessionStatus::Success);
613
614        let task = result.current_task.unwrap();
615        assert_eq!(task.id, current.id);
616        assert_eq!(task.spec, Some("Current spec".to_string()));
617
618        // Verify parent
619        assert!(result.parent_task.is_some());
620        let parent_info = result.parent_task.unwrap();
621        assert_eq!(parent_info.id, parent.id);
622
623        // Verify siblings
624        assert!(result.siblings.is_some());
625        let siblings = result.siblings.unwrap();
626        assert_eq!(siblings.total, 3);
627        assert_eq!(siblings.done, 1);
628        assert_eq!(siblings.doing, 1);
629        assert_eq!(siblings.todo, 1);
630        assert_eq!(siblings.done_list.len(), 1);
631
632        // Verify children
633        assert!(result.children.is_some());
634        let children = result.children.unwrap();
635        assert_eq!(children.total, 2);
636        assert_eq!(children.todo, 2);
637
638        // Verify events
639        assert!(result.recent_events.is_some());
640        let events = result.recent_events.unwrap();
641        assert_eq!(events.len(), 3);
642
643        // Check event types
644        let event_types: Vec<&str> = events.iter().map(|e| e.event_type.as_str()).collect();
645        assert!(event_types.contains(&"decision"));
646        assert!(event_types.contains(&"blocker"));
647        assert!(event_types.contains(&"note"));
648    }
649
650    #[tokio::test]
651    async fn test_restore_with_spec_preview() {
652        let ctx = TestContext::new().await;
653        let pool = ctx.pool();
654
655        let task_mgr = TaskManager::new(pool);
656        let workspace_mgr = WorkspaceManager::new(pool);
657
658        // Create task with long spec
659        let long_spec = "a".repeat(200);
660        let task = task_mgr
661            .add_task(
662                "Test task".to_string(),
663                Some(long_spec.to_string()),
664                None,
665                None,
666                None,
667                None,
668            )
669            .await
670            .unwrap();
671        workspace_mgr.set_current_task(task.id, None).await.unwrap();
672
673        // Restore
674        let restore_mgr = SessionRestoreManager::new(pool);
675        let result = restore_mgr.restore(3).await.unwrap();
676
677        // Verify spec preview is truncated
678        let current_task = result.current_task.unwrap();
679        assert_eq!(current_task.spec, Some(long_spec));
680        assert!(current_task.spec_preview.is_some());
681        let preview = current_task.spec_preview.unwrap();
682        assert_eq!(preview.len(), 103); // 100 + "..."
683        assert!(preview.ends_with("..."));
684    }
685
686    #[tokio::test]
687    async fn test_restore_no_focus() {
688        let ctx = TestContext::new().await;
689        let pool = ctx.pool();
690
691        let task_mgr = TaskManager::new(pool);
692
693        // Create some tasks but no current task
694        task_mgr
695            .add_task("Task 1".to_string(), None, None, None, None, None)
696            .await
697            .unwrap();
698        task_mgr
699            .add_task("Task 2".to_string(), None, None, None, None, None)
700            .await
701            .unwrap();
702
703        // Restore
704        let restore_mgr = SessionRestoreManager::new(pool);
705        let result = restore_mgr.restore(3).await.unwrap();
706
707        // Verify no focus status
708        assert_eq!(result.status, SessionStatus::NoFocus);
709        assert!(result.current_task.is_none());
710        assert!(result.stats.is_some());
711
712        let stats = result.stats.unwrap();
713        assert_eq!(stats.total_tasks, 2);
714        assert_eq!(stats.todo, 2);
715
716        // Verify new fields: recommended_task and top_pending_tasks
717        // pick_next should recommend one of the tasks
718        assert!(result.recommended_task.is_some());
719
720        // top_pending_tasks should contain the 2 todo tasks
721        assert!(result.top_pending_tasks.is_some());
722        let top_pending = result.top_pending_tasks.unwrap();
723        assert_eq!(top_pending.len(), 2);
724    }
725
726    #[tokio::test]
727    async fn test_restore_recent_events_limit() {
728        let ctx = TestContext::new().await;
729        let pool = ctx.pool();
730
731        let task_mgr = TaskManager::new(pool);
732        let event_mgr = EventManager::new(pool);
733        let workspace_mgr = WorkspaceManager::new(pool);
734
735        // Create task
736        let task = task_mgr
737            .add_task("Test task".to_string(), None, None, None, None, None)
738            .await
739            .unwrap();
740        workspace_mgr.set_current_task(task.id, None).await.unwrap();
741
742        // Add 10 events
743        for i in 0..10 {
744            event_mgr
745                .add_event(task.id, "note".to_string(), format!("Event {}", i))
746                .await
747                .unwrap();
748        }
749
750        // Restore with default limit (3)
751        let restore_mgr = SessionRestoreManager::new(pool);
752        let result = restore_mgr.restore(3).await.unwrap();
753
754        // Should only return 3 events (most recent)
755        let events = result.recent_events.unwrap();
756        assert_eq!(events.len(), 3);
757    }
758
759    #[tokio::test]
760    async fn test_restore_custom_events_limit() {
761        let ctx = TestContext::new().await;
762        let pool = ctx.pool();
763
764        let task_mgr = TaskManager::new(pool);
765        let event_mgr = EventManager::new(pool);
766        let workspace_mgr = WorkspaceManager::new(pool);
767
768        // Create task
769        let task = task_mgr
770            .add_task("Test task".to_string(), None, None, None, None, None)
771            .await
772            .unwrap();
773        workspace_mgr.set_current_task(task.id, None).await.unwrap();
774
775        // Add 10 events
776        for i in 0..10 {
777            event_mgr
778                .add_event(task.id, "note".to_string(), format!("Event {}", i))
779                .await
780                .unwrap();
781        }
782
783        // Restore with custom limit (5)
784        let restore_mgr = SessionRestoreManager::new(pool);
785        let result = restore_mgr.restore(5).await.unwrap();
786
787        // Should return 5 events
788        let events = result.recent_events.unwrap();
789        assert_eq!(events.len(), 5);
790    }
791
792    #[test]
793    fn test_suggest_commands_with_children() {
794        let current_task = CurrentTaskInfo {
795            id: 1,
796            name: "Test".to_string(),
797            status: "doing".to_string(),
798            spec: None,
799            spec_preview: None,
800            created_at: None,
801            first_doing_at: None,
802        };
803
804        let children = Some(ChildrenInfo {
805            total: 2,
806            todo: 1,
807            list: vec![],
808        });
809
810        let commands = SessionRestoreManager::suggest_commands(&current_task, children.as_ref());
811
812        assert!(!commands.is_empty());
813        assert!(commands.iter().any(|c| c.contains("blocker")));
814    }
815
816    #[test]
817    fn test_error_result_workspace_not_found() {
818        let result = SessionRestoreManager::error_result(
819            Some("/test/path".to_string()),
820            ErrorType::WorkspaceNotFound,
821            "Test message",
822            "Test recovery",
823        );
824
825        assert_eq!(result.status, SessionStatus::Error);
826        assert_eq!(result.error_type, Some(ErrorType::WorkspaceNotFound));
827        assert_eq!(result.message, Some("Test message".to_string()));
828        assert_eq!(
829            result.recovery_suggestion,
830            Some("Test recovery".to_string())
831        );
832        assert!(result.suggested_commands.is_some());
833
834        let commands = result.suggested_commands.unwrap();
835        assert!(commands.iter().any(|c| c.contains("init")));
836    }
837
838    #[test]
839    fn test_build_siblings_info_empty() {
840        let siblings: Vec<crate::db::models::Task> = vec![];
841        let result = SessionRestoreManager::build_siblings_info(&siblings);
842        assert!(result.is_none());
843    }
844
845    #[test]
846    fn test_build_children_info_empty() {
847        let children: Vec<crate::db::models::Task> = vec![];
848        let result = SessionRestoreManager::build_children_info(&children);
849        assert!(result.is_none());
850    }
851}