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