Skip to main content

rec/doctor/
mod.rs

1//! Diagnostic checks for installation health.
2//!
3//! Runs a suite of checks (shell hooks, config, storage, etc.) and produces
4//! a report with pass/fail/warn status and fix hints.
5
6pub mod checks;
7pub mod report;
8
9pub use checks::*;
10pub use report::*;
11
12/// Run all diagnostic checks and return a vector of results.
13#[must_use]
14pub fn run_all_checks() -> Vec<CheckResult> {
15    vec![
16        check_rec_version(),
17        check_rec_in_path(),
18        check_shell_detected(),
19        check_shell_hooks_installed(),
20        check_config_valid(),
21        check_storage_dir_exists(),
22        check_storage_writable(),
23        check_rc_file_writable(),
24        check_data_dir_permissions(),
25    ]
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn check_rec_version_always_passes() {
34        let result = check_rec_version();
35        assert_eq!(result.status, CheckStatus::Pass);
36        assert_eq!(result.name, "rec version");
37        assert!(!result.message.is_empty());
38        assert!(result.fix_hint.is_none());
39    }
40
41    #[test]
42    fn check_rec_in_path_returns_result() {
43        let result = check_rec_in_path();
44        assert_eq!(result.name, "rec in PATH");
45    }
46
47    #[test]
48    fn check_shell_detected_returns_result() {
49        let result = check_shell_detected();
50        assert_eq!(result.name, "Shell detected");
51    }
52
53    #[test]
54    fn check_config_valid_returns_result() {
55        let result = check_config_valid();
56        assert_eq!(result.name, "Config file");
57        assert!(
58            result.status == CheckStatus::Pass,
59            "Expected pass, got: {:?} — {}",
60            result.status,
61            result.message
62        );
63    }
64
65    #[test]
66    fn run_all_checks_returns_nine_results() {
67        let results = run_all_checks();
68        assert_eq!(results.len(), 9, "Expected 9 checks, got {}", results.len());
69    }
70
71    #[test]
72    fn format_report_contains_header_and_summary() {
73        let results = vec![
74            CheckResult {
75                name: "test check",
76                status: CheckStatus::Pass,
77                message: "ok".to_string(),
78                fix_hint: None,
79            },
80            CheckResult {
81                name: "warn check",
82                status: CheckStatus::Warn,
83                message: "maybe".to_string(),
84                fix_hint: Some("try this".to_string()),
85            },
86            CheckResult {
87                name: "fail check",
88                status: CheckStatus::Fail,
89                message: "bad".to_string(),
90                fix_hint: Some("fix it".to_string()),
91            },
92        ];
93
94        let report = format_report(&results, false);
95        assert!(report.starts_with("rec doctor"), "Should start with header");
96        assert!(
97            report.contains("test check: ok"),
98            "Should contain pass check"
99        );
100        assert!(
101            report.contains("warn check: maybe"),
102            "Should contain warn check"
103        );
104        assert!(
105            report.contains("fail check: bad"),
106            "Should contain fail check"
107        );
108        assert!(report.contains("Fix: try this"), "Should contain fix hint");
109        assert!(report.contains("Fix: fix it"), "Should contain fix hint");
110        assert!(
111            report.contains("1 passed, 1 warnings, 1 failed"),
112            "Should contain summary: got {report}"
113        );
114    }
115
116    #[test]
117    fn format_report_colored_has_ansi_escapes() {
118        let results = vec![CheckResult {
119            name: "color test",
120            status: CheckStatus::Pass,
121            message: "ok".to_string(),
122            fix_hint: None,
123        }];
124
125        let report = format_report(&results, true);
126        assert!(
127            report.contains("\x1b[32m"),
128            "Colored report should contain green ANSI code"
129        );
130    }
131
132    #[test]
133    fn format_report_json_structure() {
134        let results = vec![
135            CheckResult {
136                name: "a",
137                status: CheckStatus::Pass,
138                message: "ok".to_string(),
139                fix_hint: None,
140            },
141            CheckResult {
142                name: "b",
143                status: CheckStatus::Fail,
144                message: "bad".to_string(),
145                fix_hint: Some("fix".to_string()),
146            },
147        ];
148
149        let json_val = format_report_json(&results);
150        let checks = json_val["checks"].as_array().unwrap();
151        assert_eq!(checks.len(), 2);
152
153        assert_eq!(checks[0]["name"], "a");
154        assert_eq!(checks[0]["status"], "pass");
155        assert!(checks[0]["fix_hint"].is_null());
156
157        assert_eq!(checks[1]["name"], "b");
158        assert_eq!(checks[1]["status"], "fail");
159        assert_eq!(checks[1]["fix_hint"], "fix");
160
161        assert_eq!(json_val["summary"]["pass"], 1);
162        assert_eq!(json_val["summary"]["warn"], 0);
163        assert_eq!(json_val["summary"]["fail"], 1);
164    }
165
166    #[test]
167    fn check_status_as_str() {
168        assert_eq!(CheckStatus::Pass.as_str(), "pass");
169        assert_eq!(CheckStatus::Warn.as_str(), "warn");
170        assert_eq!(CheckStatus::Fail.as_str(), "fail");
171    }
172
173    #[test]
174    fn fail_results_always_have_fix_hints() {
175        let results = run_all_checks();
176        for result in &results {
177            if result.status == CheckStatus::Fail {
178                assert!(
179                    result.fix_hint.is_some(),
180                    "Fail result '{}' should have a fix_hint",
181                    result.name
182                );
183            }
184        }
185    }
186}