Skip to main content

agentshield/
doctor.rs

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}