//! `bxctl audit verify` against the fixture logs in `crates/proto/tests/fixtures/audit/`. //! Do not edit. The output is compared byte for byte: the owner reads it, and so do scripts. use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; static NEXT: AtomicU32 = AtomicU32::new(0); /// A home directory whose `audit/` is a copy of the fixture log `case`. Removed when dropped. struct Home { path: PathBuf, } impl Home { fn with_case(case: &str) -> Home { let n = NEXT.fetch_add(1, Ordering::SeqCst); let name = format!("bxctl-verify-{}-{n}", std::process::id()); let path = std::env::temp_dir().join(name); let _ = std::fs::remove_dir_all(&path); let audit = path.join("audit"); std::fs::create_dir_all(&audit).unwrap(); let from = format!( "{}/../proto/tests/fixtures/audit/{case}", env!("CARGO_MANIFEST_DIR") ); for entry in std::fs::read_dir(&from).unwrap_or_else(|e| panic!("{from}: {e}")) { let entry = entry.unwrap(); std::fs::copy(entry.path(), audit.join(entry.file_name())).unwrap(); } Home { path } } } impl Drop for Home { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.path); } } fn run(case: &str) -> (bool, String) { let home = Home::with_case(case); // What brokerd leaves beside the log must not be read as part of it. std::fs::write(home.path.join("audit/.lock"), "").unwrap(); let mut out = Vec::new(); let ok = bxctl::verify::run(&home.path, &mut out).unwrap(); (ok, String::from_utf8(out).unwrap()) } /// The hex of the hash of the last line of `file` in `case`. fn head_of(case: &str, file: &str) -> String { let path = format!( "{}/../proto/tests/fixtures/audit/{case}/{file}", env!("CARGO_MANIFEST_DIR") ); let text = std::fs::read_to_string(path).unwrap(); proto::sha256(text.lines().last().unwrap().as_bytes()) .unwrap() .to_hex() } #[test] fn a_good_log() { let (ok, out) = run("good"); assert!(ok); let head = head_of("good", "2026-09-18.jsonl"); assert_eq!( out, format!( "audit: ok, 10 records, head {head}\n\ pending or abandoned: approval 6\n\ running or unfinished: decision 7\n" ) ); } #[test] fn everything_worth_knowing_is_listed_one_per_line() { let (ok, out) = run("recovered-next-day"); assert!(ok); let lines: Vec<&str> = out.lines().collect(); assert!( lines[0].starts_with("audit: ok, 11 records, head "), "{out}" ); assert_eq!( lines[1..], [ "recovered line: 2026-09-17.jsonl:6", "pending or abandoned: approval 7", "running or unfinished: decision 8", ] ); let (ok, out) = run("accepted-break-older-file"); assert!(ok); assert!( out.contains("\naccepted break: 2026-09-18.jsonl:6\n"), "{out}" ); let (ok, out) = run("clock-back"); assert!(ok); assert!( out.ends_with("\nclock went backwards: 2026-09-18.jsonl:6\n"), "{out}" ); } /// A torn final line is what a crash, or a `brokerd` in the middle of a write, leaves. It is /// reported and is not a failure. #[test] fn a_torn_tail_is_reported_and_is_ok() { let (ok, out) = run("torn-tail"); assert!(ok); assert!(out.starts_with("audit: ok, 10 records, head "), "{out}"); assert!( out.ends_with( "\ntorn final line: 2026-09-18.jsonl:6 (brokerd recovers it at its next start)\n" ), "{out}" ); } #[test] fn a_broken_chain_is_two_lines_and_false() { let cases = [ ( "changed-byte", "2026-09-17.jsonl:4: prev is not the hash of the line before", ), ("deleted-line", "2026-09-17.jsonl:3: seq is 3, expected 2"), ( "cut-short", "2026-09-17.jsonl:3: does not parse as an audit record", ), ( "file-not-chained", "2026-09-18.jsonl:1: does not chain from the last line of the file before", ), ( "break-wrong-line", "2026-09-17.jsonl:4: prev is not the hash of the line before", ), ]; for (case, first) in cases { let (ok, out) = run(case); assert!(!ok, "{case}"); assert_eq!( out, format!("{first}\nsee docs/runbook.md#audit-chain-broken\n"), "{case}" ); } } #[test] fn an_empty_audit_directory_is_an_empty_log() { let home = Home::with_case("good"); for entry in std::fs::read_dir(home.path.join("audit")).unwrap() { std::fs::remove_file(entry.unwrap().path()).unwrap(); } let mut out = Vec::new(); assert!(bxctl::verify::run(&home.path, &mut out).unwrap()); assert_eq!( String::from_utf8(out).unwrap(), "audit: ok, 0 records, head none\n" ); } /// A home with no audit directory is a mistake in `--home`, not a clean log. #[test] fn a_missing_audit_directory_is_an_error() { let home = Home::with_case("good"); std::fs::remove_dir_all(home.path.join("audit")).unwrap(); let mut out = Vec::new(); let error = bxctl::verify::run(&home.path, &mut out).unwrap_err(); assert_eq!(error.kind(), std::io::ErrorKind::NotFound); assert!(out.is_empty()); }