Skip to main content

auths_cli/commands/
doctor.rs

1//! Comprehensive health check command for Auths.
2
3use crate::adapters::doctor_fixes::GitSigningConfigFix;
4use crate::adapters::system_diagnostic::PosixDiagnosticAdapter;
5use crate::ux::format::{JsonResponse, Output, is_json_mode};
6use anyhow::Result;
7use auths_sdk::keychain;
8use auths_sdk::ports::diagnostics::{
9    CheckCategory, CheckResult, ConfigIssue, DiagnosticFix, FixApplied,
10};
11use auths_sdk::workflows::diagnostics::DiagnosticsWorkflow;
12use chrono::{DateTime, Utc};
13use clap::Parser;
14use serde::Serialize;
15use std::io::IsTerminal;
16
17/// Health check command.
18#[derive(Parser, Debug, Clone)]
19#[command(
20    name = "doctor",
21    about = "Run comprehensive health checks",
22    after_help = "Examples:
23  auths doctor              # Check all health aspects
24  auths doctor --fix        # Auto-fix identified issues
25  auths doctor --json       # JSON output
26
27Exit Codes:
28  0 — All checks pass
29  1 — Critical check failed (Auths is non-functional)
30  2 — Critical checks pass, advisory checks fail (environment could be better)
31
32Related:
33  auths status  — Show identity and device status
34  auths init    — Initialize a new identity"
35)]
36pub struct DoctorCommand {
37    /// Auto-fix issues where possible
38    #[clap(long)]
39    pub fix: bool,
40}
41
42/// A single health check.
43#[derive(Debug, Serialize)]
44pub struct Check {
45    name: String,
46    passed: bool,
47    detail: String,
48    suggestion: Option<String>,
49    #[serde(skip_serializing_if = "is_advisory")]
50    category: CheckCategory,
51}
52
53fn is_advisory(cat: &CheckCategory) -> bool {
54    *cat == CheckCategory::Advisory
55}
56
57/// Overall doctor report.
58#[derive(Debug, Serialize)]
59pub struct DoctorReport {
60    pub version: String,
61    pub checks: Vec<Check>,
62    pub all_pass: bool,
63    #[serde(skip_serializing_if = "Vec::is_empty")]
64    pub fixes_applied: Vec<FixApplied>,
65}
66
67/// Handle the doctor command.
68pub fn handle_doctor(cmd: DoctorCommand) -> Result<()> {
69    let checks = run_checks();
70    let all_pass = checks.iter().all(|c| c.passed);
71
72    let (final_checks, fixes_applied) = if cmd.fix && !all_pass {
73        let out = if !is_json_mode() {
74            Some(Output::new())
75        } else {
76            None
77        };
78        let fixes = apply_fixes(&checks, out.as_ref());
79        if !fixes.is_empty() {
80            let rechecked = run_checks();
81            (rechecked, fixes)
82        } else {
83            (checks, fixes)
84        }
85    } else {
86        (checks, Vec::new())
87    };
88
89    let all_pass = final_checks.iter().all(|c| c.passed);
90
91    // Compute exit code based on check categories
92    let exit_code = compute_exit_code(&final_checks);
93
94    let report = DoctorReport {
95        version: env!("CARGO_PKG_VERSION").to_string(),
96        checks: final_checks,
97        all_pass,
98        fixes_applied,
99    };
100
101    if is_json_mode() {
102        JsonResponse {
103            success: all_pass,
104            command: "doctor".to_string(),
105            data: Some(report),
106            error: if !all_pass {
107                Some("some health checks failed".to_string())
108            } else {
109                None
110            },
111        }
112        .print()?;
113    } else {
114        print_report(&report);
115    }
116
117    if exit_code != 0 {
118        std::process::exit(exit_code);
119    }
120
121    Ok(())
122}
123
124/// Compute exit code based on check categories.
125///
126/// Returns:
127/// * 0 — all checks pass
128/// * 1 — at least one Critical check fails (Auths is non-functional)
129/// * 2 — all Critical checks pass, at least one Advisory check fails
130fn compute_exit_code(checks: &[Check]) -> i32 {
131    let critical_failures = checks
132        .iter()
133        .any(|c| !c.passed && c.category == CheckCategory::Critical);
134
135    if critical_failures {
136        return 1;
137    }
138
139    let advisory_failures = checks
140        .iter()
141        .any(|c| !c.passed && c.category == CheckCategory::Advisory);
142
143    if advisory_failures {
144        return 2;
145    }
146
147    0
148}
149
150/// Run all prerequisite checks.
151#[allow(clippy::disallowed_methods)] // CLI boundary: Utc::now() injected here
152fn run_checks() -> Vec<Check> {
153    let now = Utc::now();
154    let adapter = PosixDiagnosticAdapter;
155    let workflow = DiagnosticsWorkflow::new(&adapter, &adapter);
156
157    let mut checks = Vec::new();
158
159    if let Ok(report) = workflow.run() {
160        for cr in report.checks {
161            let suggestion = if cr.passed {
162                None
163            } else {
164                suggestion_for_check(&cr.name)
165            };
166            checks.push(Check {
167                name: cr.name.clone(),
168                passed: cr.passed,
169                detail: format_check_detail(&cr),
170                suggestion,
171                category: cr.category,
172            });
173        }
174    }
175
176    // Domain checks are all Critical
177    checks.push(check_keychain_accessible());
178    checks.push(check_identity_home());
179    checks.push(check_auths_repo());
180    checks.push(check_identity_valid(now));
181
182    // Advisory: commit-trailer hook wiring (plain `git commit` verifiability)
183    checks.push(check_commit_hook_installed());
184    checks.push(check_repo_hooks_path_override());
185
186    // Advisory: network connectivity
187    checks.push(check_registry_connectivity());
188
189    checks
190}
191
192/// Advisory: the prepare-commit-msg hook is installed, current, and wired via
193/// the global `core.hooksPath` — what makes a plain `git commit` verifiable.
194fn check_commit_hook_installed() -> Check {
195    let (passed, detail) = match auths_sdk::paths::auths_home() {
196        Ok(home) if !home.join("commit-trailers").exists() => {
197            // No identity / pre-hook install — not a failure, just not set up yet.
198            (
199                true,
200                "no identity initialized (hook not applicable)".to_string(),
201            )
202        }
203        Ok(home) => {
204            let hook_current = auths_sdk::workflows::commit_hooks::hook_is_current(&home);
205            // Resolve the *effective* core.hooksPath rather than the global one:
206            // `auths init` scopes signing config to the repo by default (a scripted
207            // init must not rewrite ~/.gitconfig), so a working local setup would
208            // otherwise be reported as broken. This is also the value git itself
209            // will use, which is the thing worth checking.
210            let hooks_path_set = crate::subprocess::git_command(&["config", "core.hooksPath"])
211                .output()
212                .ok()
213                .filter(|o| o.status.success())
214                .map(|o| {
215                    String::from_utf8_lossy(&o.stdout).trim()
216                        == home.join("githooks").to_string_lossy()
217                })
218                .unwrap_or(false);
219            match (hook_current, hooks_path_set) {
220                (true, true) => (true, "prepare-commit-msg hook installed".to_string()),
221                (false, _) => (false, "hook file missing or stale".to_string()),
222                (true, false) => (false, "core.hooksPath not pointing at the hook".to_string()),
223            }
224        }
225        Err(e) => (false, format!("could not locate ~/.auths: {e}")),
226    };
227    Check {
228        name: "Commit trailer hook".to_string(),
229        passed,
230        detail,
231        suggestion: if passed {
232            None
233        } else {
234            Some("Re-run `auths init` to reinstall the commit hook.".to_string())
235        },
236        category: CheckCategory::Advisory,
237    }
238}
239
240/// Advisory: a repo-local `core.hooksPath` (husky-style hook managers) bypasses
241/// the global auths hook — commits in this repo won't carry trailers unless the
242/// trailer hook is added to that directory.
243fn check_repo_hooks_path_override() -> Check {
244    let local_override = crate::subprocess::git_command(&["config", "--local", "core.hooksPath"])
245        .output()
246        .ok()
247        .filter(|o| o.status.success())
248        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
249        .filter(|path| !path.is_empty());
250    let (passed, detail, suggestion) = match local_override {
251        None => (
252            true,
253            "no repo-local core.hooksPath override".to_string(),
254            None,
255        ),
256        Some(path) => (
257            false,
258            format!(
259                "this repo overrides core.hooksPath to '{path}' — the auths trailer hook is bypassed"
260            ),
261            Some(format!(
262                "Copy the prepare-commit-msg hook from ~/.auths/githooks/ into '{path}' (or chain it from your hook manager)."
263            )),
264        ),
265    };
266    Check {
267        name: "Repo hook override".to_string(),
268        passed,
269        detail,
270        suggestion,
271        category: CheckCategory::Advisory,
272    }
273}
274
275fn apply_fixes(checks: &[Check], out: Option<&Output>) -> Vec<FixApplied> {
276    let failed: Vec<CheckResult> = checks
277        .iter()
278        .filter(|c| !c.passed)
279        .map(|c| CheckResult {
280            name: c.name.clone(),
281            passed: c.passed,
282            message: Some(c.detail.clone()),
283            config_issues: Vec::new(),
284            category: c.category,
285        })
286        .collect();
287
288    let fixes = build_available_fixes();
289    let interactive = std::io::stdin().is_terminal();
290    let mut applied = Vec::new();
291
292    for fix in &fixes {
293        let applicable: Vec<&CheckResult> = failed.iter().filter(|c| fix.can_fix(c)).collect();
294        if applicable.is_empty() {
295            continue;
296        }
297
298        if !fix.is_safe() && !interactive {
299            if let Some(o) = out {
300                o.print_warn(&format!(
301                    "Skipping unsafe fix '{}' (non-interactive mode)",
302                    fix.name()
303                ));
304            }
305            continue;
306        }
307
308        if !fix.is_safe() && interactive {
309            let confirm = dialoguer::Confirm::new()
310                .with_prompt(format!(
311                    "Apply fix '{}'? (may overwrite existing git config)",
312                    fix.name()
313                ))
314                .default(true)
315                .interact()
316                .unwrap_or(false);
317            if !confirm {
318                continue;
319            }
320        }
321
322        match fix.apply() {
323            Ok(message) => {
324                if let Some(o) = out {
325                    o.print_success(&format!("Fixed: {}", message));
326                }
327                applied.push(FixApplied {
328                    name: fix.name().to_string(),
329                    message,
330                });
331            }
332            Err(e) => {
333                if let Some(o) = out {
334                    o.print_error(&format!("Fix '{}' failed: {}", fix.name(), e));
335                }
336            }
337        }
338    }
339
340    applied
341}
342
343fn build_available_fixes() -> Vec<Box<dyn DiagnosticFix>> {
344    let mut fixes: Vec<Box<dyn DiagnosticFix>> = Vec::new();
345
346    if let Ok(sign_path) = which::which("auths-sign") {
347        let key_alias = resolve_key_alias().unwrap_or_else(|| "main".to_string());
348        fixes.push(Box::new(GitSigningConfigFix::new(sign_path, key_alias)));
349    }
350
351    fixes
352}
353
354fn resolve_key_alias() -> Option<String> {
355    let keychain = keychain::get_platform_keychain().ok()?;
356    let aliases = keychain.list_aliases().ok()?;
357    aliases
358        .into_iter()
359        .find(|a| !a.to_string().contains("--next-"))
360        .map(|a| a.to_string())
361}
362
363fn format_check_detail(cr: &CheckResult) -> String {
364    if !cr.config_issues.is_empty() {
365        let parts: Vec<String> = cr
366            .config_issues
367            .iter()
368            .map(|issue| match issue {
369                ConfigIssue::Mismatch {
370                    key,
371                    expected,
372                    actual,
373                } => {
374                    format!("{key} (is '{actual}', expected '{expected}')")
375                }
376                ConfigIssue::Absent(key) => format!("{key} (not set)"),
377            })
378            .collect();
379        return format!("Missing or wrong: {}", parts.join(", "));
380    }
381    cr.message.clone().unwrap_or_default()
382}
383
384fn suggestion_for_check(name: &str) -> Option<String> {
385    match name {
386        "Git installed" => {
387            Some("Install Git for your platform (see: https://git-scm.com/downloads)".to_string())
388        }
389        "Git version" => Some(
390            "Upgrade Git to 2.34.0+ for SSH signing: https://git-scm.com/downloads".to_string(),
391        ),
392        "Git user identity" => Some(
393            "Run: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\"".to_string(),
394        ),
395        "ssh-keygen installed" => {
396            let hint = if cfg!(target_os = "macos") {
397                "ssh-keygen is normally pre-installed on macOS. Check your PATH."
398            } else if cfg!(target_os = "windows") {
399                "Install OpenSSH via Settings > Apps > Optional features, or `winget install Microsoft.OpenSSH.Client`."
400            } else {
401                "Install OpenSSH: `sudo apt install openssh-client` (Debian/Ubuntu) or `sudo dnf install openssh-clients` (Fedora/RHEL)."
402            };
403            Some(hint.to_string())
404        }
405        "SSH version" => Some(
406            "Upgrade OpenSSH to 8.2+ for -Y find-principals support. Check with: ssh -V".to_string(),
407        ),
408        "Git signing config" => Some("Run: auths doctor --fix".to_string()),
409        "Auths directory" => Some("Run: auths init --profile developer".to_string()),
410        "Allowed signers file" => Some("Run: auths doctor --fix".to_string()),
411        "Registry connectivity" => {
412            Some("Check your internet connection or try again later.".to_string())
413        }
414        _ => None,
415    }
416}
417
418fn check_keychain_accessible() -> Check {
419    let (passed, detail, suggestion) = match keychain::get_platform_keychain() {
420        Ok(keychain) => (
421            true,
422            format!("{} (accessible)", keychain.backend_name()),
423            None,
424        ),
425        Err(e) => (
426            false,
427            format!("inaccessible: {e}"),
428            Some("Run: auths init --profile developer".to_string()),
429        ),
430    };
431    Check {
432        name: "System keychain".to_string(),
433        passed,
434        detail,
435        suggestion,
436        category: CheckCategory::Critical,
437    }
438}
439
440/// Advisory: report which identity home directory is in effect and why.
441///
442/// Resolution is `AUTHS_HOME` (env override, when set and non-empty) then the
443/// `~/.auths` default. Surfacing the winner and its source turns "why can't it
444/// find my identity" into a one-glance answer instead of a guessing game.
445fn check_identity_home() -> Check {
446    let env_home = std::env::var("AUTHS_HOME").ok().filter(|s| !s.is_empty());
447    let (passed, detail, suggestion) = match auths_sdk::paths::auths_home() {
448        Ok(path) => {
449            let source = if env_home.is_some() {
450                "from AUTHS_HOME"
451            } else {
452                "default ~/.auths"
453            };
454            (true, format!("{} ({source})", path.display()), None)
455        }
456        Err(e) => (
457            false,
458            format!("cannot resolve: {e}"),
459            Some("Set AUTHS_HOME, or ensure a home directory is available".to_string()),
460        ),
461    };
462    Check {
463        name: "Identity home".to_string(),
464        passed,
465        detail,
466        suggestion,
467        category: CheckCategory::Advisory,
468    }
469}
470
471fn check_auths_repo() -> Check {
472    let (passed, detail, suggestion) = match auths_sdk::paths::auths_home() {
473        Ok(path) => {
474            if !path.exists() {
475                (
476                    false,
477                    format!("{} (not found)", path.display()),
478                    Some("Run: auths init --profile developer".to_string()),
479                )
480            } else {
481                match crate::factories::storage::open_git_repo(&path) {
482                    Ok(_) => (
483                        true,
484                        format!("{} (valid git repository)", path.display()),
485                        None,
486                    ),
487                    Err(_) => (
488                        false,
489                        format!("{} (exists but not a valid git repo)", path.display()),
490                        Some("Run: auths init --profile developer".to_string()),
491                    ),
492                }
493            }
494        }
495        Err(e) => (
496            false,
497            format!("Cannot resolve path: {e}"),
498            Some("Run: auths init --profile developer".to_string()),
499        ),
500    };
501    Check {
502        name: "Auths directory".to_string(),
503        passed,
504        detail,
505        suggestion,
506        category: CheckCategory::Critical,
507    }
508}
509
510fn check_identity_valid(now: DateTime<Utc>) -> Check {
511    let (passed, detail, suggestion) = match keychain::get_platform_keychain() {
512        Ok(keychain) => match keychain.list_aliases() {
513            Ok(aliases) if aliases.is_empty() => (
514                false,
515                "No keys found in keychain".to_string(),
516                Some("Run: auths init --profile developer  (or: auths id create)".to_string()),
517            ),
518            Ok(aliases) => {
519                let key_count = aliases.len();
520                let expiry_info = check_attestation_expiry(now);
521                match expiry_info {
522                    ExpiryStatus::AllExpired(msg) => (
523                        false,
524                        format!("{key_count} key(s) found, but {msg}"),
525                        Some("Run: auths device extend".to_string()),
526                    ),
527                    ExpiryStatus::ExpiringSoon(msg) => {
528                        (true, format!("{key_count} key(s) found ({msg})"), None)
529                    }
530                    ExpiryStatus::Ok | ExpiryStatus::NoAttestations => {
531                        (true, format!("{key_count} key(s) found"), None)
532                    }
533                }
534            }
535            Err(e) => (
536                false,
537                format!("Failed to list keys: {e}"),
538                Some("Run: auths doctor  (check keychain is accessible first)".to_string()),
539            ),
540        },
541        Err(_) => (
542            false,
543            "Keychain not accessible".to_string(),
544            Some("Run: auths init --profile developer".to_string()),
545        ),
546    };
547    Check {
548        name: "Auths identity".to_string(),
549        passed,
550        detail,
551        suggestion,
552        category: CheckCategory::Critical,
553    }
554}
555
556enum ExpiryStatus {
557    Ok,
558    NoAttestations,
559    ExpiringSoon(String),
560    AllExpired(String),
561}
562
563fn check_attestation_expiry(now: DateTime<Utc>) -> ExpiryStatus {
564    use auths_sdk::storage::RegistryAttestationStorage;
565
566    let repo_path = match auths_sdk::paths::auths_home() {
567        Ok(p) if p.exists() => p,
568        _ => return ExpiryStatus::NoAttestations,
569    };
570
571    let storage = RegistryAttestationStorage::new(&repo_path);
572    let attestations = match storage
573        .load_all_enriched()
574        .map(|v| v.into_iter().map(|e| e.attestation).collect::<Vec<_>>())
575    {
576        Ok(a) => a,
577        Err(_) => return ExpiryStatus::NoAttestations,
578    };
579
580    if attestations.is_empty() {
581        return ExpiryStatus::NoAttestations;
582    }
583
584    let active: Vec<_> = attestations
585        .iter()
586        .filter(|a| a.revoked_at.is_none())
587        .collect();
588
589    if active.is_empty() {
590        return ExpiryStatus::AllExpired("all attestations revoked".to_string());
591    }
592
593    let with_expiry: Vec<_> = active.iter().filter(|a| a.expires_at.is_some()).collect();
594
595    if with_expiry.is_empty() {
596        return ExpiryStatus::Ok;
597    }
598
599    let all_expired = with_expiry
600        .iter()
601        .all(|a| a.expires_at.is_some_and(|exp| exp < now));
602
603    if all_expired {
604        return ExpiryStatus::AllExpired("all attestations expired".to_string());
605    }
606
607    let warn_threshold = now + chrono::Duration::days(7);
608    let expiring_soon = with_expiry
609        .iter()
610        .any(|a| a.expires_at.is_some_and(|exp| exp < warn_threshold));
611
612    if expiring_soon {
613        return ExpiryStatus::ExpiringSoon("some attestations expiring within 7 days".to_string());
614    }
615
616    ExpiryStatus::Ok
617}
618
619/// Report on the configured registry, if there is one.
620///
621/// There is no default registry, so with none configured this passes with
622/// "not configured" rather than failing: a registry is optional, and auths is
623/// fully functional — identity, signing, verification — without one. Previously
624/// this probed a baked-in default that returned 404, so every `auths doctor` run
625/// reported a failing check for a service the user neither had nor needed.
626fn check_registry_connectivity() -> Check {
627    let Ok(base) = std::env::var("AUTHS_REGISTRY_URL") else {
628        return Check {
629            name: "Registry connectivity".to_string(),
630            passed: true,
631            detail: "not configured (optional — signing and verification need no registry)"
632                .to_string(),
633            suggestion: None,
634            category: CheckCategory::Advisory,
635        };
636    };
637
638    let url = format!("{base}/health");
639    let client = reqwest::blocking::Client::builder()
640        .timeout(std::time::Duration::from_secs(5))
641        .build();
642
643    let (passed, detail) = match client {
644        Ok(client) => match client.get(&url).send() {
645            Ok(resp) if resp.status().is_success() => (true, format!("{base} (reachable)")),
646            Ok(resp) => (false, format!("{base} (HTTP {})", resp.status())),
647            Err(e) => (false, format!("unreachable: {e}")),
648        },
649        Err(e) => (false, format!("HTTP client error: {e}")),
650    };
651
652    Check {
653        name: "Registry connectivity".to_string(),
654        passed,
655        detail,
656        suggestion: if passed {
657            None
658        } else {
659            suggestion_for_check("Registry connectivity")
660        },
661        category: CheckCategory::Advisory,
662    }
663}
664
665/// Print the report in human-readable format.
666fn print_report(report: &DoctorReport) {
667    let out = Output::new();
668
669    out.print_heading(&format!("Auths Doctor (v{})", report.version));
670    out.println("--------------------------");
671    out.newline();
672
673    for check in &report.checks {
674        let (icon, name_styled) = if check.passed {
675            (out.success("✓"), out.bold(&check.name))
676        } else {
677            (out.error("✗"), out.error(&check.name))
678        };
679
680        out.println(&format!("[{icon}] {name_styled}: {}", check.detail));
681
682        if let Some(ref suggestion) = check.suggestion {
683            out.println(&format!("      -> {}", out.dim(suggestion)));
684        }
685    }
686
687    if !report.fixes_applied.is_empty() {
688        out.newline();
689        out.print_heading("Fixes applied:");
690        for fix in &report.fixes_applied {
691            out.println(&format!("  {} — {}", out.success(&fix.name), fix.message));
692        }
693    }
694
695    out.newline();
696
697    let passed_count = report.checks.iter().filter(|c| c.passed).count();
698    let failed_count = report.checks.len() - passed_count;
699
700    let summary = format!(
701        "Summary: {} passed, {} failed",
702        out.success(&passed_count.to_string()),
703        out.error(&failed_count.to_string())
704    );
705    out.println(&summary);
706    out.newline();
707
708    if report.all_pass {
709        out.print_success("All checks passed! Your system is ready.");
710    } else {
711        out.print_error("Some checks failed. Please review the suggestions above.");
712    }
713}
714
715impl crate::commands::executable::ExecutableCommand for DoctorCommand {
716    fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
717        handle_doctor(self.clone())
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    #[test]
724    fn test_keychain_check_suggestion_is_exact_command() {
725        let suggestion = "Run: auths init --profile developer";
726        assert!(
727            suggestion.starts_with("Run:"),
728            "suggestion must start with 'Run:'"
729        );
730    }
731
732    #[test]
733    fn test_workflow_includes_version_and_user_checks() {
734        use super::*;
735        let adapter = PosixDiagnosticAdapter;
736        let workflow = DiagnosticsWorkflow::new(&adapter, &adapter);
737        let report = workflow.run().unwrap();
738
739        let check_names: Vec<&str> = report.checks.iter().map(|c| c.name.as_str()).collect();
740        assert!(
741            check_names.contains(&"Git signing config"),
742            "signing config check must exist"
743        );
744        assert!(
745            check_names.contains(&"Git version"),
746            "git version check must exist"
747        );
748        assert!(
749            check_names.contains(&"Git user identity"),
750            "git user identity check must exist"
751        );
752    }
753
754    #[test]
755    fn test_all_failed_checks_have_exact_runnable_suggestions() {
756        let suggestions: Vec<Option<String>> = vec![
757            Some("Run: auths init --profile developer".to_string()),
758            Some("Run: auths id init".to_string()),
759            Some("Run: git config --global gpg.format ssh".to_string()),
760            Some("Run: auths init --profile developer".to_string()),
761        ];
762        for text in suggestions.into_iter().flatten() {
763            assert!(text.starts_with("Run:"), "bad suggestion: {}", text);
764        }
765    }
766
767    #[test]
768    fn test_suggestion_for_all_new_checks() {
769        use super::suggestion_for_check;
770        assert!(suggestion_for_check("Git version").is_some());
771        assert!(suggestion_for_check("Git user identity").is_some());
772        assert!(suggestion_for_check("Auths directory").is_some());
773        assert!(suggestion_for_check("Registry connectivity").is_some());
774    }
775}