Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
135 lines
5.1 KiB
Rust
135 lines
5.1 KiB
Rust
//! Audit writer edge cases found in the M3a review: a log with no complete record, a file whose
|
|
//! name only looks like a log, and the lock file outliving the writer.
|
|
|
|
#[path = "support/audit_dir.rs"]
|
|
mod audit_dir;
|
|
|
|
use audit_dir::{D1, D2, TempDir, denied, lines, ts};
|
|
use brokerd::audit::{Writer, verify_dir};
|
|
use bxcore::{AuditRecord, Hash32};
|
|
|
|
/// A kill between creating the day's file and writing its first record leaves it empty. It must
|
|
/// open like an empty log, not panic.
|
|
#[test]
|
|
fn one_zero_length_log_file_opens_as_an_empty_log() {
|
|
let dir = TempDir::unmade("zero-length");
|
|
std::fs::create_dir_all(&dir.path).unwrap();
|
|
std::fs::write(dir.path.join(D1), b"").unwrap();
|
|
|
|
let opened = Writer::open(&dir.path, false).unwrap();
|
|
assert!(!opened.recovered);
|
|
let mut writer = opened.writer;
|
|
assert_eq!(writer.next_seq(), 0);
|
|
writer
|
|
.append(ts("2026-09-17T08:00:00.000Z"), denied(1))
|
|
.unwrap();
|
|
|
|
let record: AuditRecord = serde_json::from_str(&lines(&dir.path, D1)[0]).unwrap();
|
|
assert_eq!((record.seq, record.prev), (0, Hash32::ZERO));
|
|
let report = verify_dir(&dir.path).unwrap();
|
|
assert_eq!(report.failure, None);
|
|
assert_eq!(report.records, 1);
|
|
}
|
|
|
|
/// The same crash, a day later: the older file holds records and the new one is empty.
|
|
#[test]
|
|
fn a_zero_length_file_after_records_continues_the_chain() {
|
|
let dir = TempDir::unmade("zero-length-later");
|
|
let mut writer = Writer::open(&dir.path, false).unwrap().writer;
|
|
writer
|
|
.append(ts("2026-09-17T08:00:00.000Z"), denied(1))
|
|
.unwrap();
|
|
drop(writer);
|
|
std::fs::write(dir.path.join(D2), b"").unwrap();
|
|
|
|
let mut writer = Writer::open(&dir.path, false).unwrap().writer;
|
|
assert_eq!(writer.next_seq(), 1);
|
|
writer
|
|
.append(ts("2026-09-18T08:00:00.000Z"), denied(2))
|
|
.unwrap();
|
|
let report = verify_dir(&dir.path).unwrap();
|
|
assert_eq!(report.failure, None);
|
|
assert_eq!(report.records, 2);
|
|
}
|
|
|
|
/// A crash part-way through the very first record: one file, one torn line, nothing else.
|
|
#[test]
|
|
fn a_single_torn_first_record_is_recovered() {
|
|
let dir = TempDir::unmade("torn-only");
|
|
std::fs::create_dir_all(&dir.path).unwrap();
|
|
std::fs::write(dir.path.join(D1), br#"{"seq":0,"time":"2026-09-17T08:"#).unwrap();
|
|
|
|
let opened = Writer::open(&dir.path, false).unwrap();
|
|
assert!(opened.recovered);
|
|
let report = verify_dir(&dir.path).unwrap();
|
|
assert_eq!(report.failure, None);
|
|
assert_eq!(report.recoveries.len(), 1);
|
|
}
|
|
|
|
/// `2026-0x-19.jsonl` is not a log file, for `brokerd` as for `bxctl audit verify`: it is neither
|
|
/// verified nor written to, and the real log goes on in its own files.
|
|
#[test]
|
|
fn a_file_that_only_looks_like_a_log_is_not_part_of_it() {
|
|
let dir = TempDir::case("good", None);
|
|
let odd = dir.path.join("2026-0x-19.jsonl");
|
|
std::fs::write(&odd, b"not a record\n").unwrap();
|
|
|
|
let mut writer = Writer::open(&dir.path, false)
|
|
.expect("a stray file does not break the chain")
|
|
.writer;
|
|
writer
|
|
.append(ts("2026-09-18T09:00:00.000Z"), denied(9))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
std::fs::read(&odd).unwrap(),
|
|
b"not a record\n",
|
|
"never written"
|
|
);
|
|
let report = verify_dir(&dir.path).unwrap();
|
|
assert_eq!(report.failure, None);
|
|
assert_eq!(
|
|
report.records, 11,
|
|
"the ten fixture records and the new one"
|
|
);
|
|
}
|
|
|
|
/// The lock file is never removed: a `brokerd` holding the old file open could otherwise lock it
|
|
/// while a new one creates and locks a fresh file, and both would write.
|
|
#[test]
|
|
fn the_lock_file_outlives_the_writer() {
|
|
let dir = TempDir::unmade("lock-stays");
|
|
let writer = Writer::open(&dir.path, false).unwrap().writer;
|
|
drop(writer);
|
|
assert!(dir.path.join(".lock").exists());
|
|
Writer::open(&dir.path, false).expect("the lock was released with the writer");
|
|
}
|
|
|
|
/// A torn last line in one file, then an empty later file (created, never written). The torn line
|
|
/// is ended in its own file and the recovery goes on the chain after it, so the log verifies and
|
|
/// the next start is an ordinary one. Found by the independent review of task 23.
|
|
#[test]
|
|
fn a_torn_line_before_an_empty_later_file_is_recovered_in_place() {
|
|
let dir = TempDir::unmade("torn-then-empty");
|
|
let mut writer = Writer::open(&dir.path, false).unwrap().writer;
|
|
writer
|
|
.append(ts("2026-09-17T08:00:00.000Z"), denied(1))
|
|
.unwrap();
|
|
drop(writer);
|
|
let mut d1 = std::fs::read(dir.path.join(D1)).unwrap();
|
|
d1.extend_from_slice(br#"{"seq":1,"time":"2026-09-17T09"#);
|
|
std::fs::write(dir.path.join(D1), &d1).unwrap();
|
|
std::fs::write(dir.path.join(D2), b"").unwrap();
|
|
|
|
let opened = Writer::open(&dir.path, false).unwrap();
|
|
assert!(opened.recovered);
|
|
drop(opened);
|
|
let report = verify_dir(&dir.path).unwrap();
|
|
assert_eq!(report.failure, None, "{:?}", report.failure);
|
|
assert_eq!(report.recoveries.len(), 1);
|
|
assert!(std::fs::read(dir.path.join(D1)).unwrap().ends_with(b"\n"));
|
|
|
|
let again = Writer::open(&dir.path, false).expect("the next start is an ordinary one");
|
|
assert!(!again.recovered);
|
|
}
|