use sha2::{Digest, Sha256}; pub fn compute_fingerprint(parts: &[&str]) -> String { let mut hasher = Sha256::new(); for part in parts { hasher.update(part.as_bytes()); hasher.update(b"|"); } hex::encode(hasher.finalize()) } #[cfg(test)] mod tests { use super::*; #[test] fn fingerprint_is_deterministic() { let a = compute_fingerprint(&["repo1", "rule-x", "src/main.rs", "42"]); let b = compute_fingerprint(&["repo1", "rule-x", "src/main.rs", "42"]); assert_eq!(a, b); } #[test] fn fingerprint_changes_with_different_input() { let a = compute_fingerprint(&["repo1", "rule-x", "src/main.rs", "42"]); let b = compute_fingerprint(&["repo1", "rule-x", "src/main.rs", "43"]); assert_ne!(a, b); } #[test] fn fingerprint_is_valid_hex_sha256() { let fp = compute_fingerprint(&["hello"]); assert_eq!(fp.len(), 64, "SHA-256 hex should be 64 chars"); assert!(fp.chars().all(|c| c.is_ascii_hexdigit())); } #[test] fn fingerprint_empty_parts() { let fp = compute_fingerprint(&[]); // Should still produce a valid hash (of empty input) assert_eq!(fp.len(), 64); } #[test] fn fingerprint_order_matters() { let a = compute_fingerprint(&["a", "b"]); let b = compute_fingerprint(&["b", "a"]); assert_ne!(a, b); } #[test] fn fingerprint_separator_prevents_collision() { // "ab" + "c" vs "a" + "bc" should differ because of the "|" separator let a = compute_fingerprint(&["ab", "c"]); let b = compute_fingerprint(&["a", "bc"]); assert_ne!(a, b); } }