use std::sync::atomic::{AtomicU64, Ordering};
use crate::core::config::Config;
#[must_use]
pub fn tools_config_hash(cfg: &Config) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
cfg.tool_profile.hash(&mut hasher);
cfg.tools_enabled.hash(&mut hasher);
cfg.disabled_tools.hash(&mut hasher);
hasher.finish()
}
#[must_use]
pub fn current_hash() -> u64 {
tools_config_hash(&Config::load())
}
#[must_use]
pub fn has_changed(last_hash: &AtomicU64) -> bool {
let now = current_hash();
let prev = last_hash.load(Ordering::Relaxed);
if now == prev {
false
} else {
last_hash.store(now, Ordering::Relaxed);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_config_produces_same_hash() {
let cfg = Config::default();
assert_eq!(tools_config_hash(&cfg), tools_config_hash(&cfg));
}
#[test]
fn different_profile_produces_different_hash() {
let mut a = Config::default();
let mut b = Config::default();
a.tool_profile = Some("minimal".to_string());
b.tool_profile = Some("standard".to_string());
assert_ne!(tools_config_hash(&a), tools_config_hash(&b));
}
#[test]
fn different_disabled_tools_produces_different_hash() {
let mut a = Config::default();
let mut b = Config::default();
a.disabled_tools = vec![];
b.disabled_tools = vec!["ctx_call".to_string()];
assert_ne!(tools_config_hash(&a), tools_config_hash(&b));
}
#[test]
fn has_changed_returns_false_when_unchanged() {
let hash = AtomicU64::new(current_hash());
assert!(!has_changed(&hash));
}
#[test]
fn has_changed_detects_difference() {
let hash = AtomicU64::new(0);
assert!(has_changed(&hash));
assert!(!has_changed(&hash));
}
#[test]
fn different_enabled_tools_produces_different_hash() {
let mut a = Config::default();
let mut b = Config::default();
a.tools_enabled = vec![];
b.tools_enabled = vec!["ctx_read".to_string(), "ctx_shell".to_string()];
assert_ne!(tools_config_hash(&a), tools_config_hash(&b));
}
#[test]
fn all_three_fields_contribute_to_hash() {
let base = Config::default();
let base_hash = tools_config_hash(&base);
let mut with_profile = base.clone();
with_profile.tool_profile = Some("power".to_string());
let mut with_enabled = base.clone();
with_enabled.tools_enabled = vec!["ctx_read".to_string()];
let mut with_disabled = base.clone();
with_disabled.disabled_tools = vec!["ctx_graph".to_string()];
let hashes = [
tools_config_hash(&with_profile),
tools_config_hash(&with_enabled),
tools_config_hash(&with_disabled),
];
for h in &hashes {
assert_ne!(*h, base_hash, "changing any field must produce a new hash");
}
assert_ne!(hashes[0], hashes[1]);
assert_ne!(hashes[1], hashes[2]);
}
#[test]
fn has_changed_stabilizes_after_update() {
let hash = AtomicU64::new(0);
assert!(has_changed(&hash), "first call with mismatched seed");
let stored = hash.load(Ordering::Relaxed);
assert_ne!(stored, 0, "hash should have been updated");
assert!(!has_changed(&hash), "same config → no change");
assert!(!has_changed(&hash), "still no change on third call");
}
}