Skip to main content

agent_harness_adapter_core/
error.rs

1//! The crate's error type.
2// @zen-component: KIT-Error
3
4use std::{fmt, io, path::PathBuf};
5
6use crate::harness::Scope;
7
8/// Everything that can go wrong in the library. Every variant but [`Error::Io`]
9/// and [`Error::Internal`] is a refusal: `install` returns it before any
10/// write.
11// @zen-impl: KIT-16_AC-1
12#[derive(Debug)]
13#[non_exhaustive]
14pub enum Error {
15    /// A scope name that is not `project` or `user`.
16    UnknownScope {
17        /// The name given.
18        name: String,
19    },
20    /// A hook event name that is not one of [`Event`](crate::hook::Event)'s.
21    UnknownEvent {
22        /// The name given.
23        name: String,
24    },
25    /// A harness id the tool does not support.
26    UnknownHarness {
27        /// The harness id given.
28        harness: String,
29    },
30    /// A harness that has no files at the scope.
31    UnsupportedScope {
32        /// The harness id.
33        harness: String,
34        /// The scope given.
35        scope: Scope,
36    },
37    /// A part name (e.g. in `--without`) no named harness's profile has.
38    UnknownPart {
39        /// The harness, or the harnesses named, comma-separated.
40        harness: String,
41        /// The part name given.
42        part: String,
43    },
44    /// Something a harness cannot do, e.g. an answer it cannot express for
45    /// an event.
46    Unsupported {
47        /// The harness id.
48        harness: String,
49        /// What it cannot do, e.g. `answer deny at stop`.
50        what: String,
51    },
52    /// A file the library must read does not parse or has the wrong shape.
53    File {
54        /// The file, as the tool shows it.
55        file: String,
56        /// What is wrong with it.
57        message: String,
58    },
59    /// A refusal from the tool's own [`ExternalPart`] or [`DeclinedStore`].
60    ///
61    /// [`ExternalPart`]: crate::harness::ExternalPart
62    /// [`DeclinedStore`]: crate::harness::DeclinedStore
63    Refused(String),
64    /// An I/O failure on `path`.
65    Io {
66        /// The path being operated on.
67        path: PathBuf,
68        /// The underlying I/O error.
69        source: io::Error,
70    },
71    /// A bug, or an inconsistent integration or harness.
72    Internal(String),
73}
74
75impl Error {
76    /// An [`Error::Io`] for `path` from an [`io::Error`].
77    pub fn io(path: impl Into<PathBuf>, source: io::Error) -> Self {
78        Error::Io {
79            path: path.into(),
80            source,
81        }
82    }
83
84    /// An [`Error::File`]: `file` (as the tool shows it) and what is wrong.
85    pub fn file(file: impl Into<String>, message: impl Into<String>) -> Self {
86        Error::File {
87            file: file.into(),
88            message: message.into(),
89        }
90    }
91}
92
93impl fmt::Display for Error {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Error::UnknownScope { name } => write!(f, "no scope named `{name}`"),
97            Error::UnknownEvent { name } => write!(f, "no hook event named `{name}`"),
98            Error::UnknownHarness { harness } => write!(f, "no harness named `{harness}`"),
99            Error::UnsupportedScope { harness, scope } => {
100                write!(f, "harness `{harness}` has no {scope} scope")
101            }
102            Error::UnknownPart { harness, part } => {
103                write!(f, "harness `{harness}` has no part named `{part}`")
104            }
105            Error::Unsupported { harness, what } => {
106                write!(f, "harness `{harness}` cannot {what}")
107            }
108            Error::File { file, message } => write!(f, "{file}: {message}"),
109            Error::Refused(message) => f.write_str(message),
110            Error::Io { path, source } => write!(f, "{}: {source}", path.display()),
111            Error::Internal(message) => write!(f, "internal: {message}"),
112        }
113    }
114}
115
116impl std::error::Error for Error {
117    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
118        match self {
119            Error::Io { source, .. } => Some(source),
120            _ => None,
121        }
122    }
123}
124
125/// The crate's result type.
126pub type Result<T> = std::result::Result<T, Error>;
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    // @zen-test: KIT-16_AC-1
133    #[test]
134    fn each_kind_displays_one_line() {
135        let e = Error::io(
136            "/tmp/x",
137            io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
138        );
139        let msg = e.to_string();
140        assert!(msg.contains("/tmp/x") && msg.contains("denied"), "{msg}");
141        assert!(std::error::Error::source(&e).is_some());
142        let cases = [
143            (
144                Error::UnknownScope { name: "g".into() },
145                "no scope named `g`",
146            ),
147            (
148                Error::UnknownEvent { name: "e".into() },
149                "no hook event named `e`",
150            ),
151            (
152                Error::UnknownHarness {
153                    harness: "x".into(),
154                },
155                "no harness named `x`",
156            ),
157            (
158                Error::UnsupportedScope {
159                    harness: "cursor".into(),
160                    scope: Scope::Local,
161                },
162                "harness `cursor` has no local scope",
163            ),
164            (
165                Error::UnknownPart {
166                    harness: "claude".into(),
167                    part: "p".into(),
168                },
169                "harness `claude` has no part named `p`",
170            ),
171            (
172                Error::Unsupported {
173                    harness: "copilot".into(),
174                    what: "answer context at stop".into(),
175                },
176                "harness `copilot` cannot answer context at stop",
177            ),
178            (
179                Error::file("a.json", "is not a JSON object"),
180                "a.json: is not a JSON object",
181            ),
182            (Error::Refused("could not run x".into()), "could not run x"),
183            (Error::Internal("bug".into()), "internal: bug"),
184        ];
185        for (e, text) in cases {
186            assert_eq!(e.to_string(), text);
187            assert!(std::error::Error::source(&e).is_none());
188        }
189    }
190}