Skip to main content

bock_ai/
error.rs

1//! Error types for the AI provider interface.
2//!
3//! All provider operations return `Result<_, AiError>`. The codegen
4//! pipeline inspects the variant to decide whether to fall back to
5//! Tier 2 rule-based generation (per §17.2) or fail the build.
6
7use thiserror::Error;
8
9/// Errors produced by an [`AiProvider`](crate::provider::AiProvider) call.
10///
11/// The variants capture the categories the codegen pipeline distinguishes
12/// between when deciding whether to retry, fall back, or surface the
13/// failure. They are intentionally transport-agnostic — concrete HTTP
14/// status codes or SDK errors should be mapped into one of these.
15#[derive(Debug, Clone, Error)]
16pub enum AiError {
17    /// Transport-level failure reaching the provider (DNS, connect, TLS,
18    /// read/write errors). Safe to retry.
19    #[error("AI provider network error: {0}")]
20    Network(String),
21
22    /// Authentication failed — bad API key, missing credential, revoked
23    /// token. Not retryable without re-configuring.
24    #[error("AI provider authentication failed: {0}")]
25    Auth(String),
26
27    /// The request did not complete within the configured timeout.
28    #[error("AI provider request timed out: {0}")]
29    Timeout(String),
30
31    /// The provider returned a rate-limit response (HTTP 429 or equivalent).
32    /// Callers may retry with backoff.
33    #[error("AI provider rate limited: {0}")]
34    RateLimited(String),
35
36    /// The configured cost or token budget has been exhausted. Not retryable
37    /// until the budget is replenished.
38    #[error("AI provider budget exceeded: {0}")]
39    BudgetExceeded(String),
40
41    /// The provider returned an error response that does not fit a more
42    /// specific variant (5xx, provider-side validation, model error).
43    #[error("AI provider error: {0}")]
44    ProviderError(String),
45
46    /// The provider is temporarily unavailable — compiler should fall back
47    /// to Tier 2 rule-based generation if `deterministic_fallback` is set.
48    #[error("AI provider unavailable: {0}")]
49    Unavailable(String),
50
51    /// The provider returned a response that failed structural validation —
52    /// for example, a `select()` response whose `selected_id` was not in
53    /// the provided option set (see [`validate_select_response`]).
54    #[error("invalid AI provider response: {0}")]
55    InvalidResponse(String),
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn variants_format_distinct_messages() {
64        let errs = [
65            AiError::Network("dns".into()),
66            AiError::Auth("401".into()),
67            AiError::Timeout("30s".into()),
68            AiError::RateLimited("429".into()),
69            AiError::BudgetExceeded("cap".into()),
70            AiError::ProviderError("500".into()),
71            AiError::Unavailable("down".into()),
72            AiError::InvalidResponse("bad id".into()),
73        ];
74        let msgs: Vec<String> = errs.iter().map(|e| format!("{e}")).collect();
75        for (i, m) in msgs.iter().enumerate() {
76            for (j, n) in msgs.iter().enumerate() {
77                if i != j {
78                    assert_ne!(m, n, "variants {i} and {j} produced identical messages");
79                }
80            }
81        }
82    }
83
84    #[test]
85    fn debug_output_is_serializable_to_string() {
86        // Acceptance criterion: variants serializable for debug output.
87        let e = AiError::ProviderError("boom".into());
88        let dbg = format!("{e:?}");
89        assert!(dbg.contains("ProviderError"));
90        assert!(dbg.contains("boom"));
91    }
92
93    #[test]
94    fn error_trait_implemented() {
95        fn assert_error<E: std::error::Error>() {}
96        assert_error::<AiError>();
97    }
98}