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 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 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}