devboy_core/agents/
bundles.rs1use anyhow::{Result, anyhow};
9use serde::Deserialize;
10
11const DEV: &str = include_str!("../../bundles/dev.toml");
12const PM: &str = include_str!("../../bundles/pm.toml");
13const ONCALL: &str = include_str!("../../bundles/oncall.toml");
14
15#[derive(Debug, Clone, Deserialize)]
16pub struct Bundle {
17 pub name: String,
18 pub description: String,
19 pub skills: Vec<String>,
20}
21
22pub const PROFILES: &[&str] = &["dev", "pm", "oncall"];
24
25pub fn load(profile: &str) -> Result<Bundle> {
27 let raw = match profile {
28 "dev" => DEV,
29 "pm" => PM,
30 "oncall" => ONCALL,
31 other => {
32 return Err(anyhow!(
33 "unknown profile: {other} (known: {})",
34 PROFILES.join(", ")
35 ));
36 }
37 };
38 toml::from_str::<Bundle>(raw).map_err(|e| anyhow!("failed to parse {profile}.toml: {e}"))
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn all_profiles_load() {
47 for p in PROFILES {
48 let b = load(p).unwrap_or_else(|e| panic!("{p}: {e}"));
49 assert_eq!(b.name, *p);
50 assert!(!b.skills.is_empty(), "{p} has no skills");
51 }
52 }
53
54 #[test]
55 fn unknown_profile_errors() {
56 assert!(load("ceo").is_err());
57 }
58
59 #[test]
60 fn dev_includes_analyze_usage() {
61 let b = load("dev").unwrap();
62 assert!(b.skills.contains(&"analyze-usage".to_string()));
63 }
64
65 #[test]
66 fn each_profile_has_unique_skill_ids() {
67 for p in PROFILES {
68 let b = load(p).unwrap();
69 let mut deduped = b.skills.clone();
70 deduped.sort();
71 deduped.dedup();
72 assert_eq!(
73 deduped.len(),
74 b.skills.len(),
75 "profile '{p}' has duplicate skills"
76 );
77 }
78 }
79
80 #[test]
81 fn each_profile_starts_with_self_bootstrap() {
82 for p in PROFILES {
83 let b = load(p).unwrap();
84 assert!(
85 b.skills.iter().any(|s| s == "setup"),
86 "profile '{p}' missing setup"
87 );
88 }
89 }
90
91 #[test]
92 fn unknown_profile_error_lists_known_profiles() {
93 let err = load("ceo").unwrap_err().to_string();
94 for p in PROFILES {
95 assert!(
96 err.contains(p),
97 "error message missing profile '{p}': {err}"
98 );
99 }
100 }
101
102 #[test]
103 fn pm_profile_emphasises_meeting_skills() {
104 let b = load("pm").unwrap();
105 assert!(b.skills.iter().any(|s| s.starts_with("meeting-")));
106 }
107
108 #[test]
109 fn oncall_profile_includes_notify() {
110 let b = load("oncall").unwrap();
111 assert!(b.skills.iter().any(|s| s == "notify"));
112 }
113}