use std::path::Path;
use serde_json::Value;
use crate::error::AgentConfigError;
use crate::status::ConfigPresence;
use crate::util::mcp_json_map;
use super::common::{matches_tag, read_or_empty, traverse_object};
pub(crate) fn tagged_hook_presence(
config_path: &Path,
parent_path: &[&str],
tag: &str,
) -> Result<ConfigPresence, AgentConfigError> {
if !config_path.exists() {
return Ok(ConfigPresence::Absent);
}
let root = match read_or_empty(config_path) {
Ok(v) => v,
Err(AgentConfigError::JsonInvalid { source, .. }) => {
return Ok(ConfigPresence::Invalid {
reason: source.to_string(),
});
}
Err(e) => return Err(e),
};
let count = count_tagged_under(&root, parent_path, tag);
Ok(presence_from_count(count))
}
pub(crate) fn tagged_hook_presence_jsonc(
config_path: &Path,
parent_path: &[&str],
tag: &str,
) -> Result<ConfigPresence, AgentConfigError> {
if !config_path.exists() {
return Ok(ConfigPresence::Absent);
}
let root = match mcp_json_map::read_jsonc_or_empty(config_path) {
Ok(v) => v,
Err(AgentConfigError::JsonInvalid { source, .. }) => {
return Ok(ConfigPresence::Invalid {
reason: source.to_string(),
});
}
Err(AgentConfigError::Other(e)) => {
return Ok(ConfigPresence::Invalid {
reason: e.to_string(),
});
}
Err(e) => return Err(e),
};
let count = count_tagged_under(&root, parent_path, tag);
Ok(presence_from_count(count))
}
#[allow(dead_code)]
pub(crate) fn tagged_hook_presence_for_event(
config_path: &Path,
array_path: &[&str],
tag: &str,
) -> Result<ConfigPresence, AgentConfigError> {
if !config_path.exists() {
return Ok(ConfigPresence::Absent);
}
let root = match read_or_empty(config_path) {
Ok(v) => v,
Err(AgentConfigError::JsonInvalid { source, .. }) => {
return Ok(ConfigPresence::Invalid {
reason: source.to_string(),
});
}
Err(e) => return Err(e),
};
let count = traverse_array_at(&root, array_path)
.map(|arr| arr.iter().filter(|v| matches_tag(v, tag)).count())
.unwrap_or(0);
Ok(presence_from_count(count))
}
fn count_tagged_under(root: &Value, parent_path: &[&str], tag: &str) -> usize {
let Some(parent) = traverse_object(root, parent_path) else {
return 0;
};
parent
.values()
.map(|v| {
v.as_array()
.map(|arr| arr.iter().filter(|entry| matches_tag(entry, tag)).count())
.unwrap_or(0)
})
.sum()
}
#[allow(dead_code)]
fn traverse_array_at<'a>(root: &'a Value, path: &[&str]) -> Option<&'a Vec<Value>> {
let mut cur = root;
for key in path {
cur = cur.get(*key)?;
}
cur.as_array()
}
fn presence_from_count(count: usize) -> ConfigPresence {
match count {
0 => ConfigPresence::Absent,
1 => ConfigPresence::Single,
n => ConfigPresence::Duplicate { count: n },
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::tempdir;
fn write(p: &Path, v: &Value) {
std::fs::write(p, serde_json::to_string_pretty(v).unwrap()).unwrap();
}
#[test]
fn returns_absent_when_file_missing() {
let dir = tempdir().unwrap();
let r =
tagged_hook_presence(&dir.path().join("missing.json"), &["hooks"], "alpha").unwrap();
assert!(matches!(r, ConfigPresence::Absent));
}
#[test]
fn returns_single_when_one_match() {
let dir = tempdir().unwrap();
let p = dir.path().join("settings.json");
write(
&p,
&json!({
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "_agent_config_tag": "alpha" }
]
}
}),
);
let r = tagged_hook_presence(&p, &["hooks"], "alpha").unwrap();
assert!(matches!(r, ConfigPresence::Single));
}
#[test]
fn returns_duplicate_when_multiple_matches() {
let dir = tempdir().unwrap();
let p = dir.path().join("settings.json");
write(
&p,
&json!({
"hooks": {
"PreToolUse": [
{ "_agent_config_tag": "alpha" },
{ "_agent_config_tag": "alpha" }
]
}
}),
);
let r = tagged_hook_presence(&p, &["hooks"], "alpha").unwrap();
assert!(matches!(r, ConfigPresence::Duplicate { count: 2 }));
}
#[test]
fn returns_invalid_when_parse_fails() {
let dir = tempdir().unwrap();
let p = dir.path().join("settings.json");
std::fs::write(&p, b"{not json").unwrap();
let r = tagged_hook_presence(&p, &["hooks"], "alpha").unwrap();
assert!(matches!(r, ConfigPresence::Invalid { .. }));
}
#[test]
fn returns_absent_when_tag_not_found() {
let dir = tempdir().unwrap();
let p = dir.path().join("settings.json");
write(
&p,
&json!({
"hooks": { "PreToolUse": [{ "_agent_config_tag": "other" }] }
}),
);
let r = tagged_hook_presence(&p, &["hooks"], "alpha").unwrap();
assert!(matches!(r, ConfigPresence::Absent));
}
#[test]
fn for_event_finds_tag_under_specific_array() {
let dir = tempdir().unwrap();
let p = dir.path().join("settings.json");
write(
&p,
&json!({
"pre_run_command": [{ "_agent_config_tag": "alpha" }]
}),
);
let r = tagged_hook_presence_for_event(&p, &["pre_run_command"], "alpha").unwrap();
assert!(matches!(r, ConfigPresence::Single));
}
}