Skip to main content

autom8/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum Autom8Error {
6    #[error("Spec file not found: {0}\n\nThe spec file does not exist at the specified path.\n\nTo fix this:\n  1. Check that the path is correct\n  2. Run 'autom8 init' to create the project structure\n  3. Create a spec file or use 'autom8' to generate one interactively")]
7    SpecNotFound(PathBuf),
8
9    #[error("Invalid spec format: {0}\n\nThe spec file exists but cannot be parsed.\n\nTo fix this:\n  1. Ensure the file is valid JSON or Markdown\n  2. Check for syntax errors (missing commas, brackets, etc.)\n  3. See CLAUDE.md for spec format requirements")]
10    InvalidSpec(String),
11
12    #[error("No incomplete stories found in spec\n\nAll user stories in the spec have passes: true.\n\nTo continue:\n  1. Add new user stories to the spec, or\n  2. Set passes: false on stories you want to re-implement")]
13    NoIncompleteStories,
14
15    #[error("Claude process failed: {0}")]
16    ClaudeError(String),
17
18    #[error("Claude process timed out after {0} seconds")]
19    ClaudeTimeout(u64),
20
21    #[error("State file error: {0}")]
22    StateError(String),
23
24    #[error("No active run to resume\n\nNo incomplete session was found for this project.\n\nTo start a new run:\n  1. Run 'autom8 spec.json' to start from a spec file, or\n  2. Run 'autom8' to create a new spec interactively, or\n  3. Use 'autom8 status --all' to check all sessions")]
25    NoActiveRun,
26
27    #[error("Run already in progress: {0}\n\nAnother session is actively running for this project.\n\nTo resolve this:\n  1. Wait for the current run to complete, or\n  2. Use --worktree to run in a separate worktree, or\n  3. Use 'autom8 status --all' to see all sessions")]
28    RunInProgress(String),
29
30    #[error("IO error: {0}")]
31    Io(#[from] std::io::Error),
32
33    #[error("JSON error: {0}")]
34    Json(#[from] serde_json::Error),
35
36    #[error("Git error: {0}")]
37    GitError(String),
38
39    #[error("Spec markdown file not found: {0}")]
40    SpecMarkdownNotFound(PathBuf),
41
42    #[error("Spec file is empty")]
43    EmptySpec,
44
45    #[error("Spec generation failed: {0}")]
46    SpecGenerationFailed(String),
47
48    #[error("Invalid generated spec: {0}")]
49    InvalidGeneratedSpec(String),
50
51    #[error("Configuration error: {0}")]
52    Config(String),
53
54    #[error("Review failed after 3 iterations. Please manually review autom8_review.md for remaining issues.")]
55    MaxReviewIterationsReached,
56
57    #[error("No incomplete specs found in spec/\n\nNo spec files with incomplete user stories were found.\n\nTo start a new run:\n  1. Run 'autom8' to create a new spec interactively, or\n  2. Add a spec file to ~/.config/autom8/<project>/spec/, or\n  3. Set passes: false on stories you want to re-implement")]
58    NoSpecsToResume,
59
60    #[error("Shell completion error: {0}")]
61    ShellCompletion(String),
62
63    #[error("Worktree error: {0}")]
64    WorktreeError(String),
65
66    #[error("Branch conflict: branch '{branch}' is already in use by session '{session_id}' at {worktree_path}.\n\nThe branch is checked out in another worktree session.\n\nTo resolve this:\n  1. Wait for the other session to complete, or\n  2. Use a different branch name in your spec, or\n  3. Resume the existing session: autom8 resume --session {session_id}, or\n  4. Clean up the conflicting session: autom8 clean --session {session_id}")]
67    BranchConflict {
68        branch: String,
69        session_id: String,
70        worktree_path: std::path::PathBuf,
71    },
72
73    #[error("Signal handler error: {0}")]
74    SignalHandler(String),
75
76    #[error("Interrupted by user")]
77    Interrupted,
78
79    #[error("GUI error: {0}")]
80    GuiError(String),
81
82    #[error("Not in a git repository\n\nThe improve command must be run from within a git repository.\n\nTo fix this:\n  1. Navigate to your project directory: cd /path/to/your/project\n  2. Ensure the directory is a git repository (contains .git folder)\n  3. If not initialized, run: git init")]
83    NotInGitRepo,
84
85    #[error("Claude CLI not found\n\nThe 'claude' command could not be found in your PATH.\n\nTo fix this:\n  1. Install Claude Code: https://claude.ai/code\n  2. Ensure 'claude' is in your PATH\n  3. Try running 'claude --version' to verify installation")]
86    ClaudeNotFound,
87
88    #[error("Failed to spawn Claude: {0}\n\nCould not start the Claude CLI process.\n\nTo fix this:\n  1. Check that 'claude' is installed and working\n  2. Ensure you have permissions to run the command\n  3. Try running 'claude' manually to diagnose the issue")]
89    ClaudeSpawnError(String),
90}
91
92pub type Result<T> = std::result::Result<T, Autom8Error>;
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    // ========================================================================
99    // Error message format tests (US-012)
100    // ========================================================================
101
102    #[test]
103    fn test_us012_branch_conflict_error_includes_what_happened() {
104        let err = Autom8Error::BranchConflict {
105            branch: "feature/test".to_string(),
106            session_id: "abc123".to_string(),
107            worktree_path: PathBuf::from("/path/to/worktree"),
108        };
109        let msg = err.to_string();
110
111        // What happened
112        assert!(
113            msg.contains("Branch conflict"),
114            "Error should describe what happened"
115        );
116        assert!(
117            msg.contains("feature/test"),
118            "Error should include branch name"
119        );
120    }
121
122    #[test]
123    fn test_us012_branch_conflict_error_includes_why() {
124        let err = Autom8Error::BranchConflict {
125            branch: "feature/test".to_string(),
126            session_id: "abc123".to_string(),
127            worktree_path: PathBuf::from("/path/to/worktree"),
128        };
129        let msg = err.to_string();
130
131        // Why it happened
132        assert!(
133            msg.contains("already in use") || msg.contains("checked out"),
134            "Error should explain why"
135        );
136    }
137
138    #[test]
139    fn test_us012_branch_conflict_error_includes_how_to_fix() {
140        let err = Autom8Error::BranchConflict {
141            branch: "feature/test".to_string(),
142            session_id: "abc123".to_string(),
143            worktree_path: PathBuf::from("/path/to/worktree"),
144        };
145        let msg = err.to_string();
146
147        // How to fix - multiple options
148        assert!(
149            msg.contains("To resolve"),
150            "Error should include resolution steps"
151        );
152        assert!(
153            msg.contains("autom8 resume"),
154            "Error should suggest resume command"
155        );
156        assert!(
157            msg.contains("autom8 clean"),
158            "Error should suggest clean command"
159        );
160        assert!(
161            msg.contains("abc123"),
162            "Error should include session ID for commands"
163        );
164    }
165
166    #[test]
167    fn test_us012_spec_not_found_error_includes_fix() {
168        let err = Autom8Error::SpecNotFound(PathBuf::from("/missing/spec.json"));
169        let msg = err.to_string();
170
171        assert!(
172            msg.contains("not found"),
173            "Error should describe what happened"
174        );
175        assert!(msg.contains("To fix"), "Error should include fix steps");
176        assert!(
177            msg.contains("autom8 init") || msg.contains("init"),
178            "Error should suggest init command"
179        );
180    }
181
182    #[test]
183    fn test_us012_no_active_run_error_includes_fix() {
184        let err = Autom8Error::NoActiveRun;
185        let msg = err.to_string();
186
187        assert!(
188            msg.contains("No active run"),
189            "Error should describe what happened"
190        );
191        assert!(
192            msg.contains("To start") || msg.contains("To fix"),
193            "Error should include fix steps"
194        );
195        assert!(
196            msg.contains("autom8 status"),
197            "Error should suggest status command"
198        );
199    }
200
201    #[test]
202    fn test_us012_run_in_progress_error_includes_fix() {
203        let err = Autom8Error::RunInProgress("session123".to_string());
204        let msg = err.to_string();
205
206        assert!(
207            msg.contains("in progress"),
208            "Error should describe what happened"
209        );
210        assert!(msg.contains("To resolve"), "Error should include fix steps");
211        assert!(
212            msg.contains("--worktree"),
213            "Error should suggest worktree option"
214        );
215    }
216
217    #[test]
218    fn test_us012_no_incomplete_stories_error_includes_fix() {
219        let err = Autom8Error::NoIncompleteStories;
220        let msg = err.to_string();
221
222        assert!(
223            msg.contains("No incomplete stories"),
224            "Error should describe what happened"
225        );
226        assert!(
227            msg.contains("To continue"),
228            "Error should include fix steps"
229        );
230        assert!(
231            msg.contains("passes: false"),
232            "Error should suggest how to re-run stories"
233        );
234    }
235
236    #[test]
237    fn test_us012_no_specs_to_resume_error_includes_fix() {
238        let err = Autom8Error::NoSpecsToResume;
239        let msg = err.to_string();
240
241        assert!(
242            msg.contains("No incomplete specs"),
243            "Error should describe what happened"
244        );
245        assert!(msg.contains("To start"), "Error should include fix steps");
246    }
247
248    #[test]
249    fn test_us012_invalid_spec_error_includes_fix() {
250        let err = Autom8Error::InvalidSpec("missing field 'id'".to_string());
251        let msg = err.to_string();
252
253        assert!(
254            msg.contains("Invalid spec format"),
255            "Error should describe what happened"
256        );
257        assert!(msg.contains("To fix"), "Error should include fix steps");
258        assert!(
259            msg.contains("JSON") || msg.contains("syntax"),
260            "Error should mention format"
261        );
262    }
263
264    // ========================================================================
265    // US-009: NotInGitRepo error tests (improve command edge case)
266    // ========================================================================
267
268    #[test]
269    fn test_us009_not_in_git_repo_error_includes_what_happened() {
270        let err = Autom8Error::NotInGitRepo;
271        let msg = err.to_string();
272
273        assert!(
274            msg.contains("Not in a git repository"),
275            "Error should describe what happened"
276        );
277    }
278
279    #[test]
280    fn test_us009_not_in_git_repo_error_includes_why() {
281        let err = Autom8Error::NotInGitRepo;
282        let msg = err.to_string();
283
284        assert!(
285            msg.contains("must be run from within a git repository"),
286            "Error should explain why"
287        );
288    }
289
290    #[test]
291    fn test_us009_not_in_git_repo_error_includes_fix() {
292        let err = Autom8Error::NotInGitRepo;
293        let msg = err.to_string();
294
295        assert!(msg.contains("To fix"), "Error should include fix steps");
296        assert!(
297            msg.contains("cd") || msg.contains("Navigate"),
298            "Error should suggest changing directory"
299        );
300        assert!(
301            msg.contains(".git") || msg.contains("git init"),
302            "Error should mention git initialization"
303        );
304    }
305}