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