use crate::babysit;
use crate::paths::Paths;
use crate::util;
use std::fs;
use std::path::{Path, PathBuf};
fn rel(paths: &Paths, p: &Path) -> String {
p.strip_prefix(&paths.data_dir)
.unwrap_or(p)
.to_string_lossy()
.replace('\\', "/")
}
fn wake_signal(v: serde_json::Value) -> serde_json::Value {
match &v {
serde_json::Value::Object(m) if m.contains_key("signal") => {
m.get("signal").cloned().unwrap_or(serde_json::Value::Null)
}
_ => v,
}
}
fn sorted_glob(dir: &Path, ext: &str) -> Vec<PathBuf> {
let mut v: Vec<PathBuf> = fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().map(|e| e == ext).unwrap_or(false))
.collect();
v.sort();
v
}
pub fn world_hash(paths: &Paths) -> String {
let mut buf: Vec<u8> = Vec::new();
let mut files = vec![paths.playbook()];
files.extend(sorted_glob(&paths.goals_dir(), "md"));
for f in files {
if !f.is_file() {
continue;
}
buf.extend_from_slice(format!("@@ {}\n", rel(paths, &f)).as_bytes());
if let Ok(bytes) = fs::read(&f) {
buf.extend_from_slice(&bytes);
}
}
for f in sorted_glob(&paths.snapshots_dir(), "json") {
buf.extend_from_slice(format!("@@ {}\n", rel(paths, &f)).as_bytes());
let raw = fs::read(&f).unwrap_or_default();
match serde_json::from_slice::<serde_json::Value>(&raw) {
Ok(v) => {
buf.extend_from_slice(wake_signal(v).to_string().as_bytes());
buf.push(b'\n');
}
Err(_) => buf.extend_from_slice(&raw), }
}
for s in babysit::list_looop() {
let exit = s
.exit_code
.map(|c| c.to_string())
.unwrap_or_else(|| "null".into());
let note = s.note.clone().unwrap_or_else(|| "null".into());
buf.extend_from_slice(format!("{} {} {} {}\n", s.id, s.state, exit, note).as_bytes());
}
util::content_hash(&buf)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn wake_signal_keeps_only_signal_when_present() {
let v = json!({ "signal": { "open": 3 }, "detail": { "checked_at": "now" } });
assert_eq!(wake_signal(v), json!({ "open": 3 }));
}
#[test]
fn wake_signal_passes_through_objects_without_signal() {
let v = json!({ "open": 3, "closed": 1 });
assert_eq!(wake_signal(v.clone()), v);
}
#[test]
fn wake_signal_passes_through_non_objects() {
assert_eq!(wake_signal(json!(42)), json!(42));
assert_eq!(wake_signal(json!([1, 2])), json!([1, 2]));
}
#[test]
fn wake_signal_ignores_volatile_detail_changes() {
let a = json!({ "signal": { "open": 3 }, "detail": { "ts": 1 } });
let b = json!({ "signal": { "open": 3 }, "detail": { "ts": 999 } });
assert_eq!(wake_signal(a), wake_signal(b));
}
#[test]
fn world_hash_is_stable_and_change_sensitive() {
let p = Paths::temp();
fs::create_dir_all(p.goals_dir()).unwrap();
fs::write(p.playbook(), b"rule one\n").unwrap();
fs::write(p.goals_dir().join("a.md"), b"goal a\n").unwrap();
let h1 = world_hash(&p);
let h2 = world_hash(&p);
assert_eq!(h1, h2, "same content must hash the same");
fs::write(p.goals_dir().join("a.md"), b"goal a changed\n").unwrap();
let h3 = world_hash(&p);
assert_ne!(h1, h3, "a goal edit must change the world hash");
}
}