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