1use std::path::{Path, PathBuf};
2
3use serde::Serialize;
4
5use crate::adapter::all_adapters;
6use crate::config::Config;
7use crate::error::Result;
8
9#[derive(Debug, Clone, Serialize)]
10pub struct EnabledFeatures {
11 pub python: bool,
12 pub typescript: bool,
13 pub runtime: bool,
14}
15
16#[derive(Debug, Clone, Serialize)]
17pub struct DoctorReport {
18 pub version: String,
19 pub target: PathBuf,
20 pub config_path: PathBuf,
21 pub config_found: bool,
22 pub fail_on: String,
23 pub ignore_tests: bool,
24 pub enabled_features: EnabledFeatures,
25 pub detected_adapters: Vec<String>,
26 pub available_adapters: Vec<String>,
27 pub runtime_wrap_available: bool,
28}
29
30pub fn run_doctor(
31 target: &Path,
32 config_path: Option<PathBuf>,
33 ignore_tests_override: bool,
34) -> Result<DoctorReport> {
35 let effective_config_path = config_path.unwrap_or_else(|| target.join(".agentshield.toml"));
36 let config_found = effective_config_path.exists();
37 let config = Config::load(&effective_config_path)?;
38 let ignore_tests = ignore_tests_override || config.scan.ignore_tests;
39
40 let adapters = all_adapters();
41 let available_adapters = adapters
42 .iter()
43 .map(|adapter| adapter.framework().to_string())
44 .collect();
45 let detected_adapters = adapters
46 .iter()
47 .filter(|adapter| adapter.detect(target))
48 .map(|adapter| adapter.framework().to_string())
49 .collect();
50
51 Ok(DoctorReport {
52 version: env!("CARGO_PKG_VERSION").to_string(),
53 target: target.to_path_buf(),
54 config_path: effective_config_path,
55 config_found,
56 fail_on: config.policy.fail_on.to_string(),
57 ignore_tests,
58 enabled_features: EnabledFeatures {
59 python: cfg!(feature = "python"),
60 typescript: cfg!(feature = "typescript"),
61 runtime: cfg!(feature = "runtime"),
62 },
63 detected_adapters,
64 available_adapters,
65 runtime_wrap_available: cfg!(feature = "runtime"),
66 })
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn reports_missing_config_and_enabled_features() {
75 let tmp = tempfile::TempDir::new().unwrap();
76
77 let report = run_doctor(tmp.path(), None, false).unwrap();
78
79 assert_eq!(report.version, env!("CARGO_PKG_VERSION"));
80 assert_eq!(report.target, tmp.path());
81 assert_eq!(report.config_path, tmp.path().join(".agentshield.toml"));
82 assert!(!report.config_found);
83 assert_eq!(report.fail_on, "high");
84 assert!(!report.ignore_tests);
85 assert_eq!(report.enabled_features.python, cfg!(feature = "python"));
86 assert_eq!(
87 report.enabled_features.typescript,
88 cfg!(feature = "typescript")
89 );
90 assert_eq!(report.enabled_features.runtime, cfg!(feature = "runtime"));
91 assert_eq!(report.runtime_wrap_available, cfg!(feature = "runtime"));
92 assert!(!report.available_adapters.is_empty());
93 assert!(report.detected_adapters.is_empty());
94 }
95
96 #[test]
97 fn ignore_tests_cli_override_enables_effective_setting() {
98 let tmp = tempfile::TempDir::new().unwrap();
99
100 let report = run_doctor(tmp.path(), None, true).unwrap();
101
102 assert!(report.ignore_tests);
103 }
104}