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}