use std::path::{Path, PathBuf};
use super::support::*;
use crate::validate::Finding;
use prov_graph::exec::block_on;
use prov_history::*;
#[test]
fn a_capture_leaves_check_clean() {
let dir = seed("capture-check");
capture(&dir, "2026-07-31T09:15:22Z", Some("pre-sync"));
assert_eq!(
check(&dir),
vec![],
"a capture must leave the workspace valid"
);
write(
&dir,
"notes/a.md",
"---\ntitle: A\npart_of: '../index.md'\n---\nedited\n",
);
capture(&dir, "2026-08-01T09:00:00Z", None);
assert!(dir.join("history/events/2026/08/index.md").exists());
assert_eq!(check(&dir), vec![]);
}
#[test]
fn a_restore_leaves_check_clean() {
let dir = seed("restore-check");
let Captured::Written { id, .. } = capture(&dir, "2026-07-31T09:15:22Z", None) else {
panic!("the first capture must write an event");
};
write(&dir, "notes/a.md", "clobbered by a sync conflict");
relink_live(&dir, &["notes/photo.jpg.yaml"]);
let mut w = ws(&dir);
let root = Path::new("index.md");
let event = block_on(w.history_event(root, &id)).unwrap().unwrap();
let plan = block_on(w.history_restore_plan(root, &event, &Scope::Whole, false)).unwrap();
block_on(w.history_restore(root, &plan, false)).unwrap();
assert!(read(&dir, "index.md").contains("notes/a.md"));
assert_eq!(check(&dir), vec![]);
}
#[test]
fn a_prune_leaves_check_clean() {
let dir = seed("prune-check");
capture_edited(&dir, "2026-07-31T09:00:00.000000Z", "july", "alpha");
capture_edited(&dir, "2026-08-01T09:00:00.000000Z", "august", "beta");
let mut w = ws(&dir);
let root = Path::new("index.md");
let plan =
block_on(w.history_prune_plan(root, &Retention::Before("2026-08-01".into()))).unwrap();
block_on(w.history_prune(root, &plan)).unwrap();
assert!(!dir.join("history/events/2026/07/index.md").exists());
assert_eq!(check(&dir), vec![]);
}
#[test]
fn a_forget_leaves_check_clean() {
let dir = seed("forget-check");
capture_edited(&dir, "2026-07-31T09:00:00.000000Z", "one", "alpha");
std::fs::remove_file(dir.join("notes/a.md")).unwrap();
relink_live(&dir, &["notes/photo.jpg.yaml"]);
let mut w = ws(&dir);
block_on(w.history_forget(
Path::new("index.md"),
&Subject::Path(PathBuf::from("notes/a.md")),
"2026-08-01T12:00:00.000000Z",
false,
))
.unwrap();
assert!(read(&dir, "history/index.md").contains("forgotten.yaml"));
assert_eq!(
check(&dir),
vec![],
"a recorded destruction is not a finding — a `check` that never came \
back to clean would teach the user to stop reading it"
);
}
#[test]
fn lost_bytes_are_reported_with_both_causes_a_reader_has_to_weigh() {
let dir = seed("blob-missing");
capture(&dir, "2026-07-31T09:00:00.000000Z", None);
let payload = crate::fixity::digest(b"JPEGBYTES");
std::fs::remove_file(dir.join(blob_of(b"JPEGBYTES"))).unwrap();
let findings = check(&dir);
assert_eq!(
findings,
vec![Finding::HistoryBlobMissing {
store: PathBuf::from("history/index.md"),
hash: payload,
paths: vec![PathBuf::from("notes/photo.jpg")],
}]
);
let text = findings[0].to_string();
assert!(
text.contains("has not arrived yet") && text.contains("gone"),
"{text}"
);
assert!(text.contains("notes/photo.jpg"), "{text}");
}
#[test]
fn orphaned_bytes_name_the_verb_that_collects_them() {
let dir = seed("blob-orphan");
capture(&dir, "2026-07-31T09:00:00.000000Z", None);
write(&dir, "history/blobs/ab/sync-conflict-20260731", "junk");
let findings = check(&dir);
assert_eq!(
findings,
vec![Finding::HistoryBlobOrphaned {
store: PathBuf::from("history/index.md"),
blobs: vec![PathBuf::from("history/blobs/ab/sync-conflict-20260731")],
}]
);
assert!(
findings[0].to_string().contains("history-prune"),
"the report names the verb that collects them: {}",
findings[0]
);
}
#[test]
fn a_torn_event_is_reported_as_a_plain_unreadable_document() {
let dir = seed("check-torn");
let first = capture_edited(&dir, "2026-07-31T09:00:00.000000Z", "one", "alpha");
let torn = event_path(Path::new("history/index.md"), &first, "md").unwrap();
tear(&dir, torn.to_str().unwrap());
let findings = check(&dir);
assert!(
findings
.iter()
.any(|f| matches!(f, Finding::Unreadable { doc, .. } if doc == &torn)),
"missing the promised finding for {}: {findings:?}",
torn.display()
);
}
#[test]
fn an_unlinked_store_is_reported_first_and_its_fix_retires_it() {
let dir = seed("check-unlinked");
capture(&dir, "2026-07-31T09:00:00.000000Z", None);
unlink_the_store(&dir);
let findings = check(&dir);
let unlinked = Finding::HistoryStoreUnlinked {
root: PathBuf::from("index.md"),
store: PathBuf::from("history/index.md"),
};
assert!(
findings.contains(&unlinked),
"a store nothing declares must not be silent: {findings:?}"
);
assert_eq!(
findings.iter().position(|f| f == &unlinked),
Some(0),
"{findings:?}"
);
let text = unlinked.to_string();
assert!(
text.contains("history/index.md") && text.contains("index.md"),
"{text}"
);
let fix = block_on(ws(&dir).suggest_fix(&unlinked)).unwrap().unwrap();
assert_eq!(
fix,
crate::Fix::LinkHistoryStore {
root: PathBuf::from("index.md"),
store: PathBuf::from("history/index.md"),
}
);
block_on(ws(&dir).apply_fix(&fix)).unwrap();
assert!(read(&dir, "index.md").contains("history: /history/index.md"));
assert_eq!(
check(&dir),
vec![],
"the fix has to actually retire the finding"
);
}
#[test]
fn an_undeclared_store_is_not_a_finding_when_history_is_off() {
let dir = seed("check-unlinked-off");
capture(&dir, "2026-07-31T09:00:00.000000Z", None);
unlink_the_store(&dir);
assert!(
!block_on(ws_history_off(&dir).check(Path::new("index.md")))
.unwrap()
.iter()
.any(|f| matches!(f, Finding::HistoryStoreUnlinked { .. }))
);
}
#[test]
fn a_stale_shard_index_is_rebuilt_by_its_fix() {
let dir = seed("check-index-stale");
capture_edited(&dir, "2026-07-31T09:00:00.000000Z", "one", "alpha");
let before = read(&dir, "history/events/2026/07/index.md");
capture_edited(&dir, "2026-07-31T10:00:00.000000Z", "two", "beta");
write(&dir, "history/events/2026/07/index.md", &before);
let findings = check(&dir);
let stale: Vec<_> = findings
.iter()
.filter(|f| matches!(f, Finding::HistoryIndexStale { .. }))
.collect();
assert_eq!(stale.len(), 1, "expected one stale shard: {findings:?}");
let mut w = ws(&dir);
let fix = block_on(w.suggest_fix(stale[0])).unwrap().expect("a fix");
block_on(w.apply_fix(&fix)).unwrap();
assert_eq!(
check(&dir),
vec![],
"the rebuild should have settled the index"
);
}
fn unlink_the_store(dir: &Path) {
let root = read(dir, "index.md");
write(
dir,
"index.md",
&root
.lines()
.filter(|l| !l.starts_with("history:"))
.map(|l| format!("{l}\n"))
.collect::<String>(),
);
}