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 include_patterns: Vec<String>,
25 pub exclude_patterns: Vec<String>,
26 pub enabled_features: EnabledFeatures,
27 pub detected_adapters: Vec<String>,
28 pub available_adapters: Vec<String>,
29 pub runtime_wrap_available: bool,
30}
31
32pub fn run_doctor(
33 target: &Path,
34 config_path: Option<PathBuf>,
35 ignore_tests_override: bool,
36) -> Result<DoctorReport> {
37 let effective_config_path = config_path.unwrap_or_else(|| target.join(".agentshield.toml"));
38 let config_found = effective_config_path.exists();
39 let config = Config::load(&effective_config_path)?;
40 let ignore_tests = ignore_tests_override || config.scan.ignore_tests;
41
42 let adapters = all_adapters();
43 let available_adapters = adapters
44 .iter()
45 .map(|adapter| adapter.framework().to_string())
46 .collect();
47 let detected_adapters = adapters
48 .iter()
49 .filter(|adapter| adapter.detect(target))
50 .map(|adapter| adapter.framework().to_string())
51 .collect();
52
53 Ok(DoctorReport {
54 version: env!("CARGO_PKG_VERSION").to_string(),
55 target: target.to_path_buf(),
56 config_path: effective_config_path,
57 config_found,
58 fail_on: config.policy.fail_on.to_string(),
59 ignore_tests,
60 include_patterns: config.scan.include.clone(),
61 exclude_patterns: config.scan.exclude.clone(),
62 enabled_features: EnabledFeatures {
63 python: cfg!(feature = "python"),
64 typescript: cfg!(feature = "typescript"),
65 runtime: cfg!(feature = "runtime"),
66 },
67 detected_adapters,
68 available_adapters,
69 runtime_wrap_available: cfg!(feature = "runtime"),
70 })
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn reports_missing_config_and_enabled_features() {
79 let tmp = tempfile::TempDir::new().unwrap();
80
81 let report = run_doctor(tmp.path(), None, false).unwrap();
82
83 assert_eq!(report.version, env!("CARGO_PKG_VERSION"));
84 assert_eq!(report.target, tmp.path());
85 assert_eq!(report.config_path, tmp.path().join(".agentshield.toml"));
86 assert!(!report.config_found);
87 assert_eq!(report.fail_on, "high");
88 assert!(!report.ignore_tests);
89 assert!(report.include_patterns.is_empty());
90 assert!(report.exclude_patterns.is_empty());
91 assert_eq!(report.enabled_features.python, cfg!(feature = "python"));
92 assert_eq!(
93 report.enabled_features.typescript,
94 cfg!(feature = "typescript")
95 );
96 assert_eq!(report.enabled_features.runtime, cfg!(feature = "runtime"));
97 assert_eq!(report.runtime_wrap_available, cfg!(feature = "runtime"));
98 assert!(!report.available_adapters.is_empty());
99 assert!(report.detected_adapters.is_empty());
100 }
101
102 #[test]
103 fn ignore_tests_cli_override_enables_effective_setting() {
104 let tmp = tempfile::TempDir::new().unwrap();
105
106 let report = run_doctor(tmp.path(), None, true).unwrap();
107
108 assert!(report.ignore_tests);
109 }
110}