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}