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

nordisk/monetize

crates/monetize-cloud/src/sandbox/tests.rs

//! Red-first for the rehearsal cloud. What matters is what it REFUSES and what it
//! REMEMBERS: a cloud that says yes to everything and forgets on restart cannot
//! rehearse the one property this seam exists for — never buying twice.
//!
//! Assertions are on applied output: the resources it recorded, the bytes on
//! disk, the mock's recorded requests. Never on a round-trip.

use super::*;
use crate::cap::{Ceiling, SpendCap};
use crate::ServerRole;

const REF: &str = "gunnar/alice/2026-09";
const REF2: &str = "gunnar/alice/2026-09/increase";
const SRV: &str = "gunnar-se-sto1";

fn tmp(name: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "monetize-cloud-sandbox-{}-{name}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    dir.join("sandbox-cloud.json")
}

/// 23 cents/GiB/month and two plans, in EUR — the declared fallback.
fn declared() -> DeclaredPrices {
    DeclaredPrices {
        disk_minor_per_gib_month: 23,
        plan_minor_per_month: BTreeMap::from([
            ("STARTER-2xCPU-4GB".to_string(), 1304),
            ("STARTER-4xCPU-16GB".to_string(), 3042),
        ]),
        currency: Currency::Eur,
    }
}

fn cfg(pool_gib: u64) -> SandboxConfig {
    let mut c = SandboxConfig::new();
    c.pool_gib = pool_gib;
    c.declared = Some(declared());
    c
}

fn sandbox(pool_gib: u64) -> Sandbox {
    Sandbox::new(cfg(pool_gib))
        .unwrap()
        .with_clock(|| 1_788_000_000_000)
}

fn disk(gib: u64) -> Resource {
    Resource::Disk {
        server: ServerId(SRV.into()),
        gib,
    }
}

fn provisioned(e: &Ensured) -> (ResourceId, Price) {
    match e {
        Ensured::Provisioned { id, cost } => (id.clone(), *cost),
        other => panic!("expected Provisioned, got {other:?}"),
    }
}

// ── ensure: marked, priced, idempotent on reference + fingerprint ─────────────

#[test]
fn a_purchase_records_fake_iron_marked_sandbox_at_the_declared_price() {
    let sb = sandbox(0);
    let out = sb.ensure(REF, &disk(110)).unwrap();
    let (id, cost) = provisioned(&out);
    assert_eq!(id.provider, "sandbox");
    assert_eq!(
        id.id, "sandbox:disk:1",
        "the kind and the counter are in the id"
    );
    assert_eq!(
        cost,
        Price {
            minor_per_month: 110 * 23,
            currency: Currency::Eur
        },
        "110 GiB at 23 cents, declared"
    );

    let rs = sb.resources();
    assert_eq!(rs.len(), 1);
    let r = &rs[0];
    assert_eq!(r.reference, REF, "rule 2: attached to one order");
    assert_eq!(r.fingerprint, "disk:gunnar-se-sto1:110");
    assert_eq!(r.labels[LABEL_REF], REF);
    assert_eq!(r.labels[LABEL_RES], "disk:gunnar-se-sto1:110");
    assert_eq!(
        r.labels[LABEL_PROVENANCE], "sandbox",
        "the mark the console shows"
    );
    assert_eq!(
        r.labels[LABEL_PRICE], "declared",
        "and where the price came from"
    );
    assert_eq!(r.status, ResourceStatus::Active);
    assert_eq!(r.created_unix_ms, 1_788_000_000_000);
    assert_eq!(
        r.size_gib, 110,
        "110 is already a multiple of the 10 GiB step"
    );
    assert_eq!(sb.live_gib(), 110);
}

/// **The double-charge seam.** The same (reference, resource) twice mints ONE
/// resource; a new reference for the same resource mints a second — which is
/// exactly why the transaction computes the delta before it calls this.
#[test]
fn ensure_is_idempotent_on_reference_plus_fingerprint_like_the_label_search() {
    let sb = sandbox(0);
    let first = sb.ensure(REF, &disk(10)).unwrap();
    let again = sb.ensure(REF, &disk(10)).unwrap();
    assert_eq!(
        first, again,
        "a retried attempt gets the same resource back"
    );
    assert_eq!(
        sb.resources().len(),
        1,
        "one resource for one (reference, resource)"
    );
    assert_eq!(sb.live_gib(), 10);

    // Twin: a different reference IS a different purchase.
    let other = sb.ensure(REF2, &disk(10)).unwrap();
    assert_ne!(provisioned(&first).0, provisioned(&other).0);
    assert_eq!(sb.resources().len(), 2);
    assert_eq!(sb.live_gib(), 20, "MEASURED: a re-ensure under a new reference buys again — the transaction must send the delta");

    // Twin: the same reference with a different SIZE is a different fingerprint.
    let bigger = sb.ensure(REF, &disk(20)).unwrap();
    assert_ne!(provisioned(&first).0, provisioned(&bigger).0);
    assert_eq!(sb.resources().len(), 3);
}

/// The 110 GiB shape the transaction tests use: 10 held, 90 more ensured under a
/// new reference → 100 live, never 110. The provider's half of the delta rule.
#[test]
fn the_increase_delta_lands_as_ten_plus_ninety_not_one_hundred_and_ten() {
    let sb = sandbox(0);
    sb.ensure(REF, &disk(10)).unwrap();
    sb.ensure(REF2, &disk(90)).unwrap();
    assert_eq!(
        sb.live_gib(),
        100,
        "10 + 90: the tenant holds what was asked for"
    );
    let sizes: Vec<u64> = sb.resources().iter().map(|r| r.size_gib).collect();
    assert_eq!(sizes, vec![10, 90]);

    // Twin: the naive re-purchase of the whole 100 under the new reference.
    let naive = sandbox(0);
    naive.ensure(REF, &disk(10)).unwrap();
    naive.ensure(REF2, &disk(100)).unwrap();
    assert_eq!(
        naive.live_gib(),
        110,
        "MEASURED: selling the bigger plan outright buys the 10 again"
    );
}

// ── the pool ─────────────────────────────────────────────────────────────────

#[test]
fn a_declared_pool_answers_on_hand_and_a_bought_surplus_becomes_pool() {
    let sb = sandbox(100);
    assert_eq!(
        sb.ensure(REF, &disk(50)).unwrap(),
        Ensured::OnHand {
            pool_free_after: 50
        }
    );
    assert_eq!(sb.resources().len(), 0, "OnHand mints nothing");
    // 60 > 50 free: buys. 60 is on the 10 GiB step already.
    let (_, cost) = provisioned(&sb.ensure(REF2, &disk(60)).unwrap());
    assert_eq!(cost.minor_per_month, 60 * 23);
    // pool = 100 declared + 60 bought − 110 sold = 50 free.
    assert_eq!(
        sb.ensure("gunnar/beta/2026-09", &disk(50)).unwrap(),
        Ensured::OnHand { pool_free_after: 0 }
    );
    // The step rounds a purchase up and the surplus is tomorrow's pool: 5 → 10, 5 spare.
    let (_, cost) = provisioned(&sb.ensure("gunnar/gamma/2026-09", &disk(5)).unwrap());
    assert_eq!(
        cost.minor_per_month,
        10 * 23,
        "priced on the size bought, not the size asked"
    );
    assert_eq!(
        sb.ensure("gunnar/delta/2026-09", &disk(5)).unwrap(),
        Ensured::OnHand { pool_free_after: 0 }
    );
    assert_eq!(sb.live_gib(), 70);

    // Twin: no pool, everything is bought.
    let none = sandbox(0);
    assert!(matches!(
        none.ensure(REF, &disk(1)).unwrap(),
        Ensured::Provisioned { .. }
    ));
}

// ── destroy / status ─────────────────────────────────────────────────────────

#[test]
fn destroy_is_idempotent_status_flips_to_gone_and_an_unknown_id_is_an_error_not_gone() {
    let sb = sandbox(0);
    let (id, _) = provisioned(&sb.ensure(REF, &disk(10)).unwrap());
    assert_eq!(sb.status(&id).unwrap(), ResourceStatus::Active);
    crate::ask::destroy_one(&sb, &id).unwrap();
    assert_eq!(sb.status(&id).unwrap(), ResourceStatus::Gone);
    crate::ask::destroy_one(&sb, &id).expect("destroying what is already gone is fine");
    assert_eq!(sb.live_gib(), 0);
    assert_eq!(sb.resources()[0].destroyed_unix_ms, Some(1_788_000_000_000));

    // Gone gives its GiB back: the same pair can be ensured again and mints anew.
    let (id2, _) = provisioned(&sb.ensure(REF, &disk(10)).unwrap());
    assert_ne!(
        id2, id,
        "a destroyed resource is not adopted; a new one is minted"
    );

    let ghost = ResourceId {
        provider: PROVIDER.into(),
        id: "sandbox:disk:99".into(),
    };
    assert!(
        matches!(sb.status(&ghost), Err(CloudError::UnknownResource(_))),
        "never minted ⇒ an error, not Gone"
    );
    assert!(matches!(
        crate::ask::destroy_one(&sb, &ghost),
        Err(CloudError::UnknownResource(_))
    ));
    let alien = ResourceId {
        provider: "upcloud".into(),
        id: "storage/abc".into(),
    };
    assert!(
        matches!(sb.status(&alien), Err(CloudError::UnknownResource(_))),
        "another provider's id"
    );
    assert!(matches!(
        crate::ask::destroy_one(&sb, &alien),
        Err(CloudError::UnknownResource(_))
    ));
}

// ── persistence ──────────────────────────────────────────────────────────────

/// The payment sandbox's restart bug, guarded here before it can happen: the
/// process that minted the iron goes away, the next one still owns it.
#[test]
fn the_iron_survives_the_process_that_minted_it() {
    let path = tmp("restart");
    let mut c = cfg(0);
    c.state_path = Some(path.clone());
    let id = {
        let sb = Sandbox::new(c.clone()).unwrap();
        let (id, _) = provisioned(&sb.ensure(REF, &disk(10)).unwrap());
        id
        // dropped: the in-memory instance is gone
    };
    // Applied output: the bytes on disk carry the id, the order and the mark.
    let bytes = std::fs::read_to_string(&path).unwrap();
    assert!(bytes.contains("\"sandbox:disk:1\""), "{bytes}");
    assert!(bytes.contains(REF), "{bytes}");
    assert!(
        bytes.contains("\"monetize_provenance\": \"sandbox\""),
        "{bytes}"
    );

    let again = Sandbox::new(c.clone()).unwrap();
    assert_eq!(
        again.status(&id).unwrap(),
        ResourceStatus::Active,
        "a restart does not forget what it owns"
    );
    // And the idempotency survives too: the same pair is adopted, not minted again.
    assert_eq!(
        again.ensure(REF, &disk(10)).unwrap(),
        Ensured::Provisioned {
            id: id.clone(),
            cost: Price {
                minor_per_month: 230,
                currency: Currency::Eur
            }
        }
    );
    assert_eq!(again.resources().len(), 1);
    crate::ask::destroy_one(&again, &id).expect("destroy still works after a restart");

    let third = Sandbox::new(c).unwrap();
    assert_eq!(
        third.status(&id).unwrap(),
        ResourceStatus::Gone,
        "and so does the destroy"
    );

    // Twin: a sandbox at a DIFFERENT path knows nothing — the file is the memory.
    let mut elsewhere = cfg(0);
    elsewhere.state_path = Some(tmp("restart-elsewhere"));
    let stranger = Sandbox::new(elsewhere).unwrap();
    assert!(matches!(
        stranger.status(&id),
        Err(CloudError::UnknownResource(_))
    ));

    // A corrupt file is refused, not guessed over.
    std::fs::write(&path, b"{ not json").unwrap();
    let mut corrupt = cfg(0);
    corrupt.state_path = Some(path);
    match Sandbox::new(corrupt) {
        Err(CloudError::Unavailable(m)) => assert!(m.contains("does not parse"), "{m}"),
        other => panic!("{other:?}"),
    }
}

// ── refusals ─────────────────────────────────────────────────────────────────

#[test]
fn refuse_twin_a_zero_in_the_declared_table_or_no_price_source_cannot_start() {
    let mut zero = cfg(0);
    zero.declared.as_mut().unwrap().disk_minor_per_gib_month = 0;
    match Sandbox::new(zero) {
        Err(CloudError::Refused(m)) => {
            assert!(m.contains("disk at 0"), "{m}");
            assert!(m.contains("untestable"), "must say WHY: {m}");
        }
        other => panic!("{other:?}"),
    }
    let mut zero_plan = cfg(0);
    zero_plan
        .declared
        .as_mut()
        .unwrap()
        .plan_minor_per_month
        .insert("FREE".into(), 0);
    match Sandbox::new(zero_plan) {
        Err(CloudError::Refused(m)) => assert!(m.contains("plan FREE at 0"), "{m}"),
        other => panic!("{other:?}"),
    }
    let mut none = cfg(0);
    none.declared = None;
    match Sandbox::new(none) {
        Err(CloudError::Refused(m)) => {
            assert!(m.contains("no price source"), "{m}");
            assert!(
                m.contains("UPCLOUD_TOKEN") && m.contains("disk_minor_per_gib_month"),
                "must name both ways out: {m}"
            );
        }
        other => panic!("{other:?}"),
    }
    let mut blank = cfg(0);
    blank.zone = Zone(String::new());
    assert!(matches!(Sandbox::new(blank), Err(CloudError::Refused(_))));
    // Green twin: the same table with real numbers starts.
    assert!(Sandbox::new(cfg(0)).is_ok());
}

#[test]
fn refuse_twin_the_things_a_real_provider_refuses() {
    let sb = sandbox(0);
    assert!(
        matches!(sb.ensure(REF, &disk(0)), Err(CloudError::Refused(_))),
        "a disk of nothing"
    );
    assert!(
        matches!(sb.ensure("", &disk(1)), Err(CloudError::Refused(_))),
        "no reference to attach it to"
    );
    assert!(
        matches!(
            sb.ensure(&"r".repeat(256), &disk(1)),
            Err(CloudError::Refused(_))
        ),
        "a reference no label can carry"
    );
    let unpriced = Resource::Server {
        plan: "NO-SUCH-PLAN".into(),
        zone: Zone("se-sto1".into()),
        role: ServerRole::Replica,
    };
    assert!(
        matches!(sb.ensure(REF, &unpriced), Err(CloudError::Refused(_))),
        "a plan the table does not price"
    );
    assert_eq!(sb.resources().len(), 0, "nothing was minted by a refusal");
    assert_eq!(sb.live_gib(), 0);
}

// ── servers and plan changes ─────────────────────────────────────────────────

#[test]
fn a_server_is_priced_by_plan_and_a_plan_change_charges_the_delta_or_is_on_hand() {
    let sb = sandbox(0);
    let srv = Resource::Server {
        plan: "STARTER-2xCPU-4GB".into(),
        zone: Zone("se-sto1".into()),
        role: ServerRole::Replica,
    };
    let (sid, cost) = provisioned(&sb.ensure(REF, &srv).unwrap());
    assert_eq!(sid.id, "sandbox:server:1");
    assert_eq!(cost.minor_per_month, 1304);

    // The fake server already runs its plan: asking for the same plan is OnHand.
    let same = Resource::Compute {
        server: ServerId(sid.id.clone()),
        plan: "STARTER-2xCPU-4GB".into(),
    };
    assert_eq!(
        sb.ensure(REF2, &same).unwrap(),
        Ensured::OnHand { pool_free_after: 0 }
    );

    // Up a step: the delta, and only the delta.
    let up = Resource::Compute {
        server: ServerId(sid.id.clone()),
        plan: "STARTER-4xCPU-16GB".into(),
    };
    assert_eq!(
        crate::Cloud::price(&sb, &crate::cloud::Priced::Resource(&up))
            .unwrap()
            .minor_per_month,
        3042 - 1304
    );
    let (cid, cost) = provisioned(&sb.ensure("gunnar/alice/2026-10", &up).unwrap());
    assert_eq!(cid.id, "sandbox:compute:2");
    assert_eq!(cost.minor_per_month, 3042 - 1304);
    let change = sb.resources().into_iter().find(|r| r.id == cid.id).unwrap();
    assert_eq!(
        change.from_plan.as_deref(),
        Some("STARTER-2xCPU-4GB"),
        "what destroy puts back"
    );

    // Destroying the change puts the plan back, so the same change is chargeable again.
    crate::ask::destroy_one(&sb, &cid).unwrap();
    assert_eq!(
        crate::Cloud::price(&sb, &crate::cloud::Priced::Resource(&up))
            .unwrap()
            .minor_per_month,
        3042 - 1304
    );

    // A server the sandbox was never told about is charged the WHOLE plan — the
    // conservative direction for a cap.
    let foreign = Resource::Compute {
        server: ServerId("some-real-box".into()),
        plan: "STARTER-2xCPU-4GB".into(),
    };
    assert_eq!(
        crate::Cloud::price(&sb, &crate::cloud::Priced::Resource(&foreign))
            .unwrap()
            .minor_per_month,
        1304
    );
}

// ── behind the cap ───────────────────────────────────────────────────────────

/// Rule 4: it sits behind `SpendCap` like any provider. The cap refuses BEFORE the
/// sandbox is called — asserted on the sandbox's own call log, not on the error.
#[test]
fn the_spend_cap_refuses_before_the_sandbox_is_called() {
    let capped =
        SpendCap::new(sandbox(0), Ceiling::total(1000, Currency::Eur), tmp("cap")).unwrap();
    // 50 GiB × 23 = 1150 > 1000.
    let err = crate::ask::ensure(&capped, REF, &disk(50), None).unwrap_err();
    // The cap's own refusal (`OverCap`, said as a refusal); this test first compiled in the
    // workspace run when `sandbox` entered the default features (monetize v2.0 P1).
    assert!(
        matches!(err, CloudError::Refused(_) | CloudError::OverCap(_)),
        "{err}"
    );
    assert!(
        err.to_string().contains("11.50 EUR"),
        "names the price, in EUR with two decimals: {err}"
    );
    assert_eq!(capped.spent_total(), 0, "nothing spent");

    // The cap owns its inner, so to read the sandbox's call log the sandbox is
    // held behind an Arc (a shared provider is a provider).
    let sb = std::sync::Arc::new(sandbox(0));
    let capped =
        SpendCap::new(sb.clone(), Ceiling::total(1000, Currency::Eur), tmp("cap2")).unwrap();
    assert!(crate::ask::ensure(&capped, REF, &disk(50), None).is_err());
    assert!(
        sb.calls().is_empty(),
        "the sandbox must not have been called: {:?}",
        sb.calls()
    );
    assert!(sb.resources().is_empty());

    // Twin: under the ceiling it IS called, and the total rises by the real cost.
    let out = crate::ask::ensure(&capped, REF, &disk(40), None).unwrap();
    assert_eq!(provisioned(&out).1.minor_per_month, 920);
    assert_eq!(
        sb.calls(),
        vec![Call::Ensure {
            reference: REF.into(),
            fingerprint: "disk:gunnar-se-sto1:40".into()
        }]
    );
    assert_eq!(capped.spent_total(), 920);
    assert_eq!(capped.ceiling(), Ceiling::total(1000, Currency::Eur));
}

// ── config from the environment ──────────────────────────────────────────────

#[test]
fn from_env_reads_the_state_path_and_the_credential_decides_the_price_source() {
    let none = SandboxConfig::from_env_with(|_| None);
    assert!(none.state_path.is_none());
    let with = SandboxConfig::from_env_with(|k| match k {
        "MONETIZE_SANDBOX_CLOUD_STATE" => Some("/var/lib/monetize/sandbox-cloud.json".into()),
        _ => None,
    });
    assert_eq!(
        with.state_path.as_deref(),
        Some(Path::new("/var/lib/monetize/sandbox-cloud.json"))
    );
    let blank =
        SandboxConfig::from_env_with(|k| (k == "MONETIZE_SANDBOX_CLOUD_STATE").then(String::new));
    assert!(blank.state_path.is_none(), "an empty path is no path");
    assert_eq!(CONFIG_ENVS, &["MONETIZE_SANDBOX_CLOUD_STATE"]);
}

// ── inventory ────────────────────────────────────────────────────────────────

/// `inventory()` answers from the persisted state: there after `ensure`, gone
/// after `destroy`, and still there after a restart in between.
#[test]
fn inventory_shows_the_iron_after_ensure_and_not_after_destroy() {
    let path = tmp("inventory");
    let mut c = cfg(0);
    c.state_path = Some(path);
    let sb = Sandbox::new(c.clone()).unwrap();
    assert_eq!(
        crate::ask::account(&sb).unwrap(),
        Inventory {
            owned: vec![],
            foreign: 0,
            currency: Currency::Eur
        },
        "nothing owned yet, nothing foreign ever — and still a currency"
    );
    let (id, cost) = provisioned(&sb.ensure(REF, &disk(10)).unwrap());
    let srv = Resource::Server {
        plan: "STARTER-2xCPU-4GB".into(),
        zone: Zone("se-sto1".into()),
        role: ServerRole::Replica,
    };
    let (sid, _) = provisioned(&sb.ensure(REF2, &srv).unwrap());
    // A plan change is not a thing that exists at a provider: not listed.
    sb.ensure(
        "gunnar/alice/2026-10",
        &Resource::Compute {
            server: ServerId(sid.id.clone()),
            plan: "STARTER-4xCPU-16GB".into(),
        },
    )
    .unwrap();
    // And an OnHand grant owns nothing.
    let pooled = Sandbox::new(cfg(100)).unwrap();
    pooled.ensure(REF, &disk(10)).unwrap();
    assert!(
        crate::ask::account(&pooled).unwrap().owned.is_empty(),
        "OnHand bought nothing to own"
    );

    let inv = crate::ask::account(&Sandbox::new(c.clone()).unwrap()).unwrap();
    assert_eq!(inv.foreign, 0);
    assert_eq!(
        inv.currency,
        Currency::Eur,
        "the declared table's currency, on the list"
    );
    assert_eq!(
        inv.owned,
        vec![
            Owned {
                id: id.clone(),
                kind: OwnedKind::Disk,
                gib: 10,
                zone: "se-sto1".into(),
                attached_to: Some(SRV.into()),
                reference: Some(REF.into()),
                monthly_minor: Some(cost.minor_per_month),
                currency: cost.currency,
                provenance: "sandbox".into(),
                resource: None,
                backup: None,
            },
            Owned {
                id: sid.clone(),
                kind: OwnedKind::Server,
                gib: 0,
                zone: "se-sto1".into(),
                attached_to: None,
                reference: Some(REF2.into()),
                monthly_minor: Some(1304),
                currency: Currency::Eur,
                provenance: "sandbox".into(),
                resource: None,
                backup: None,
            },
        ]
    );
    crate::ask::destroy_one(&sb, &id).unwrap();
    let after = crate::ask::account(&Sandbox::new(c).unwrap()).unwrap();
    assert_eq!(
        after.owned.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
        vec![sid],
        "the destroyed disk is no longer owned"
    );
}

/// **The rehearsal follows the product's room, like UpCloud.** `Room::OnHand`
/// mints nothing even with an empty declared pool; `Room::Buy` mints even with
/// a declared pool that would have covered it — the pool has never seen the
/// product's disk.
#[test]
fn the_room_decides_and_the_declared_pool_steps_aside() {
    let sb = sandbox(0);
    assert_eq!(
        sb.ensure_with(REF, &disk(10), Room::OnHand).unwrap(),
        Ensured::OnHand { pool_free_after: 0 }
    );
    assert!(
        crate::ask::account(&sb).unwrap().owned.is_empty(),
        "nothing minted for a product that had the room"
    );

    let roomy = sandbox(500);
    let (id, _) = provisioned(&roomy.ensure_with(REF, &disk(10), Room::Buy).unwrap());
    assert_eq!(id.provider, "sandbox");
    // Twin: the same roomy sandbox with no room given answers from its pool.
    assert!(matches!(
        roomy.ensure(REF2, &disk(10)).unwrap(),
        Ensured::OnHand { .. }
    ));
}

// ── the estate lay (monetize v2.0 P1, the money-free open default) ───────────

/// ★ **An estate is laid with play iron, each machine its own server** — two machines of one
/// plan are two servers (minted under the lay's own key `lay:<role>:server`, not the resource's
/// fingerprint); a second lay adopts every one; the plan list is the declared table's.
#[test]
fn an_estate_is_laid_one_server_per_machine_and_a_second_lay_adopts_it() {
    use crate::lay::{DiskPlan, EstatePlan, MachinePlan};
    let sb = sandbox(0);
    let catalog = sb.plan_catalog(&Zone("se-sto1".into())).unwrap();
    assert!(
        catalog
            .iter()
            .any(|p| p.name == "STARTER-2xCPU-4GB" && p.cores == 2 && p.memory_mib == 4096),
        "{catalog:?}"
    );
    let machine = |role: &str| MachinePlan {
        role: role.into(),
        hostname: format!("lake-{role}-acme"),
        title: format!("Lake {role} acme"),
        plan: "STARTER-2xCPU-4GB".into(),
        boot_order: "disk,cdrom".into(),
        labels: Default::default(),
        interfaces: crate::lay::default_interfaces(),
        medium: String::new(),
        medium_object: None,
        template: String::new(),
        user_data: Default::default(),
        disks: vec![DiskPlan {
            title: format!("lake-{role}-acme system"),
            gib: 20,
            tier: "standard".into(),
            labels: Default::default(),
        }],
    };
    let plan = EstatePlan {
        reference: crate::lay::reference_of("acme"),
        company: "acme".into(),
        zone: Zone("se-sto1".into()),
        machines: vec![machine("front"), machine("be"), machine("fe")],
        narrate_from: String::new(),
        shape: None,
    };
    let at = tmp("lay");
    std::fs::create_dir_all(at.parent().unwrap()).unwrap();
    let journal = crate::grow::Journal::open(at.with_file_name("journal.jsonl")).unwrap();
    let laid = sb.lay_estate(&plan, &journal).unwrap();
    let servers: std::collections::BTreeSet<&str> =
        laid.machines.iter().map(|m| m.server.as_str()).collect();
    assert_eq!(
        servers.len(),
        3,
        "three machines of one plan are three servers: {laid:?}"
    );
    let again = sb.lay_estate(&plan, &journal).unwrap();
    assert_eq!(
        again
            .machines
            .iter()
            .map(|m| m.server.clone())
            .collect::<Vec<_>>(),
        laid.machines
            .iter()
            .map(|m| m.server.clone())
            .collect::<Vec<_>>(),
        "a second lay adopts"
    );
    let inv = crate::ask::account(&sb).unwrap();
    assert_eq!(
        inv.owned
            .iter()
            .filter(|o| o.kind == OwnedKind::Server)
            .count(),
        3
    );
    assert_eq!(
        inv.owned
            .iter()
            .filter(|o| o.kind == OwnedKind::Disk)
            .count(),
        3
    );
}

/// R5a: the sandbox is the open default's cloud, served by the ONE box itself.
/// A panic that once held its iron or its call log must not make every later
/// order panic: each mint is written before it answers.
#[test]
fn a_poisoned_sandbox_still_mints_and_answers() {
    let sb = sandbox(0);
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let _held = sb.state.lock();
        panic!("a panic while the iron is held");
    }));
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let _held = sb.calls.lock();
        panic!("a panic while the call log is held");
    }));
    assert!(sb.state.is_poisoned() && sb.calls.is_poisoned());
    let out = sb
        .ensure(REF, &disk(110))
        .expect("a purchase after the panic");
    let (id, _) = provisioned(&out);
    assert_eq!(sb.live_gib(), 110);
    assert_eq!(sb.resources().len(), 1);
    assert!(!sb.calls().is_empty());
    assert!(crate::ask::destroy_one(&sb, &id).is_ok());
}