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