Skip to main content

buildfix_edit/
error.rs

1//! Error types for buildfix-edit.
2//!
3//! This module defines error types that distinguish between:
4//! - Policy blocks (exit code 2): precondition mismatch, safety gate denial, allowlist/denylist
5//! - Runtime errors (exit code 1): I/O errors, parse errors, invalid arguments
6
7use thiserror::Error;
8
9/// The top-level error type for buildfix-edit operations.
10#[derive(Debug, Error)]
11pub enum EditError {
12    /// A policy block occurred (exit code 2).
13    /// This includes precondition failures, safety gate denials, and policy denials.
14    #[error("policy block: {0}")]
15    PolicyBlock(#[from] PolicyBlockError),
16
17    /// A runtime/tool error occurred (exit code 1).
18    /// This includes I/O errors, parse errors, and invalid arguments.
19    #[error("runtime error: {0}")]
20    Runtime(#[from] anyhow::Error),
21}
22
23/// Policy block errors that should result in exit code 2.
24#[derive(Debug, Error)]
25pub enum PolicyBlockError {
26    /// One or more preconditions failed (file changed, missing, sha256 mismatch).
27    #[error("precondition mismatch: {message}")]
28    PreconditionMismatch {
29        /// A descriptive message about which preconditions failed.
30        message: String,
31    },
32
33    /// A fix was denied by the safety gate (guarded/unsafe not allowed).
34    #[error("safety gate denial: {message}")]
35    SafetyGateDenial {
36        /// A descriptive message about which safety class was blocked.
37        message: String,
38    },
39
40    /// A fix was denied by the allow/deny policy.
41    #[error("policy denial: {message}")]
42    PolicyDenial {
43        /// A descriptive message about which policy blocked the fix.
44        message: String,
45    },
46
47    /// Caps exceeded (max operations, max files, max diff size).
48    #[error("caps exceeded: {message}")]
49    CapsExceeded {
50        /// A descriptive message about which cap was exceeded.
51        message: String,
52    },
53}
54
55impl EditError {
56    /// Returns true if this is a policy block error (exit code 2).
57    pub fn is_policy_block(&self) -> bool {
58        matches!(self, EditError::PolicyBlock(_))
59    }
60
61    /// Returns the recommended exit code for this error.
62    pub fn exit_code(&self) -> u8 {
63        match self {
64            EditError::PolicyBlock(_) => 2,
65            EditError::Runtime(_) => 1,
66        }
67    }
68}
69
70/// Result type alias using EditError.
71pub type EditResult<T> = Result<T, EditError>;
72
73#[cfg(test)]
74mod tests {
75    use super::{EditError, PolicyBlockError};
76
77    #[test]
78    fn policy_block_reports_exit_code_2() {
79        let err = EditError::from(PolicyBlockError::PreconditionMismatch {
80            message: "oops".to_string(),
81        });
82        assert!(err.is_policy_block());
83        assert_eq!(err.exit_code(), 2);
84        assert!(err.to_string().contains("policy block"));
85    }
86
87    #[test]
88    fn runtime_error_reports_exit_code_1() {
89        let err = EditError::from(anyhow::anyhow!("boom"));
90        assert!(!err.is_policy_block());
91        assert_eq!(err.exit_code(), 1);
92        assert!(err.to_string().contains("runtime error"));
93    }
94
95    #[test]
96    fn policy_block_display_includes_variant() {
97        let err = PolicyBlockError::SafetyGateDenial {
98            message: "guarded".to_string(),
99        };
100        assert!(err.to_string().contains("safety gate denial"));
101        assert!(err.to_string().contains("guarded"));
102    }
103}