feat: end-to-end integrity checks with self-healing; website; installer and Windows fixes
Store::verify compares the whole SQLite projection, every table row for row, against a fresh projection of the journal; verify_and_heal rebuilds a drifted database (keeping the old file) at every server start and via the new books.verify tool, which also re-hashes every blob. The GUI checks the server's database read-only and every stored file, and shows the result. rebuild() also drops stale WAL/SHM files. site/: the wanderbooks website (just site, just publish). install.sh: OS detection (Windows points to WSL2), C compiler and git checks, WANDERBOOKS_REPO. Windows: no directory fsync off Unix; .exe-aware lookup. Repo justfile: check, build, install, demo, site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
25 files changed
+808
-41
No files matched your search
@@ -683,3 +683,47 @@ fn vendor_1099_tags_replay_and_project() {
|
||||
assert_eq!(form, "nec");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_whole_projection_is_checked_and_healed() {
|
||||
let dir = test_dir("heal");
|
||||
let mut store = build_books(&dir);
|
||||
let e = store
|
||||
.ledger()
|
||||
.posted_entries()
|
||||
.next()
|
||||
.map(|e| e.id())
|
||||
.unwrap();
|
||||
store
|
||||
.attach_receipt(Some(e), "r.png", "image/png", b"receipt", None)
|
||||
.unwrap();
|
||||
store.verify().unwrap();
|
||||
drop(store);
|
||||
// Drift no accounting report would notice: a receipt quietly unlinked.
|
||||
let db = dir.join(ledger_store::DB_FILE);
|
||||
let conn = rusqlite::Connection::open(&db).unwrap();
|
||||
conn.execute("UPDATE attachments SET entry_id = NULL", [])
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
let mut store = Store::open(&dir).unwrap();
|
||||
match store.verify() {
|
||||
Err(StoreError::VerifyMismatch(why)) => assert!(why.contains("attachments"), "{why}"),
|
||||
other => panic!("drift not caught: {other:?}"),
|
||||
}
|
||||
let report = store.verify_and_heal().unwrap();
|
||||
assert!(report.healed.is_some(), "healed");
|
||||
assert!(report.events > 0);
|
||||
store.verify().unwrap();
|
||||
assert_eq!(
|
||||
store.list_attachments(Some(e)).unwrap().len(),
|
||||
1,
|
||||
"link restored"
|
||||
);
|
||||
let kept = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.filter_map(|d| d.ok())
|
||||
.any(|d| d.file_name().to_string_lossy().contains(".mismatch."));
|
||||
assert!(kept, "the divergent database is kept for forensics");
|
||||
// A healthy store reports nothing to heal.
|
||||
assert_eq!(store.verify_and_heal().unwrap().healed, None);
|
||||
}
|
||||
Reference in new issue
Block a user