Skip to main content

auths_sdk/ports/
diagnostics.rs

1//! Diagnostic provider ports for system health checks.
2//!
3//! Follows Interface Segregation: one trait per tool category so tests
4//! only mock what they need.
5
6use serde::{Deserialize, Serialize};
7
8/// Category of a diagnostic check — determines exit code behavior.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum CheckCategory {
11    /// Critical: if this check fails, Auths is non-functional.
12    /// Doctor should exit 1 if any Critical check fails.
13    Critical,
14    /// Advisory: if this check fails, Auths works but environment is not ideal.
15    /// Doctor exits 2 if all Critical checks pass but some Advisory checks fail.
16    Advisory,
17}
18
19/// A structured issue found during git signing configuration checks.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum ConfigIssue {
22    /// A key exists but has the wrong value.
23    Mismatch {
24        /// The git config key name.
25        key: String,
26        /// The expected value.
27        expected: String,
28        /// The actual value found.
29        actual: String,
30    },
31    /// A required key is not set.
32    Absent(String),
33}
34
35/// Result of a single diagnostic check.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CheckResult {
38    /// Human-readable name of the diagnostic check.
39    pub name: String,
40    /// Whether the check passed.
41    pub passed: bool,
42    /// Free-form informational text (e.g. version strings).
43    pub message: Option<String>,
44    /// Structured config issues — populated only by config checks.
45    #[serde(default)]
46    pub config_issues: Vec<ConfigIssue>,
47    /// Category of this check (Critical vs Advisory).
48    /// Critical checks failing means Auths is non-functional.
49    /// Advisory checks failing means environment is suboptimal.
50    #[serde(default = "default_check_category")]
51    pub category: CheckCategory,
52}
53
54fn default_check_category() -> CheckCategory {
55    CheckCategory::Advisory
56}
57
58/// Aggregated diagnostic report.
59#[derive(Debug, Serialize, Deserialize)]
60pub struct DiagnosticReport {
61    /// All individual check results in the report.
62    pub checks: Vec<CheckResult>,
63}
64
65/// Errors from diagnostic check execution.
66#[derive(Debug, thiserror::Error)]
67pub enum DiagnosticError {
68    /// A diagnostic check failed to execute.
69    #[error("check failed to execute: {0}")]
70    ExecutionFailed(String),
71
72    /// The requested check name does not exist.
73    #[error("check not found: {0}")]
74    CheckNotFound(String),
75}
76
77/// Port for Git-related diagnostic checks.
78///
79/// Usage:
80/// ```ignore
81/// let result = provider.check_git_version()?;
82/// let config_val = provider.get_git_config("user.email")?;
83/// ```
84pub trait GitDiagnosticProvider: Send + Sync {
85    /// Check that git is installed and return version info.
86    fn check_git_version(&self) -> Result<CheckResult, DiagnosticError>;
87
88    /// Read a global git config value, returning `None` if unset.
89    fn get_git_config(&self, key: &str) -> Result<Option<String>, DiagnosticError>;
90}
91
92/// Port for cryptographic tool diagnostic checks.
93///
94/// Usage:
95/// ```ignore
96/// let result = provider.check_ssh_keygen_available()?;
97/// ```
98pub trait CryptoDiagnosticProvider: Send + Sync {
99    /// Check that ssh-keygen is available on the system.
100    fn check_ssh_keygen_available(&self) -> Result<CheckResult, DiagnosticError>;
101
102    /// Return the raw SSH version string from `ssh -V`.
103    ///
104    /// Default returns "unknown" so existing implementors are not broken.
105    fn check_ssh_version(&self) -> Result<String, DiagnosticError> {
106        Ok("unknown".to_string())
107    }
108}
109
110impl<T: GitDiagnosticProvider> GitDiagnosticProvider for &T {
111    fn check_git_version(&self) -> Result<CheckResult, DiagnosticError> {
112        (**self).check_git_version()
113    }
114    fn get_git_config(&self, key: &str) -> Result<Option<String>, DiagnosticError> {
115        (**self).get_git_config(key)
116    }
117}
118
119/// A fix that can be applied to resolve a failed diagnostic check.
120///
121/// Usage:
122/// ```ignore
123/// if fix.can_fix(&check) {
124///     if fix.is_safe() || user_confirmed() {
125///         let message = fix.apply()?;
126///     }
127/// }
128/// ```
129pub trait DiagnosticFix: Send + Sync {
130    /// Human-readable name of this fix.
131    fn name(&self) -> &str;
132
133    /// Whether this fix is safe to apply without user confirmation.
134    fn is_safe(&self) -> bool;
135
136    /// Whether this fix can address the given failed check.
137    fn can_fix(&self, check: &CheckResult) -> bool;
138
139    /// Apply the fix, returning a description of what was done.
140    fn apply(&self) -> Result<String, DiagnosticError>;
141}
142
143/// Record of a fix that was applied.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct FixApplied {
146    /// Name of the fix.
147    pub name: String,
148    /// Description of what was done.
149    pub message: String,
150}
151
152impl<T: CryptoDiagnosticProvider> CryptoDiagnosticProvider for &T {
153    fn check_ssh_keygen_available(&self) -> Result<CheckResult, DiagnosticError> {
154        (**self).check_ssh_keygen_available()
155    }
156    fn check_ssh_version(&self) -> Result<String, DiagnosticError> {
157        (**self).check_ssh_version()
158    }
159}