1use std::path::PathBuf;
4
5#[derive(Debug, thiserror::Error)]
7pub enum Error {
8 #[error("codex binary not found in PATH")]
10 NotFound,
11
12 #[error("codex command failed: {command} (exit code {exit_code}){}{}{}", working_dir.as_ref().map(|d| format!(" (in {})", d.display())).unwrap_or_default(), if stdout.is_empty() { String::new() } else { format!("\nstdout: {stdout}") }, if stderr.is_empty() { String::new() } else { format!("\nstderr: {stderr}") })]
14 CommandFailed {
15 command: String,
16 exit_code: i32,
17 stdout: String,
18 stderr: String,
19 working_dir: Option<PathBuf>,
20 },
21
22 #[error("io error: {message}{}", working_dir.as_ref().map(|d| format!(" (in {})", d.display())).unwrap_or_default())]
24 Io {
25 message: String,
26 #[source]
27 source: std::io::Error,
28 working_dir: Option<PathBuf>,
29 },
30
31 #[error("codex command timed out after {timeout_seconds}s")]
33 Timeout { timeout_seconds: u64 },
34
35 #[cfg(feature = "json")]
37 #[error("json parse error: {message}")]
38 Json {
39 message: String,
40 #[source]
41 source: serde_json::Error,
42 },
43
44 #[error("CLI version {found} does not meet minimum requirement {minimum}")]
46 VersionMismatch {
47 found: crate::version::CliVersion,
48 minimum: crate::version::CliVersion,
49 },
50}
51
52impl From<std::io::Error> for Error {
53 fn from(e: std::io::Error) -> Self {
54 Self::Io {
55 message: e.to_string(),
56 source: e,
57 working_dir: None,
58 }
59 }
60}
61
62pub type Result<T> = std::result::Result<T, Error>;
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn display_not_found() {
71 let err = Error::NotFound;
72 assert_eq!(err.to_string(), "codex binary not found in PATH");
73 }
74
75 #[test]
76 fn display_command_failed_minimal() {
77 let err = Error::CommandFailed {
78 command: "exec".to_string(),
79 exit_code: 1,
80 stdout: String::new(),
81 stderr: String::new(),
82 working_dir: None,
83 };
84 assert_eq!(err.to_string(), "codex command failed: exec (exit code 1)");
85 }
86
87 #[test]
88 fn display_command_failed_with_all_fields() {
89 let err = Error::CommandFailed {
90 command: "exec".to_string(),
91 exit_code: 2,
92 stdout: "out".to_string(),
93 stderr: "err".to_string(),
94 working_dir: Some(PathBuf::from("/tmp")),
95 };
96 assert_eq!(
97 err.to_string(),
98 "codex command failed: exec (exit code 2) (in /tmp)\nstdout: out\nstderr: err"
99 );
100 }
101
102 #[test]
103 fn display_io_without_working_dir() {
104 let source = std::io::Error::other("disk full");
105 let err = Error::Io {
106 message: source.to_string(),
107 source,
108 working_dir: None,
109 };
110 assert_eq!(err.to_string(), "io error: disk full");
111 }
112
113 #[test]
114 fn display_io_with_working_dir() {
115 let source = std::io::Error::other("disk full");
116 let err = Error::Io {
117 message: source.to_string(),
118 source,
119 working_dir: Some(PathBuf::from("/home/user")),
120 };
121 assert_eq!(err.to_string(), "io error: disk full (in /home/user)");
122 }
123
124 #[test]
125 fn display_timeout() {
126 let err = Error::Timeout {
127 timeout_seconds: 30,
128 };
129 assert_eq!(err.to_string(), "codex command timed out after 30s");
130 }
131
132 #[cfg(feature = "json")]
133 #[test]
134 fn display_json() {
135 let source: serde_json::Error =
136 serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
137 let err = Error::Json {
138 message: source.to_string(),
139 source,
140 };
141 assert!(err.to_string().starts_with("json parse error:"));
142 }
143
144 #[test]
145 fn display_version_mismatch() {
146 let err = Error::VersionMismatch {
147 found: crate::version::CliVersion::new(0, 100, 0),
148 minimum: crate::version::CliVersion::new(0, 116, 0),
149 };
150 assert_eq!(
151 err.to_string(),
152 "CLI version 0.100.0 does not meet minimum requirement 0.116.0"
153 );
154 }
155}