feat: add enabled_macros config field at global, role, agent, and session levels

Mirrors the enabled_skills plumbing per plans/custom-commands-design.md §5:
- global: Config + AppConfig structs, from_config copy, and the
  COYOTE_ENABLED_MACROS env-override arm (csv_to_vec parsing)
- role: frontmatter via parse_string_or_array (list or csv string),
  plus the export() mirror so Role::save round-trips the field
- agent (non-graph): plain serde on AgentConfig; graph.yaml silently
  ignores the key (pinned by test, no field on Graph by design)
- session: plain serde with csv-or-vec deserializer

Empty list/string deserializes to Some([]) (explicit zero), distinct
from absent/null (None) — pinned by tests at every level, including
the env arm (serial-fenced against the from_config tests, which read
the process env via load_envs).
This commit is contained in:
2026-08-21 11:55:13 -06:00
parent e8b55bba15
commit f39381aa9d
6 changed files with 250 additions and 0 deletions
+45
View File
@@ -46,6 +46,12 @@ pub struct Session {
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_skills: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_macros: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
save_session: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -931,6 +937,45 @@ mod tests {
assert!(!session.dirty());
}
#[test]
fn session_enabled_macros_absent_is_none() {
let session: Session = serde_yaml::from_str("model: provider:test\nmessages: []").unwrap();
assert_eq!(session.enabled_macros, None);
}
#[test]
fn session_enabled_macros_empty_list_is_some_empty() {
let session: Session =
serde_yaml::from_str("model: provider:test\nenabled_macros: []\nmessages: []").unwrap();
assert_eq!(session.enabled_macros, Some(vec![]));
}
#[test]
fn session_enabled_macros_empty_string_is_some_empty() {
let session: Session =
serde_yaml::from_str("model: provider:test\nenabled_macros: \"\"\nmessages: []")
.unwrap();
assert_eq!(session.enabled_macros, Some(vec![]));
}
#[test]
fn session_enabled_macros_csv_string() {
let session: Session =
serde_yaml::from_str("model: provider:test\nenabled_macros: \"a,b\"\nmessages: []")
.unwrap();
assert_eq!(
session.enabled_macros,
Some(vec!["a".to_string(), "b".to_string()])
);
}
#[test]
fn session_serialize_omits_enabled_macros_when_none() {
let session = Session::default();
let yaml = serde_yaml::to_string(&session).unwrap();
assert!(!yaml.contains("enabled_macros"));
}
#[test]
fn session_new_from_ctx_captures_save_session() {
let app_config = Arc::new(AppConfig::default());