intent_engine/
error.rs

1use serde::Serialize;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum IntentError {
6    #[error("Database error: {0}")]
7    DatabaseError(#[from] sqlx::Error),
8
9    #[error("IO error: {0}")]
10    IoError(#[from] std::io::Error),
11
12    #[error("Task not found: {0}")]
13    TaskNotFound(i64),
14
15    #[error("Invalid input: {0}")]
16    InvalidInput(String),
17
18    #[error("Circular dependency detected: adding dependency from task {blocking_task_id} to task {blocked_task_id} would create a cycle")]
19    CircularDependency {
20        blocking_task_id: i64,
21        blocked_task_id: i64,
22    },
23
24    #[error("Task {task_id} is blocked by incomplete tasks: {blocking_task_ids:?}")]
25    TaskBlocked {
26        task_id: i64,
27        blocking_task_ids: Vec<i64>,
28    },
29
30    #[error("Action not allowed: {0}")]
31    ActionNotAllowed(String),
32
33    #[error("Uncompleted children exist")]
34    UncompletedChildren,
35
36    #[error("Current directory is not an Intent-Engine project")]
37    NotAProject,
38
39    #[error("JSON serialization error: {0}")]
40    JsonError(#[from] serde_json::Error),
41}
42
43#[derive(Serialize)]
44pub struct ErrorResponse {
45    pub error: String,
46    pub code: String,
47}
48
49impl IntentError {
50    pub fn to_error_code(&self) -> &'static str {
51        match self {
52            IntentError::TaskNotFound(_) => "TASK_NOT_FOUND",
53            IntentError::DatabaseError(_) => "DATABASE_ERROR",
54            IntentError::InvalidInput(_) => "INVALID_INPUT",
55            IntentError::CircularDependency { .. } => "CIRCULAR_DEPENDENCY",
56            IntentError::TaskBlocked { .. } => "TASK_BLOCKED",
57            IntentError::ActionNotAllowed(_) => "ACTION_NOT_ALLOWED",
58            IntentError::UncompletedChildren => "UNCOMPLETED_CHILDREN",
59            IntentError::NotAProject => "NOT_A_PROJECT",
60            _ => "INTERNAL_ERROR",
61        }
62    }
63
64    pub fn to_error_response(&self) -> ErrorResponse {
65        ErrorResponse {
66            error: self.to_string(),
67            code: self.to_error_code().to_string(),
68        }
69    }
70}
71
72pub type Result<T> = std::result::Result<T, IntentError>;
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_task_not_found_error() {
80        let error = IntentError::TaskNotFound(123);
81        assert_eq!(error.to_string(), "Task not found: 123");
82        assert_eq!(error.to_error_code(), "TASK_NOT_FOUND");
83    }
84
85    #[test]
86    fn test_invalid_input_error() {
87        let error = IntentError::InvalidInput("Bad input".to_string());
88        assert_eq!(error.to_string(), "Invalid input: Bad input");
89        assert_eq!(error.to_error_code(), "INVALID_INPUT");
90    }
91
92    #[test]
93    fn test_circular_dependency_error() {
94        let error = IntentError::CircularDependency {
95            blocking_task_id: 1,
96            blocked_task_id: 2,
97        };
98        assert!(error.to_string().contains("Circular dependency detected"));
99        assert!(error.to_string().contains("task 1"));
100        assert!(error.to_string().contains("task 2"));
101        assert_eq!(error.to_error_code(), "CIRCULAR_DEPENDENCY");
102    }
103
104    #[test]
105    fn test_action_not_allowed_error() {
106        let error = IntentError::ActionNotAllowed("Cannot do this".to_string());
107        assert_eq!(error.to_string(), "Action not allowed: Cannot do this");
108        assert_eq!(error.to_error_code(), "ACTION_NOT_ALLOWED");
109    }
110
111    #[test]
112    fn test_uncompleted_children_error() {
113        let error = IntentError::UncompletedChildren;
114        assert_eq!(error.to_string(), "Uncompleted children exist");
115        assert_eq!(error.to_error_code(), "UNCOMPLETED_CHILDREN");
116    }
117
118    #[test]
119    fn test_not_a_project_error() {
120        let error = IntentError::NotAProject;
121        assert_eq!(
122            error.to_string(),
123            "Current directory is not an Intent-Engine project"
124        );
125        assert_eq!(error.to_error_code(), "NOT_A_PROJECT");
126    }
127
128    #[test]
129    fn test_io_error_conversion() {
130        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
131        let error: IntentError = io_error.into();
132        assert!(matches!(error, IntentError::IoError(_)));
133    }
134
135    #[test]
136    fn test_json_error_conversion() {
137        let json_str = "{invalid json";
138        let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
139        let error: IntentError = json_error.into();
140        assert!(matches!(error, IntentError::JsonError(_)));
141    }
142
143    #[test]
144    fn test_error_response_structure() {
145        let error = IntentError::TaskNotFound(456);
146        let response = error.to_error_response();
147
148        assert_eq!(response.code, "TASK_NOT_FOUND");
149        assert_eq!(response.error, "Task not found: 456");
150    }
151
152    #[test]
153    fn test_error_response_serialization() {
154        let error = IntentError::InvalidInput("Test".to_string());
155        let response = error.to_error_response();
156        let json = serde_json::to_string(&response).unwrap();
157
158        assert!(json.contains("\"code\":\"INVALID_INPUT\""));
159        assert!(json.contains("\"error\":\"Invalid input: Test\""));
160    }
161
162    #[test]
163    fn test_database_error_code() {
164        // We can't easily create a real sqlx::Error, so we test through the pattern match
165        let error = IntentError::TaskNotFound(1);
166        if let IntentError::DatabaseError(_) = error {
167            unreachable!()
168        }
169    }
170
171    #[test]
172    fn test_internal_error_fallback() {
173        // Test the _ => "INTERNAL_ERROR" case by testing IoError
174        let io_error = std::io::Error::other("test");
175        let error: IntentError = io_error.into();
176        assert_eq!(error.to_error_code(), "INTERNAL_ERROR");
177    }
178}