spec_driven_docs/commands/
doctor.rs1use serde::Serialize;
8
9use crate::cli::doctor::DoctorArgs;
10use crate::context::AppContext;
11use crate::error::AppError;
12use crate::output;
13use crate::probes::{self, ProbeClass, ProbeResult, ProbeStatus};
14
15#[derive(Debug, Serialize)]
17struct Report {
18 schema: &'static str,
20 probes: Vec<ProbeResult>,
22 next: Vec<String>,
24}
25
26pub fn run(_ctx: &AppContext, args: &DoctorArgs) -> Result<(), AppError> {
33 let probes = probes::run_all();
34 let next = vec!["sdd status --target . reports the instance".to_owned()];
35 if args.json {
36 return output::json(&Report {
37 schema: "sdd.doctor/1",
38 probes,
39 next,
40 });
41 }
42 for class in [ProbeClass::Hard, ProbeClass::Soft] {
43 output::line(match class {
44 ProbeClass::Hard => "hard",
45 ProbeClass::Soft => "soft",
46 });
47 for probe in probes.iter().filter(|probe| probe.class == class) {
48 let status = match probe.status {
49 ProbeStatus::Ok => "ok ",
50 ProbeStatus::Failed => "failed",
51 };
52 output::line(format!(" {status} {}: {}", probe.id, probe.message));
53 if let Some(remediation) = &probe.remediation {
54 output::line(format!(" next: {remediation}"));
55 }
56 }
57 }
58 output::line("Next:");
59 for line in &next {
60 output::line(format!(" {line}"));
61 }
62 Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67 #![allow(
68 clippy::expect_used,
69 reason = "a test panics as its failure signal, not as control flow"
70 )]
71
72 use crate::probes::{ProbeClass, ProbeResult, ProbeStatus};
73
74 #[test]
76 fn the_doctor_report_schema_snapshot_holds() {
77 let report = super::Report {
78 schema: "sdd.doctor/1",
79 probes: vec![ProbeResult {
80 id: "state-root",
81 class: ProbeClass::Hard,
82 status: ProbeStatus::Ok,
83 message: "the state root is writable".into(),
84 remediation: None,
85 }],
86 next: vec!["sdd status --target . reports the instance".into()],
87 };
88 assert_eq!(
89 serde_json::to_string(&report).expect("a report serializes"),
90 r#"{"schema":"sdd.doctor/1","probes":[{"id":"state-root","class":"hard","status":"ok","message":"the state root is writable"}],"next":["sdd status --target . reports the instance"]}"#
91 );
92 }
93
94 #[test]
96 fn the_probe_schema_snapshot_holds() {
97 let ok = ProbeResult {
98 id: "git",
99 class: ProbeClass::Soft,
100 status: ProbeStatus::Ok,
101 message: "git runs".into(),
102 remediation: None,
103 };
104 assert_eq!(
105 serde_json::to_string(&ok).expect("a probe serializes"),
106 r#"{"id":"git","class":"soft","status":"ok","message":"git runs"}"#
107 );
108 let failed = ProbeResult {
109 id: "pre-commit",
110 class: ProbeClass::Soft,
111 status: ProbeStatus::Failed,
112 message: "pre-commit is not on PATH".into(),
113 remediation: Some("install pre-commit; the delivered gates run through it".into()),
114 };
115 assert_eq!(
116 serde_json::to_string(&failed).expect("a probe serializes"),
117 r#"{"id":"pre-commit","class":"soft","status":"failed","message":"pre-commit is not on PATH","remediation":"install pre-commit; the delivered gates run through it"}"#
118 );
119 }
120}