//! `brokerd serve --accept-break` on a real break, through the binary: it records the break and //! serves, and the next plain start serves too (spec section 12, "Audit edges"; missing from the //! given tests, found in the M3a review). #[path = "support/tmp.rs"] mod tmp; use std::io::Read; use std::os::unix::net::UnixStream; use std::path::Path; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use brokerd::audit::verify_dir; use tmp::TempDir; /// Start `brokerd serve` with `extra`, wait until it listens, stop it, and return its stderr. fn serve_once(config: &Path, tools: &Path, extra: &[&str]) -> String { let mut child = Command::new(env!("CARGO_BIN_EXE_brokerd")) .args(["serve", "--config"]) .arg(config) .args(extra) .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .unwrap(); let until = Instant::now() + Duration::from_secs(10); while UnixStream::connect(tools).is_err() { if let Some(status) = child.try_wait().unwrap() { let mut err = String::new(); child .stderr .take() .unwrap() .read_to_string(&mut err) .unwrap(); panic!("brokerd exited with {status}: {err}"); } assert!(Instant::now() < until, "brokerd never listened"); std::thread::sleep(Duration::from_millis(20)); } child.kill().unwrap(); child.wait().unwrap(); let mut err = String::new(); child .stderr .take() .unwrap() .read_to_string(&mut err) .unwrap(); err } #[test] fn a_break_is_accepted_and_the_next_plain_start_serves() { let dir = TempDir::new("accept-break"); let audit = dir.path().join("audit"); std::fs::create_dir_all(&audit).unwrap(); std::fs::create_dir_all(dir.path().join("grants")).unwrap(); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../proto/tests/fixtures/audit/changed-byte"); // The damage is in the first file; an ordinary start checks only the latest, so copy that one // alone, as `audit_startup.rs` does. std::fs::copy( fixture.join("2026-09-17.jsonl"), audit.join("2026-09-17.jsonl"), ) .unwrap(); let config = dir.write( "brokerd.toml", &format!( "[paths]\nhome = \"{0}\"\ngrants = \"{0}/grants\"\n", dir.path().display() ), ); let tools = dir.path().join("run/loop-broker/broker.sock"); // A plain start must refuse: wait for it to exit, and fail (not hang) if it serves instead. let mut plain = Command::new(env!("CARGO_BIN_EXE_brokerd")) .args(["serve", "--config"]) .arg(&config) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .unwrap(); let until = Instant::now() + Duration::from_secs(10); let status = loop { if let Some(status) = plain.try_wait().unwrap() { break status; } if Instant::now() > until { plain.kill().unwrap(); panic!("a plain start served on a broken chain"); } std::thread::sleep(Duration::from_millis(20)); }; assert_eq!(status.code(), Some(1), "a plain start refuses first"); let printed = serve_once(&config, &tools, &["--accept-break"]); assert!( printed.contains("audit: accepted the break at"), "{printed}" ); let printed = serve_once(&config, &tools, &[]); assert!(!printed.contains("accepted the break"), "{printed}"); let report = verify_dir(&audit).unwrap(); assert_eq!(report.failure, None, "{:?}", report.failure); assert_eq!(report.accepted_breaks.len(), 1); }