Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/testing/guardian.rs

//! **A Gunnar-shaped TEST estate guardian** (monetize v2.0 P2 step 2, item 12.0; A28 H6): the
//! rules the native `monetize-product-gunnar` guardian has (and the signed Gunnar and Holger
//! guests answer over ops 100-102), kept for the server's tests when the native product crates go
//! (A25 2.1, 2.2). Pure: it reads what the boxes REPORTED and answers findings and plans.
//!
//! | order | body | run by |
//! |---|---|---|
//! | `recharge_from_twin` | `{main, twin, accept_lost, epoch}` | the main (fresh or data-lost) |
//! | `fence_primary` | `{primary, epoch}` | the main, on itself, before a recharge while it still serves |
//! | `adopt_twin` | `{twin, principal}` | the main, once a new twin called home |
//! | `review_twin` | `{}` | the main |
//! | `restate_entitlements` | `{snapshot}` (signed `Snapshot`) | the main, once recharged and serving |
//!
//! Health words a box reports (flat): `serving` (`yes`/`no`/`fenced`), `data`
//! (`ok`/`lost`/`diverged`/`fresh`), `serves_at`, `twin` (`paired`/`silent`/`none`, on the
//! main), `primary` (`live`/`silent`, on the twin), `epoch`, `lag_entries`.

use monetize_product::guardian::{role, CloudStep, EstateGuardian, EstateView, Finding, Remedy, Step, StepState};
use monetize_product::guardian::rebuild::{self, Rebuild, Twin};

/// The one TEST guardian (stateless).
pub static GUARDIAN: TestGuardian = TestGuardian;

/// **Gunnar's guardian rules, for the tests** (a main, its twin, a front restored from backup).
pub struct TestGuardian;

/// How long a laid box has to call home (cloud create + installer + first boot).
pub const HEARD_WITHIN_MS: u64 = 30 * 60_000;
/// How long the main has to show its new twin paired after adopting it.
pub const PAIRED_WITHIN_MS: u64 = 10 * 60_000;
/// How long a recharge may take to show the main's data whole again.
pub const RECHARGED_WITHIN_MS: u64 = 6 * 3_600_000;

fn order(to: &str, kind: &str, body: &[(&str, &str)]) -> Step {
    Step::Order { role: to.into(), kind: kind.into(), body: body.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() }
}

fn wait(r: &str, what: &str, within_ms: u64) -> Step {
    Step::Await { role: r.into(), what: what.into(), within_ms }
}

fn recharge() -> Step {
    order(role::MAIN, "recharge_from_twin", &[("main", "$addr:main"), ("twin", "$addr:twin"), ("accept_lost", "false"), ("epoch", "$epoch")])
}

/// The money back on a recharged main: monetize's books as a signed snapshot it pulls. The
/// twin's log carries the caps on a good day; `accept_lost` and a lost control database are
/// the days it does not, and only monetize can re-state a sold cap.
fn restate() -> Step {
    order(role::MAIN, "restate_entitlements", &[("snapshot", "$entitlements")])
}

/// One line of evidence about a box: when it was heard and what it said.
fn heard(v: &EstateView, r: &str) -> String {
    match v.get(r) {
        None => format!("the {r}: no box registered"),
        Some(b) if b.heard_unix_ms == 0 => format!("the {r}: never heard since its key was registered"),
        Some(b) => {
            let words: Vec<String> = b.health.iter().map(|(k, v)| format!("{k}={v}")).collect();
            format!("the {r}: last report {} s ago ({})", v.now_unix_ms.saturating_sub(b.heard_unix_ms) / 1000, words.join(" "))
        }
    }
}

/// **Gunnar's twin as the guard reads it** (lane G11): it still hears its primary unless it says
/// `primary=silent`; it can give the estate back when it lacks no entry (`lag_entries` 0, an
/// older twin that reports none is taken at its word); the highest entry it holds is off its
/// own review (`twin_review`, `head_seq`/`applied_seq`), for the guard's R4 against the main's
/// last `data_head`. `None` while the twin is silent.
fn twin_of(v: &EstateView) -> Option<Twin> {
    let b = v.get(role::TWIN).filter(|_| !v.silent(role::TWIN))?;
    let lag: u64 = b.word("lag_entries").parse().unwrap_or(0);
    let review: Option<serde_json::Value> = serde_json::from_str(&b.twin_review).ok();
    let holds = review.as_ref().and_then(|r| Some(r.get("head_seq")?.as_u64()?.max(r.get("applied_seq").and_then(|a| a.as_u64()).unwrap_or(0))));
    Some(Twin {
        addr: rebuild::twin_addr(v),
        hears_primary: b.word("primary") != "silent",
        recharge_possible: lag == 0,
        verdict: if lag == 0 { "it lacks no entry".into() } else { format!("it lacks {lag} entr(y/ies) the main wrote") },
        holds,
    })
}

impl TestGuardian {
    fn main_word<'a>(v: &'a EstateView, key: &str) -> &'a str {
        v.get(role::MAIN).map(|b| b.word(key)).unwrap_or("")
    }
}

impl EstateGuardian for TestGuardian {
    fn diagnose(&self, v: &EstateView) -> Vec<Finding> {
        let main_silent = v.silent(role::MAIN);
        let has_twin = v.get(role::TWIN).is_some();
        let twin_silent = v.silent(role::TWIN);
        let twin_up = has_twin && !twin_silent;
        let evidence = vec![heard(v, role::MAIN), heard(v, role::TWIN)];
        if main_silent && has_twin && twin_silent {
            return vec![Finding {
                code: "both_lost".into(),
                role: role::MAIN.into(),
                words: "the main and the twin are both silent: a human looks first (provider console, last receipts)".into(),
                evidence,
                remedies: Vec::new(),
                auto: None,
            }];
        }
        let mut out = Vec::new();
        let data = Self::main_word(v, "data");
        if !main_silent && matches!(data, "lost" | "diverged") {
            out.push(Finding {
                code: "main_data_lost".into(),
                role: role::MAIN.into(),
                words: format!("the main reports its data {data}: recharge from the twin"),
                evidence: evidence.clone(),
                remedies: vec![Remedy::RechargeFromTwin],
                auto: twin_up.then_some(Remedy::RechargeFromTwin),
            });
        }
        if main_silent && twin_up {
            let twin_blind = v.get(role::TWIN).is_some_and(|b| b.word("primary") == "silent");
            // ★ Main rebuild (owner 2026-10-12), the guard's R4 over the health words: a twin
            // that reports entries it never applied (`lag_entries` > 0) cannot give the
            // estate back whole — a person decides; auto-fix does not rebuild from it.
            let lag: u64 = v.get(role::TWIN).map(|b| b.word("lag_entries")).and_then(|w| w.parse().ok()).unwrap_or(0);
            let words = match (twin_blind, lag) {
                (true, 0) => "the main is silent, and the twin cannot see it either: the main is rebuilt from the twin (never promoted)".to_string(),
                (true, n) => format!("the main is silent, and the twin cannot see it either — but the twin lacks {n} entr(y/ies) the main wrote: a person decides (accept the loss, or bring the main's disk back)"),
                (false, _) => "the main is silent to its provider; the twin still sees it".to_string(),
            };
            // ★ The guard (Gunnar's `rebuild::judge`, R1-R6, shared: lane G11): silence alone
            // never lays a main again. Auto-fix runs New server only where the guard says
            // `lay_again` — the main GONE at the provider, a twin that can give it back and
            // does not hear it, the main not ahead of it.
            let guard = rebuild::auto_new_server(v, twin_of(v));
            let auto = matches!(guard, Rebuild::LayAgain { .. });
            out.push(Finding {
                code: "main_lost".into(),
                role: role::MAIN.into(),
                words: format!("{words} — the guard: {}", guard.why()),
                evidence: evidence.clone(),
                remedies: vec![Remedy::NewServer, Remedy::RechargeFromTwin],
                auto: auto.then_some(Remedy::NewServer),
            });
        }
        let main_says_twin_silent = !main_silent && matches!(Self::main_word(v, "twin"), "silent" | "lost");
        if !main_silent && ((has_twin && twin_silent) || main_says_twin_silent) {
            out.push(Finding {
                code: "twin_lost".into(),
                role: role::TWIN.into(),
                words: "the twin is silent; the main serves alone".into(),
                evidence,
                remedies: vec![Remedy::NewTwin, Remedy::ReviewTwin],
                auto: Some(Remedy::NewTwin),
            });
        }
        out
    }

    fn plan(&self, remedy: Remedy, v: &EstateView) -> Result<Vec<Step>, String> {
        let main_up = !v.silent(role::MAIN);
        let twin_up = !v.silent(role::TWIN);
        match remedy {
            Remedy::NewTwin => {
                if !main_up {
                    return Err("no primary is reporting to adopt a new twin: the main comes first (New server)".into());
                }
                Ok(vec![
                    Step::Cloud(CloudStep::Fence { role: role::TWIN.into() }),
                    Step::Cloud(CloudStep::Lay { role: role::TWIN.into() }),
                    wait(role::TWIN, "heard", HEARD_WITHIN_MS),
                    order(role::MAIN, "adopt_twin", &[("twin", "$addr:twin"), ("principal", "$principal")]),
                    wait(role::MAIN, "paired", PAIRED_WITHIN_MS),
                ])
            }
            Remedy::NewServer => {
                if !twin_up {
                    return Err("no twin is reporting: a new main would have nothing to recharge from".into());
                }
                // The guard before the fence gives the old main back whole (lane G11).
                rebuild::new_server(v, twin_of(v))?;
                Ok(vec![
                    Step::Cloud(CloudStep::Fence { role: role::MAIN.into() }),
                    Step::Cloud(CloudStep::Lay { role: role::MAIN.into() }),
                    wait(role::MAIN, "heard", HEARD_WITHIN_MS),
                    recharge(),
                    wait(role::MAIN, "recharged", RECHARGED_WITHIN_MS),
                    restate(),
                ])
            }
            Remedy::RechargeFromTwin => {
                if !twin_up {
                    return Err("no twin is reporting: nothing to recharge from".into());
                }
                if !main_up {
                    return Err("the main is silent and cannot pull its order: press New server".into());
                }
                let mut steps = Vec::new();
                if Self::main_word(v, "serving") == "yes" {
                    steps.push(order(role::MAIN, "fence_primary", &[("primary", "$addr:main"), ("epoch", "$epoch")]));
                }
                steps.push(recharge());
                steps.push(wait(role::MAIN, "recharged", RECHARGED_WITHIN_MS));
                steps.push(restate());
                Ok(steps)
            }
            Remedy::ReviewTwin => {
                if !main_up {
                    return Err("the main is silent: nobody to review the twin".into());
                }
                Ok(vec![order(role::MAIN, "review_twin", &[])])
            }
            // S8: the front reports nothing on Fleet; main and twin do, and they are the proof
            // that the estate the restored front comes back into stands.
            Remedy::RestoreFront => {
                if !main_up && !twin_up {
                    return Err("main and twin are silent: a front is restored only into an estate whose boxes stand".into());
                }
                let heard = if main_up { role::MAIN } else { role::TWIN };
                Ok(vec![Step::Cloud(CloudStep::Restore { role: role::FRONT.into() }), wait(heard, "heard", HEARD_WITHIN_MS)])
            }
        }
    }

    fn observe(&self, r: &str, what: &str, v: &EstateView) -> StepState {
        let Some(b) = v.get(r) else { return StepState::Waiting(format!("the {r} is not registered yet")) };
        match what {
            "paired" => match b.word("twin") {
                "paired" => StepState::Met(format!("the {r} reports its twin paired")),
                w => StepState::Waiting(format!("the {r} reports twin={w}")),
            },
            "recharged" => match (b.word("data"), b.word("recharge")) {
                ("ok", _) => StepState::Met(format!("the {r} reports its data whole")),
                (_, "failed") => StepState::Failed(format!("the {r} reports the recharge failed")),
                (d, rc) => StepState::Waiting(format!("the {r} reports data={d} recharge={rc}")),
            },
            other => StepState::Failed(format!("gunnar's guardian does not know how to await {other:?}")),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use monetize_product::guardian::BoxView;

    use super::*;

    const NOW: u64 = 1_800_000_000_000;

    fn boxed(r: &str, heard_ago_ms: Option<u64>, words: &[(&str, &str)]) -> (String, BoxView) {
        let health: BTreeMap<String, String> = words.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
        (r.to_string(), BoxView { role: r.into(), heard_unix_ms: heard_ago_ms.map(|a| NOW - a).unwrap_or(0), health, ..Default::default() })
    }

    /// The provider's word on the box (the runner fills it for a silent main monetize laid).
    fn at(mut b: (String, BoxView), word: &str) -> (String, BoxView) {
        b.1.at_provider = word.into();
        b
    }

    fn view(boxes: Vec<(String, BoxView)>) -> EstateView {
        EstateView { product: "gunnar".into(), instance: "acme".into(), now_unix_ms: NOW, silent_after_ms: 90_000, boxes: boxes.into_iter().collect() }
    }

    fn healthy_main() -> (String, BoxView) {
        boxed("main", Some(1_000), &[("serving", "yes"), ("data", "ok"), ("serves_at", "10.6.0.10:50051"), ("twin", "paired")])
    }
    fn healthy_twin() -> (String, BoxView) {
        boxed("twin", Some(1_000), &[("serving", "no"), ("data", "ok"), ("serves_at", "10.6.0.12:50051"), ("primary", "live")])
    }

    fn codes(v: &EstateView) -> Vec<String> {
        GUARDIAN.diagnose(v).into_iter().map(|f| f.code).collect()
    }

    #[test]
    fn a_healthy_estate_has_no_finding() {
        assert!(codes(&view(vec![healthy_main(), healthy_twin()])).is_empty());
        // An estate with no twin at all is not a lost twin.
        assert!(codes(&view(vec![healthy_main()])).is_empty());
    }

    /// ★ Main rebuild (R4): a silent main whose twin cannot see it either is rebuilt by
    /// auto-fix only when the twin is level; a twin that lacks entries leaves it to a person.
    #[test]
    fn a_lost_main_is_rebuilt_automatically_only_from_a_level_twin() {
        let silent_main = at(boxed("main", Some(600_000), &[("data", "ok"), ("data_head", "120")]), "gone");
        let level = boxed("twin", Some(1_000), &[("primary", "silent"), ("lag_entries", "0")]);
        let f = GUARDIAN.diagnose(&view(vec![silent_main.clone(), level]));
        assert_eq!((f[0].code.as_str(), f[0].auto), ("main_lost", Some(Remedy::NewServer)));
        assert!(f[0].words.contains("never promoted"), "{}", f[0].words);
        let behind = boxed("twin", Some(1_000), &[("primary", "silent"), ("lag_entries", "20")]);
        let f = GUARDIAN.diagnose(&view(vec![silent_main.clone(), behind]));
        assert_eq!((f[0].code.as_str(), f[0].auto), ("main_lost", None), "a person decides");
        assert!(f[0].words.contains("lacks 20"), "{}", f[0].words);
        assert_eq!(f[0].remedies, vec![Remedy::NewServer, Remedy::RechargeFromTwin], "the buttons stay");
        // An older twin that reports no lag word keeps the earlier behaviour.
        let old = boxed("twin", Some(1_000), &[("primary", "silent")]);
        assert_eq!(GUARDIAN.diagnose(&view(vec![silent_main.clone(), old]))[0].auto, Some(Remedy::NewServer));
        // The guard's R4 over the reports: the main's last `data_head` above what the twin's own
        // review holds is a person's call, even from a level twin.
        let mut reviewed = boxed("twin", Some(1_000), &[("primary", "silent"), ("lag_entries", "0")]);
        reviewed.1.twin_review = r#"{"head_seq":100,"applied_seq":100}"#.into();
        let f = GUARDIAN.diagnose(&view(vec![silent_main, reviewed]));
        assert_eq!(f[0].auto, None, "{}", f[0].words);
        assert!(f[0].words.contains("20 entr"), "{}", f[0].words);
    }

    /// ★ **Row 5 (lane G11): silence alone never lays a main again.** The guard (Gunnar's
    /// `rebuild::judge`) reads the provider: a main GONE is laid again by auto-fix; a standing
    /// one is booted again first, a stopped one started, and an unasked provider is no evidence.
    #[test]
    fn auto_fix_lays_a_silent_main_again_only_when_the_provider_says_it_is_gone() {
        let level = || boxed("twin", Some(1_000), &[("primary", "silent"), ("lag_entries", "0")]);
        for (word, auto, said) in [("gone", Some(Remedy::NewServer), "GONE"), ("active", None, "booted once more"), ("stopped", None, "start it"), ("", None, "not asked")] {
            let f = GUARDIAN.diagnose(&view(vec![at(boxed("main", Some(600_000), &[]), word), level()]));
            assert_eq!((f[0].code.as_str(), f[0].auto), ("main_lost", auto), "{word:?}: {}", f[0].words);
            assert!(f[0].words.contains(said), "{word:?}: {}", f[0].words);
            assert_eq!(f[0].remedies, vec![Remedy::NewServer, Remedy::RechargeFromTwin], "the buttons stay");
        }
    }

    #[test]
    fn a_silent_twin_is_twin_lost_and_auto_fix_may_lay_a_new_one() {
        let f = GUARDIAN.diagnose(&view(vec![healthy_main(), boxed("twin", Some(600_000), &[])]));
        assert_eq!(f.len(), 1);
        assert_eq!((f[0].code.as_str(), f[0].role.as_str()), ("twin_lost", "twin"));
        assert_eq!(f[0].remedies, vec![Remedy::NewTwin, Remedy::ReviewTwin]);
        assert_eq!(f[0].auto, Some(Remedy::NewTwin));
        assert!(!f[0].evidence.is_empty());
        // The main saying its twin is silent is the same finding, even with no twin box known.
        let main = boxed("main", Some(1_000), &[("serving", "yes"), ("data", "ok"), ("twin", "silent")]);
        assert_eq!(codes(&view(vec![main])), vec!["twin_lost"]);
    }

    #[test]
    fn a_main_that_lost_its_data_is_recharged_from_the_twin() {
        let main = boxed("main", Some(1_000), &[("serving", "no"), ("data", "lost"), ("serves_at", "10.6.0.10:50051")]);
        let f = GUARDIAN.diagnose(&view(vec![main.clone(), healthy_twin()]));
        assert_eq!(f[0].code, "main_data_lost");
        assert_eq!((f[0].remedies.clone(), f[0].auto), (vec![Remedy::RechargeFromTwin], Some(Remedy::RechargeFromTwin)));
        let diverged = boxed("main", Some(1_000), &[("serving", "yes"), ("data", "diverged")]);
        assert_eq!(codes(&view(vec![diverged, healthy_twin()])), vec!["main_data_lost"]);
        // With no twin heard there is nothing to recharge from: no auto.
        let f = GUARDIAN.diagnose(&view(vec![main, boxed("twin", Some(600_000), &[])]));
        assert_eq!(f[0].code, "main_data_lost");
        assert_eq!(f[0].auto, None);
    }

    #[test]
    fn a_silent_main_is_main_lost_and_auto_only_when_the_twin_cannot_see_it_either() {
        let silent_main = boxed("main", Some(600_000), &[]);
        let f = GUARDIAN.diagnose(&view(vec![silent_main.clone(), healthy_twin()]));
        assert_eq!(f[0].code, "main_lost");
        assert_eq!(f[0].remedies, vec![Remedy::NewServer, Remedy::RechargeFromTwin]);
        assert_eq!(f[0].auto, None, "the twin still sees the primary: monetize's view alone never lays a main");
        let twin_blind = boxed("twin", Some(1_000), &[("serves_at", "10.6.0.12:50051"), ("primary", "silent")]);
        let f = GUARDIAN.diagnose(&view(vec![at(silent_main, "gone"), twin_blind]));
        assert_eq!(f[0].auto, Some(Remedy::NewServer));
    }

    /// ★ **A main merely cut off from its twin keeps serving** (owner 20:20Z): it still reports
    /// to monetize, so it is never fenced, replaced or recharged — the only finding is the
    /// twin's, and its repair touches the twin alone. The twin catches up from the main.
    #[test]
    fn a_main_cut_off_from_its_twin_but_heard_by_monetize_is_never_fenced_or_replaced() {
        let cut_off = boxed("main", Some(1_000), &[("serving", "yes"), ("data", "ok"), ("serves_at", "10.6.0.10:50051"), ("twin", "silent")]);
        let blind_twin = boxed("twin", Some(1_000), &[("serving", "no"), ("data", "ok"), ("serves_at", "10.6.0.12:50051"), ("primary", "silent")]);
        for v in [view(vec![cut_off.clone(), blind_twin]), view(vec![cut_off.clone(), boxed("twin", Some(600_000), &[])])] {
            let f = GUARDIAN.diagnose(&v);
            assert_eq!(f.iter().map(|f| (f.code.as_str(), f.role.as_str())).collect::<Vec<_>>(), [("twin_lost", "twin")], "{f:?}");
            assert!(f.iter().all(|f| !f.remedies.contains(&Remedy::NewServer) && !f.remedies.contains(&Remedy::RechargeFromTwin)));
            let steps = GUARDIAN.plan(f[0].auto.expect("auto"), &v).unwrap();
            let kinds = order_kinds(&steps);
            assert!(kinds.iter().all(|k| !k.contains("main") || k.starts_with("adopt_twin") || k == "await main paired"), "the main is left serving: {kinds:?}");
        }
    }

    #[test]
    fn both_silent_is_for_a_human() {
        let f = GUARDIAN.diagnose(&view(vec![boxed("main", Some(600_000), &[]), boxed("twin", Some(600_000), &[])]));
        assert_eq!(f.len(), 1);
        assert_eq!(f[0].code, "both_lost");
        assert_eq!(f[0].auto, None);
    }

    fn order_kinds(steps: &[Step]) -> Vec<String> {
        steps
            .iter()
            .map(|s| match s {
                Step::Cloud(CloudStep::Fence { role }) => format!("fence {role}"),
                Step::Cloud(CloudStep::Lay { role }) => format!("lay {role}"),
                Step::Cloud(CloudStep::Restore { role }) => format!("restore {role}"),
                Step::Order { role, kind, .. } => format!("{kind}→{role}"),
                Step::Await { role, what, .. } => format!("await {role} {what}"),
            })
            .collect()
    }

    #[test]
    fn new_twin_fences_lays_awaits_and_the_primary_adopts() {
        let v = view(vec![healthy_main(), boxed("twin", Some(600_000), &[])]);
        let steps = GUARDIAN.plan(Remedy::NewTwin, &v).unwrap();
        assert_eq!(order_kinds(&steps), vec!["fence twin", "lay twin", "await twin heard", "adopt_twin→main", "await main paired"]);
        let Step::Order { body, .. } = &steps[3] else { panic!() };
        assert_eq!(body.get("twin").map(String::as_str), Some("$addr:twin"));
        assert_eq!(body.get("principal").map(String::as_str), Some("$principal"));
        // No primary standing to adopt it: refused.
        assert!(GUARDIAN.plan(Remedy::NewTwin, &view(vec![boxed("main", Some(600_000), &[]), healthy_twin()])).is_err());
    }

    #[test]
    fn new_server_fences_the_main_lays_one_and_it_pulls_the_recharge() {
        let blind = || boxed("twin", Some(1_000), &[("serving", "no"), ("serves_at", "10.6.0.12:50051"), ("primary", "silent"), ("lag_entries", "0")]);
        let v = view(vec![at(boxed("main", Some(600_000), &[]), "gone"), blind()]);
        let steps = GUARDIAN.plan(Remedy::NewServer, &v).unwrap();
        assert_eq!(order_kinds(&steps), vec!["fence main", "lay main", "await main heard", "recharge_from_twin→main", "await main recharged", "restate_entitlements→main"]);
        let Step::Order { body, .. } = &steps[5] else { panic!() };
        assert_eq!(body.get("snapshot").map(String::as_str), Some("$entitlements"), "the money goes back on the new main as a signed snapshot it pulls");
        let Step::Order { body, .. } = &steps[3] else { panic!() };
        assert_eq!(body.get("main").map(String::as_str), Some("$addr:main"));
        assert_eq!(body.get("twin").map(String::as_str), Some("$addr:twin"));
        assert_eq!(body.get("epoch").map(String::as_str), Some("$epoch"));
        assert_eq!(body.get("accept_lost").map(String::as_str), Some("false"));
        // Nothing to recharge from: refused before a box is laid.
        assert!(GUARDIAN.plan(Remedy::NewServer, &view(vec![boxed("main", Some(600_000), &[]), boxed("twin", Some(600_000), &[])])).is_err());
        // ★ The guard (lane G11): a person's New server destroys no standing disk and lays no
        // second main beside one the twin still hears.
        let standing = GUARDIAN.plan(Remedy::NewServer, &view(vec![at(boxed("main", Some(600_000), &[]), "active"), blind()])).unwrap_err();
        assert!(standing.contains("booted once more"), "{standing}");
        let heard = GUARDIAN.plan(Remedy::NewServer, &view(vec![at(boxed("main", Some(600_000), &[]), "gone"), healthy_twin()])).unwrap_err();
        assert!(heard.contains("still hears its primary"), "{heard}");
        let answers = GUARDIAN.plan(Remedy::NewServer, &view(vec![boxed("main", Some(1_000), &[("serving", "yes"), ("data", "ok")]), blind()])).unwrap_err();
        assert!(answers.contains("nothing is rebuilt"), "{answers}");
        // An operator's estate (monetize laid no main: no provider word): the twin's half of the
        // guard decides here, and the operator's kit confirms before it destroys anything.
        assert!(GUARDIAN.plan(Remedy::NewServer, &view(vec![boxed("main", Some(600_000), &[]), blind()])).is_ok());
    }

    /// ★ S8: Restore front lays and fences nothing — the front restored from its backup, then
    /// the estate heard again; refused while main and twin are both silent.
    #[test]
    fn restore_front_restores_from_backup_and_lays_nothing() {
        let steps = GUARDIAN.plan(Remedy::RestoreFront, &view(vec![healthy_main(), healthy_twin()])).unwrap();
        assert_eq!(order_kinds(&steps), vec!["restore front", "await main heard"]);
        let steps = GUARDIAN.plan(Remedy::RestoreFront, &view(vec![boxed("main", Some(600_000), &[]), healthy_twin()])).unwrap();
        assert_eq!(order_kinds(&steps), vec!["restore front", "await twin heard"], "the twin stands for a silent main");
        let both = GUARDIAN.plan(Remedy::RestoreFront, &view(vec![boxed("main", Some(600_000), &[]), boxed("twin", Some(600_000), &[])]));
        assert!(both.is_err_and(|w| w.contains("silent")));
        assert_eq!(Remedy::parse("restore_front"), Some(Remedy::RestoreFront), "the Press RPC takes its wire name");
    }

    #[test]
    fn recharge_on_a_standing_main_uses_no_cloud_and_fences_it_first_only_while_it_serves() {
        let lost = boxed("main", Some(1_000), &[("serving", "no"), ("data", "lost")]);
        let steps = GUARDIAN.plan(Remedy::RechargeFromTwin, &view(vec![lost, healthy_twin()])).unwrap();
        assert_eq!(order_kinds(&steps), vec!["recharge_from_twin→main", "await main recharged", "restate_entitlements→main"]);
        let diverged = boxed("main", Some(1_000), &[("serving", "yes"), ("data", "diverged")]);
        let steps = GUARDIAN.plan(Remedy::RechargeFromTwin, &view(vec![diverged, healthy_twin()])).unwrap();
        assert_eq!(order_kinds(&steps), vec!["fence_primary→main", "recharge_from_twin→main", "await main recharged", "restate_entitlements→main"]);
        assert!(GUARDIAN.plan(Remedy::RechargeFromTwin, &view(vec![healthy_main(), boxed("twin", Some(600_000), &[])])).is_err());
    }

    #[test]
    fn review_twin_is_one_order_to_the_main() {
        let steps = GUARDIAN.plan(Remedy::ReviewTwin, &view(vec![healthy_main(), healthy_twin()])).unwrap();
        assert_eq!(order_kinds(&steps), vec!["review_twin→main"]);
    }

    #[test]
    fn observe_reads_paired_and_recharged_from_the_mains_report() {
        let v = view(vec![healthy_main(), healthy_twin()]);
        assert!(matches!(GUARDIAN.observe("main", "paired", &v), StepState::Met(_)));
        assert!(matches!(GUARDIAN.observe("main", "recharged", &v), StepState::Met(_)));
        let v = view(vec![boxed("main", Some(1_000), &[("data", "lost"), ("twin", "silent"), ("recharge", "failed")])]);
        assert!(matches!(GUARDIAN.observe("main", "paired", &v), StepState::Waiting(_)));
        assert!(matches!(GUARDIAN.observe("main", "recharged", &v), StepState::Failed(_)));
        assert!(matches!(GUARDIAN.observe("main", "dancing", &v), StepState::Failed(_)));
    }
}