xtask/src/drill.rs
//! `cargo xtask drill [--live --owner-yes]`: the 5.4 resilience drill.
//!
//! By default it runs the guardian functional tests on the mock cloud
//! (mock-upcloud plus the pulling gunnar mock). These are the tests CI runs:
//! a twin dies → case → button → new box laid → the main pulls adopt_twin → done;
//! main data lost → case → button → the main pulls recharge_from_twin, then
//! restate_entitlements (a signed snapshot) → done.
//!
//! `--live` is the owner's opt-in drill on real UpCloud (owner, 2026-10-10):
//! it may touch only boxes it creates itself, tagged [`tag`]; it never touches
//! gunnar.rs production, holger or the build box; it deletes everything
//! it created even when it fails; it never prints tokens; and its receipt goes
//! to `~/runs/repair-2026-10-10/`. The owner must say yes before the first live
//! run, so the live path refuses unless `--owner-yes` is given. It also refuses
//! until it is built: today it only names its plan.
/// What `cargo xtask drill` was asked to do.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Drill {
/// The mock drill: the guardian functional tests (the default).
Mock,
/// The live drill on real UpCloud; needs `--owner-yes`.
Live,
}
/// Parse the arguments after `drill`.
pub fn parse(args: &[String]) -> Result<Drill, String> {
let (mut live, mut yes) = (false, false);
for a in args {
match a.as_str() {
"--live" => live = true,
"--owner-yes" => yes = true,
other => return Err(format!("unknown flag `{other}` (usage: drill [--live --owner-yes])")),
}
}
match (live, yes) {
(false, false) => Ok(Drill::Mock),
(false, true) => Err("`--owner-yes` means nothing without `--live`".into()),
(true, false) => Err("the live drill runs on real UpCloud and needs the owner's yes first: add `--owner-yes` only after he has said so".into()),
(true, true) => Ok(Drill::Live),
}
}
/// The tag every box a live drill creates carries, e.g. `drill-r5-4-2026-10-10`.
pub fn tag(date: &str) -> String {
format!("drill-r5-4-{date}")
}
/// The cargo invocation the mock drill runs, as arguments to `cargo`.
pub fn mock_command() -> Vec<&'static str> {
vec!["test", "-p", "monetize-server", "--lib", "estate_order::tests::guardian"]
}
#[cfg(test)]
mod tests {
use super::*;
fn a(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
/// ★ Off by default: a bare `drill` is the mock.
#[test]
fn a_bare_drill_is_the_mock() {
assert_eq!(parse(&a(&[])), Ok(Drill::Mock));
}
/// ★ Live without the owner's yes is refused, naming why.
#[test]
fn live_without_the_owners_yes_is_refused() {
let e = parse(&a(&["--live"])).unwrap_err();
assert!(e.contains("owner"), "{e}");
assert_eq!(parse(&a(&["--live", "--owner-yes"])), Ok(Drill::Live));
assert_eq!(parse(&a(&["--owner-yes", "--live"])), Ok(Drill::Live));
}
/// `--owner-yes` alone is meaningless, and an unknown flag is refused.
#[test]
fn stray_flags_are_refused() {
assert!(parse(&a(&["--owner-yes"])).is_err());
assert!(parse(&a(&["--yes"])).is_err());
}
/// The live drill's tag, the one marker the cleanup is allowed to delete by.
#[test]
fn the_tag_names_the_drill_and_its_date() {
assert_eq!(tag("2026-10-10"), "drill-r5-4-2026-10-10");
}
/// The mock drill runs the guardian functional tests in monetize-server.
#[test]
fn the_mock_drill_runs_the_guardian_functional_tests() {
let c = mock_command();
assert_eq!(&c[..3], &["test", "-p", "monetize-server"]);
assert!(c.contains(&"estate_order::tests::guardian"), "{c:?}");
}
}