1use crate::prompt::types::{PromptDiff, PromptSnapshot};
5use std::collections::HashMap;
6
7pub fn diff(a: &PromptSnapshot, b: &PromptSnapshot) -> PromptDiff {
9 let files_a = a.files();
10 let map_a: HashMap<&str, &str> = files_a
11 .iter()
12 .map(|f| (f.path.as_str(), f.sha256.as_str()))
13 .collect();
14 let map_b: HashMap<String, String> =
15 b.files().into_iter().map(|f| (f.path, f.sha256)).collect();
16 let mut added = Vec::new();
17 let mut removed = Vec::new();
18 let mut changed = Vec::new();
19 for (path, hash_b) in &map_b {
20 match map_a.get(path.as_str()) {
21 None => added.push(path.clone()),
22 Some(hash_a) if *hash_a != hash_b.as_str() => changed.push(path.clone()),
23 _ => {}
24 }
25 }
26 for path in map_a.keys() {
27 if !map_b.contains_key(*path) {
28 removed.push((*path).to_string());
29 }
30 }
31 added.sort();
32 removed.sort();
33 changed.sort();
34 PromptDiff {
35 added,
36 removed,
37 changed,
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::prompt::types::PromptFile;
45
46 fn snap(files: &[(&str, &str)]) -> PromptSnapshot {
47 let pfs: Vec<PromptFile> = files
48 .iter()
49 .map(|(p, h)| PromptFile {
50 path: p.to_string(),
51 sha256: h.to_string(),
52 bytes: 0,
53 })
54 .collect();
55 PromptSnapshot {
56 fingerprint: "x".into(),
57 captured_at_ms: 0,
58 files_json: serde_json::to_string(&pfs).unwrap(),
59 total_bytes: 0,
60 }
61 }
62
63 #[test]
64 fn identical_snapshots_empty_diff() {
65 let a = snap(&[("a.md", "h1")]);
66 let d = diff(&a, &a);
67 assert!(d.is_empty());
68 }
69
70 #[test]
71 fn detects_added_removed_changed() {
72 let a = snap(&[("a.md", "h1"), ("b.md", "h2")]);
73 let b = snap(&[("a.md", "h9"), ("c.md", "h3")]);
74 let d = diff(&a, &b);
75 assert_eq!(d.changed, vec!["a.md"]);
76 assert_eq!(d.added, vec!["c.md"]);
77 assert_eq!(d.removed, vec!["b.md"]);
78 }
79}