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/estate_order/tests/held_stopped.rs

//! ★ **SEC-S1 (sec review 2026-11-01): the guardian never starts what monetize stopped on
//! purpose.** BOX-START's start pass (e9d2d1b1) started every silent box the provider read
//! `stopped` — also the boxes the term itself had stopped: an estate unpaid past its grace, or
//! cancelled by its buyer at the end of its paid period, ran free until its deletion date.
//! Monetize knows why it stopped them (the run's phase and its `stopped` list), and the guardian
//! asks it before it starts anything: the estate stays stopped, its disks kept, nothing laid.

use super::*;

/// `acme` delivered, its fleet row known to the guardian with auto-fix ON (the start pass
/// needs no auto-fix; a rebuild would), and every box heard.
fn guarded(name: &str) -> (Rig, Vec<String>) {
    let r = delivered(name, cfg());
    r.state
        .fleet
        .link("gunnar", &crate::testing::guardian::GUARDIAN);
    let run = r.state.estates.get("acme").unwrap();
    let g = r.state.fleet.guardian().unwrap();
    if g.store.product_of(run.fleet_name()).is_none() {
        g.store.declare(run.fleet_name(), "gunnar").unwrap();
    }
    g.store.set_auto_fix(run.fleet_name(), true).unwrap();
    // Every box laid has its fleet key (as the install pins them): the guardian knows its roles.
    for (i, m) in run.laid.as_ref().unwrap().machines.iter().enumerate() {
        let key = monetize::sign::Signer::from_bytes(&[40 + i as u8; 32]).public_key();
        g.store
            .register_key(
                run.fleet_name(),
                &m.role,
                &monetize_embed::signing::base64_encode(&key),
            )
            .unwrap();
    }
    let servers = server_ids(&run);
    drive(&r.state, "acme");
    (r, servers)
}

/// The guardian's ticks for `minutes` (one a minute, the boxes silent: a stopped box calls
/// nobody), its provider looks asked inline. Its lines.
fn silent_ticks(r: &Rig, minutes: u64) -> Vec<String> {
    let mut lines = Vec::new();
    for _ in 0..minutes {
        r.clock.fetch_add(60_000, Ordering::SeqCst);
        lines.extend(crate::fleet::tick(&r.state));
    }
    lines
}

fn no_start(r: &Rig, servers: &[String], lines: &[String]) {
    assert!(
        servers.iter().all(|s| !r.cloud.running(s)),
        "the term's stop stands: {lines:#?}"
    );
    assert!(
        !lines.iter().any(|l| l.contains("guardian.started")),
        "nothing is started: {lines:#?}"
    );
    let g = r.state.fleet.guardian().unwrap();
    assert!(
        g.store
            .cases_of("acme")
            .iter()
            .all(|c| c.finding.code != monetize_guardian::STOPPED_AT_PROVIDER),
        "no start case: {:#?}",
        g.store.cases_of("acme")
    );
    assert!(
        r.cloud
            .calls
            .lock()
            .unwrap()
            .iter()
            .all(|c| !matches!(c, monetize::testing::CloudCall::Power(_, true))),
        "no power-on was asked"
    );
}

/// RED: unpaid past its grace, the term stops every server (SUSPENDED); an hour of the
/// guardian's ticks starts none of them.
#[test]
fn an_estate_the_term_stopped_for_nonpayment_stays_stopped_through_the_start_pass() {
    let (r, servers) = guarded("held-unpaid");
    let paid = r.state.estates.get("acme").unwrap().paid_until_ms;
    at(&r, paid);
    let run = at(&r, paid + 14 * DAY);
    assert_eq!(run.phase, phase::SUSPENDED);
    assert!(
        servers.iter().all(|s| !r.cloud.running(s)),
        "every server is stopped"
    );
    let lines = silent_ticks(&r, 60);
    no_start(&r, &servers, &lines);
    // Renewed and paid: the term starts the boxes itself, as before.
    assert_eq!(r.state.estates.get("acme").unwrap().phase, phase::SUSPENDED);
}

/// RED: cancelled by its buyer, stopped at the end of its paid period: stays stopped.
#[test]
fn an_estate_its_buyer_cancelled_stays_stopped_through_the_start_pass() {
    let (r, servers) = guarded("held-cancelled");
    let paid = r.state.estates.get("acme").unwrap().paid_until_ms;
    cancel(&r.state, "acme").expect("cancelled");
    let run = at(&r, paid);
    assert_eq!(run.phase, phase::SUSPENDED, "{:#?}", run.history);
    let lines = silent_ticks(&r, 60);
    no_start(&r, &servers, &lines);
}
/// The guardian says why it leaves the estate alone (the run's own words), and the shop's
/// world answers it for every role; a running estate is held by nothing.
#[test]
fn the_shops_world_says_why_it_holds_an_estate_stopped() {
    use monetize_guardian::World as _;
    let (r, _) = guarded("held-why");
    let world = crate::fleet::ShopWorld { state: &r.state };
    assert_eq!(
        world.estate_held("acme"),
        None,
        "a paid, running estate is held by nothing"
    );
    assert_eq!(world.box_held("acme", "main"), None);
    let paid = r.state.estates.get("acme").unwrap().paid_until_ms;
    at(&r, paid);
    at(&r, paid + 14 * DAY);
    let world = crate::fleet::ShopWorld { state: &r.state };
    let why = world.estate_held("acme").expect("held by the term");
    assert!(why.contains("unpaid"), "{why}");
    assert!(
        world
            .box_held("acme", "main")
            .is_some_and(|w| w.contains("term")),
        "the run's stopped list names the main"
    );
}