macro_rules! stub_error {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum $name {
NotImplemented,
}
impl $name {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::NotImplemented => "NotImplemented",
}
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
impl std::error::Error for $name {}
};
}
use std::path::PathBuf;
stub_error!(
SettingsError,
"Errors from reading `~/.claude/settings.json` + overlays."
);
stub_error!(ClaudeJsonError, "Errors from reading `~/.claude.json`.");
stub_error!(SessionError, "Errors from the live sessions directory.");
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum GitError {
CorruptRepo { path: PathBuf, message: String },
WalkFailed { path: PathBuf, message: String },
}
impl GitError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::CorruptRepo { .. } => "CorruptRepo",
Self::WalkFailed { .. } => "WalkFailed",
}
}
}
impl std::fmt::Display for GitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CorruptRepo { path, message } => {
write!(f, "gix failed to open {}: {message}", path.display())
}
Self::WalkFailed { path, message } => {
write!(f, "gix walk at {} failed: {message}", path.display())
}
}
}
}
impl std::error::Error for GitError {}
pub use super::credentials::CredentialError;
pub use super::jsonl::JsonlError;
use std::time::Duration;
#[derive(Debug)]
#[non_exhaustive]
pub enum UsageError {
NoCredentials,
Credentials(CredentialError),
Timeout,
RateLimited { retry_after: Option<Duration> },
NetworkError,
ParseError,
Unauthorized,
Jsonl(JsonlError),
}
impl UsageError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::NoCredentials => "NoCredentials",
Self::Credentials(inner) => inner.code(),
Self::Timeout => "Timeout",
Self::RateLimited { .. } => "RateLimited",
Self::NetworkError => "NetworkError",
Self::ParseError => "ParseError",
Self::Unauthorized => "Unauthorized",
Self::Jsonl(inner) => inner.code(),
}
}
}
impl std::fmt::Display for UsageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoCredentials => f.write_str("no OAuth credentials found"),
Self::Credentials(inner) => write!(f, "credentials error: {inner}"),
Self::Timeout => f.write_str("endpoint request timed out"),
Self::RateLimited {
retry_after: Some(d),
} => write!(f, "endpoint rate-limited; retry after {}s", d.as_secs()),
Self::RateLimited { retry_after: None } => {
f.write_str("endpoint rate-limited (no Retry-After)")
}
Self::NetworkError => f.write_str("network error"),
Self::ParseError => f.write_str("endpoint response failed to parse"),
Self::Unauthorized => f.write_str("endpoint returned 401 Unauthorized"),
Self::Jsonl(inner) => write!(f, "JSONL fallback failed: {inner}"),
}
}
}
impl std::error::Error for UsageError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Credentials(inner) => Some(inner),
Self::Jsonl(inner) => Some(inner),
_ => None,
}
}
}
#[cfg(test)]
mod usage_error_tests {
use super::*;
#[test]
fn display_covers_every_variant() {
let cases: &[(UsageError, &str)] = &[
(UsageError::NoCredentials, "no OAuth credentials found"),
(
UsageError::Credentials(CredentialError::NoCredentials),
"credentials error: no OAuth credentials found",
),
(UsageError::Timeout, "endpoint request timed out"),
(
UsageError::RateLimited {
retry_after: Some(Duration::from_secs(42)),
},
"endpoint rate-limited; retry after 42s",
),
(
UsageError::RateLimited { retry_after: None },
"endpoint rate-limited (no Retry-After)",
),
(UsageError::NetworkError, "network error"),
(UsageError::ParseError, "endpoint response failed to parse"),
(
UsageError::Unauthorized,
"endpoint returned 401 Unauthorized",
),
(
UsageError::Jsonl(JsonlError::NoEntries),
"JSONL fallback failed: Claude Code project directory has no JSONL entries",
),
];
for (err, expected) in cases {
assert_eq!(err.to_string(), *expected);
}
}
#[test]
fn code_flattens_wrapping_variants_via_delegation() {
assert_eq!(UsageError::NoCredentials.code(), "NoCredentials");
assert_eq!(UsageError::Timeout.code(), "Timeout");
assert_eq!(
UsageError::RateLimited { retry_after: None }.code(),
"RateLimited",
);
assert_eq!(UsageError::NetworkError.code(), "NetworkError");
assert_eq!(UsageError::ParseError.code(), "ParseError");
assert_eq!(UsageError::Unauthorized.code(), "Unauthorized");
assert_eq!(
UsageError::Credentials(CredentialError::NoCredentials).code(),
"NoCredentials",
);
assert_eq!(UsageError::Jsonl(JsonlError::NoEntries).code(), "NoEntries",);
}
#[test]
fn source_chains_through_wrapping_variants() {
use std::error::Error;
let wrapped = UsageError::Credentials(CredentialError::IoError {
path: std::path::PathBuf::from("/x"),
cause: std::io::Error::other("boom"),
});
let source = wrapped.source().unwrap();
assert!(source.source().is_some());
let credless = UsageError::Credentials(CredentialError::NoCredentials);
let source = credless.source().unwrap();
assert!(source.source().is_none());
let wrapped_jsonl = UsageError::Jsonl(JsonlError::NoEntries);
assert!(wrapped_jsonl.source().is_some());
let bare = UsageError::NoCredentials;
assert!(bare.source().is_none());
}
}