Skip to main content

codex_wrapper/
error.rs

1//! Error types for `codex-wrapper`.
2
3use std::path::PathBuf;
4
5/// Errors returned by `codex-wrapper` operations.
6///
7/// This enum is `#[non_exhaustive]`: match arms must include a `_` catch-all so
8/// new variants can be added without a breaking change. This mirrors
9/// `claude-wrapper`'s `Error` for cross-crate consistency.
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum Error {
13    /// The `codex` binary was not found in PATH.
14    #[error("codex binary not found in PATH")]
15    NotFound,
16
17    /// A codex command failed with a non-zero exit code.
18    #[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    /// An I/O error occurred while spawning or communicating with the process.
28    #[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    /// The command timed out.
37    #[error("codex command timed out after {timeout_seconds}s")]
38    Timeout { timeout_seconds: u64 },
39
40    /// JSON parsing failed.
41    #[cfg(feature = "json")]
42    #[error("json parse error: {message}")]
43    Json {
44        message: String,
45        #[source]
46        source: serde_json::Error,
47    },
48
49    /// The installed CLI version does not meet the minimum requirement.
50    #[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
67/// Result type alias for codex-wrapper operations.
68pub 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}