Skip to main content

aegis_memory/
skill.rs

1/// Skill requirements checker: validates binaries and env vars before loading a skill.
2
3#[derive(Debug, Clone)]
4/// Whether a skill's requirements are satisfied.
5pub enum SkillAvailability {
6    Ready,
7    Unavailable(String),
8}
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11/// Declares binaries, env vars, and version needed to load a skill.
12pub struct SkillRequirements {
13    pub binaries: Vec<String>,
14    pub env_vars: Vec<String>,
15    pub min_version: Option<String>,
16}
17
18impl SkillRequirements {
19    /// Create empty requirements (everything satisfied).
20    pub fn new() -> Self {
21        Self {
22            binaries: Vec::new(),
23            env_vars: Vec::new(),
24            min_version: None,
25        }
26    }
27
28    /// Verify all required binaries and env vars are present.
29    pub fn check(&self) -> SkillAvailability {
30        for bin in &self.binaries {
31            if std::process::Command::new("which")
32                .arg(bin)
33                .output()
34                .map(|o| !o.status.success())
35                .unwrap_or(true)
36            {
37                return SkillAvailability::Unavailable(format!("missing binary: {}", bin));
38            }
39        }
40        for var in &self.env_vars {
41            if std::env::var(var).is_err() {
42                return SkillAvailability::Unavailable(format!("missing env var: {}", var));
43            }
44        }
45        SkillAvailability::Ready
46    }
47}
48
49impl Default for SkillRequirements {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn skill_requirements_empty_is_ready() {
61        let reqs = SkillRequirements::new();
62        assert!(matches!(reqs.check(), SkillAvailability::Ready));
63    }
64
65    #[test]
66    fn skill_requirements_missing_binary() {
67        let reqs = SkillRequirements {
68            binaries: vec!["nonexistent_binary_xyz_12345".into()],
69            env_vars: vec![],
70            min_version: None,
71        };
72        match reqs.check() {
73            SkillAvailability::Unavailable(msg) => assert!(msg.contains("missing binary")),
74            _ => panic!("expected Unavailable"),
75        }
76    }
77
78    #[test]
79    fn skill_requirements_present_binary() {
80        let reqs = SkillRequirements {
81            binaries: vec!["ls".into()],
82            env_vars: vec![],
83            min_version: None,
84        };
85        assert!(matches!(reqs.check(), SkillAvailability::Ready));
86    }
87
88    #[test]
89    fn skill_requirements_missing_env_var() {
90        let reqs = SkillRequirements {
91            binaries: vec![],
92            env_vars: vec!["TOTALLY_FAKE_ENV_VAR_12345678".into()],
93            min_version: None,
94        };
95        match reqs.check() {
96            SkillAvailability::Unavailable(msg) => assert!(msg.contains("missing env var")),
97            _ => panic!("expected Unavailable"),
98        }
99    }
100
101    #[test]
102    fn skill_requirements_present_env_var() {
103        // HOME should exist everywhere
104        let reqs = SkillRequirements {
105            binaries: vec![],
106            env_vars: vec!["HOME".into()],
107            min_version: None,
108        };
109        assert!(matches!(reqs.check(), SkillAvailability::Ready));
110    }
111
112    #[test]
113    fn skill_requirements_combined() {
114        let reqs = SkillRequirements {
115            binaries: vec!["ls".into()],
116            env_vars: vec!["HOME".into()],
117            min_version: Some("1.0.0".into()),
118        };
119        assert!(matches!(reqs.check(), SkillAvailability::Ready));
120    }
121
122    #[test]
123    fn skill_requirements_default() {
124        let reqs = SkillRequirements::default();
125        assert!(matches!(reqs.check(), SkillAvailability::Ready));
126    }
127
128    #[test]
129    fn skill_availability_debug() {
130        let avail = SkillAvailability::Ready;
131        assert!(format!("{:?}", avail).contains("Ready"));
132
133        let unavail = SkillAvailability::Unavailable("test reason".into());
134        assert!(format!("{:?}", unavail).contains("test reason"));
135    }
136}