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

//! Red-first for the spend cap. The assertions that matter are the ones about
//! money NOT moving: a cap is only worth having if it refuses, and only safe if
//! it refuses for a reason an operator can act on.

use super::*;
use crate::{ResourceId, ResourceStatus};
use monetize_pay::Currency;
use std::sync::atomic::{AtomicU64, Ordering};

/// A provider written here rather than reused: `monetize::testing::MockCloud`
/// lives in a crate that DEPENDS on this one, so using it would be a cycle. It is
/// also the wrong tool — this needs to count calls and be told to answer OnHand,
/// which is about the CAP's behaviour, not a provider's.
#[derive(Debug, Default)]
struct FakeCloud {
    /// Price per GiB, minor units per month.
    per_gib: u64,
    on_hand: bool,
    ensure_calls: Mutex<Vec<String>>,
    destroy_calls: AtomicU64,
    zones: Vec<Zone>,
    /// `change_plan` and `set_power` as they reached the provider.
    plan_calls: Mutex<Vec<String>>,
    /// It lays an estate (three toy servers) instead of refusing to.
    lays: bool,
}

impl FakeCloud {
    fn new() -> Self {
        FakeCloud {
            per_gib: 100,
            on_hand: false,
            zones: vec![Zone("se-sto1".into())],
            ..Default::default()
        }
    }
    fn always_on_hand(mut self) -> Self {
        self.on_hand = true;
        self
    }
    fn laying(mut self) -> Self {
        self.lays = true;
        self
    }
    fn ensure_calls_for(&self, reference: &str) -> usize {
        self.ensure_calls
            .lock()
            .unwrap()
            .iter()
            .filter(|r| *r == reference)
            .count()
    }
    fn destroy_calls(&self) -> u64 {
        self.destroy_calls.load(Ordering::SeqCst)
    }
}

#[allow(dead_code)]
impl FakeCloud {
    pub fn provider(&self) -> &'static str {
        "fake"
    }
    pub fn zones(&self) -> &[Zone] {
        &self.zones
    }
    pub fn price_resource(&self, resource: &Resource) -> Result<Price, CloudError> {
        let gib = match resource {
            Resource::Disk { gib, .. } => *gib,
            _ => 1,
        };
        Ok(Price {
            minor_per_month: self.per_gib * gib,
            currency: Currency::Sek,
        })
    }
    pub fn ensure(&self, reference: &str, resource: &Resource) -> Result<Ensured, CloudError> {
        self.ensure_calls.lock().unwrap().push(reference.to_owned());
        if self.on_hand {
            return Ok(Ensured::OnHand {
                pool_free_after: 999,
            });
        }
        Ok(Ensured::Provisioned {
            id: ResourceId {
                provider: "fake".into(),
                id: format!("storage/{reference}"),
            },
            cost: self.price_resource(resource)?,
        })
    }
    pub fn destroy_id(&self, _id: &ResourceId) -> Result<(), CloudError> {
        self.destroy_calls.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }
    pub fn status(&self, _id: &ResourceId) -> Result<ResourceStatus, CloudError> {
        Ok(ResourceStatus::Active)
    }
    /// One owned disk per reference ever ensured — a list only the inner can
    /// produce, which is what the passthrough test reads back through the cap.
    pub fn list_owned(&self) -> Result<crate::Inventory, CloudError> {
        let owned = self
            .ensure_calls
            .lock()
            .unwrap()
            .iter()
            .map(|r| crate::Owned {
                id: ResourceId {
                    provider: "fake".into(),
                    id: format!("storage/{r}"),
                },
                kind: crate::OwnedKind::Disk,
                gib: 1,
                zone: self.zones[0].0.clone(),
                attached_to: None,
                reference: Some(r.clone()),
                monthly_minor: Some(self.per_gib),
                currency: Currency::Sek,
                provenance: "fake".into(),
                resource: None,
                backup: None,
            })
            .collect();
        Ok(crate::Inventory {
            owned,
            foreign: 7,
            currency: Currency::Sek,
        })
    }
    pub fn set_power(&self, server: &ResourceId, on: bool) -> Result<(), CloudError> {
        self.plan_calls
            .lock()
            .unwrap()
            .push(format!("power {} {on}", server.id));
        Ok(())
    }
    pub fn change_plan(
        &self,
        server: &ResourceId,
        plan: &str,
        _delta: Price,
        _journal: &crate::grow::Journal,
    ) -> Result<(), CloudError> {
        self.plan_calls
            .lock()
            .unwrap()
            .push(format!("plan {} {plan}", server.id));
        Ok(())
    }
    pub fn lay_estate(
        &self,
        plan: &crate::lay::EstatePlan,
        _journal: &crate::grow::Journal,
    ) -> Result<crate::lay::LaidEstate, CloudError> {
        if !self.lays {
            return Err(crate::lay::unsupported(self.provider()));
        }
        let machines = plan
            .machines
            .iter()
            .map(|m| crate::lay::LaidMachine {
                role: m.role.clone(),
                hostname: m.hostname.clone(),
                title: m.title.clone(),
                plan: m.plan.clone(),
                server: format!("{}:{}", plan.reference, m.role),
                public_ipv4: "192.0.2.1".into(),
                utility_ipv4: "10.0.0.1".into(),
                disks: vec![],
                medium_clone: String::new(),
            })
            .collect();
        Ok(crate::lay::LaidEstate {
            reference: plan.reference.clone(),
            company: plan.company.clone(),
            zone: plan.zone.clone(),
            machines,
            narrate_from: plan.narrate_from.clone(),
            shape: plan.shape.clone(),
        })
    }
    pub fn release_for(&self, _references: &[String]) -> Result<crate::Reclaimed, CloudError> {
        Ok(crate::Reclaimed::default())
    }
}
#[allow(unused_variables, unreachable_code)]
impl crate::cloud::Cloud for FakeCloud {
    fn manifest(&self) -> crate::cloud::CloudManifest {
        crate::cloud::CloudManifest {
            provider: self.provider().to_string(),
            zones: self.zones().to_vec(),
            settings: Vec::new(),
            grow_refused: Some(crate::grow::unsupported(self.provider()).to_string()),
        }
    }
    fn catalog(&self, zone: &crate::Zone) -> Result<crate::cloud::Catalog, crate::CloudError> {
        Err(crate::lay::unsupported(self.provider()))
    }
    fn price(&self, what: &crate::cloud::Priced<'_>) -> Result<crate::Price, crate::CloudError> {
        match what {
            crate::cloud::Priced::Resource(r) => self.price_resource(r),
            crate::cloud::Priced::Plan(p) => {
                crate::price_estate_by_rows(p, |r| self.price_resource(r), None)
            }
            crate::cloud::Priced::Grow(set) => Err(crate::grow::unsupported(self.provider())),
        }
    }
    fn lay(
        &self,
        plan: &crate::lay::EstatePlan,
        journal: &crate::grow::Journal,
    ) -> Result<crate::lay::LaidEstate, crate::CloudError> {
        self.lay_estate(plan, journal)
    }
    fn change(
        &self,
        change: &crate::cloud::Change,
        journal: &crate::grow::Journal,
    ) -> Result<crate::cloud::Changed, crate::CloudError> {
        Ok(match change {
            crate::cloud::Change::Plan {
                server,
                plan,
                delta,
            } => {
                self.change_plan(server, plan, *delta, journal)?;
                crate::cloud::Changed::Done
            }
            crate::cloud::Change::Grow { set, estate } => {
                return Err(crate::grow::unsupported(self.provider()))
            }
            crate::cloud::Change::Settle { set } => {
                return Err(crate::grow::unsupported(self.provider()))
            }
            crate::cloud::Change::ForgetMedia { laid } => {
                crate::cloud::Changed::Forgotten(Vec::new())
            }
            crate::cloud::Change::Ensure {
                reference,
                resource,
                room,
            } => crate::cloud::Changed::Ensured(self.ensure(reference, resource)?),
            crate::cloud::Change::Retire { reference, machine } => crate::cloud::Changed::Retired(
                crate::cloud::retire_by_rows(self, reference, machine)?,
            ),
            crate::cloud::Change::LabelAdopted { .. } => {
                return Err(crate::lay::unsupported_verb(
                    self.provider(),
                    "label_adopted",
                ))
            }
        })
    }
    fn boot(
        &self,
        boot: &crate::cloud::Boot,
        journal: &crate::grow::Journal,
    ) -> Result<crate::cloud::Booted, crate::CloudError> {
        Ok(match boot {
            crate::cloud::Boot::Keep(k) => {
                return Err(crate::lay::unsupported_verb(self.provider(), "keep_boot"))
            }
            crate::cloud::Boot::Power { server, on } => {
                self.set_power(server, *on)?;
                crate::cloud::Booted::Powered
            }
            crate::cloud::Boot::Restore(r) => {
                return Err(crate::lay::unsupported_verb(
                    self.provider(),
                    "restore_from_backup",
                ))
            }
        })
    }
    fn firewall(&self, laid: &crate::lay::LaidEstate) -> Result<(), crate::CloudError> {
        Err(crate::lay::unsupported_verb(
            self.provider(),
            "set_estate_firewall",
        ))
    }
    fn destroy(
        &self,
        what: &crate::cloud::Give<'_>,
    ) -> Result<crate::Reclaimed, crate::CloudError> {
        match what {
            crate::cloud::Give::References(refs) => self.release_for(refs),
            crate::cloud::Give::Resource(id) => {
                self.destroy_id(id)?;
                Ok(crate::Reclaimed {
                    destroyed: vec![(*id).clone()],
                    ..crate::Reclaimed::default()
                })
            }
        }
    }
    fn inventory(
        &self,
        what: &crate::cloud::Listing<'_>,
    ) -> Result<crate::cloud::Listed, crate::CloudError> {
        Ok(match what {
            crate::cloud::Listing::Account => crate::cloud::Listed::Account(self.list_owned()?),
            crate::cloud::Listing::Set(set) => {
                return Err(crate::grow::unsupported(self.provider()))
            }
            crate::cloud::Listing::Status(id) => crate::cloud::Listed::Status(self.status(id)?),
            crate::cloud::Listing::Under(refs) => {
                crate::cloud::Listed::Under(crate::cloud::under(self.list_owned()?, refs))
            }
            crate::cloud::Listing::Room { delta, room } => {
                crate::cloud::Listed::Room(crate::cloud::RoomLeft {
                    serve: Ok(()),
                    left_bytes: None,
                    ceilings: Default::default(),
                })
            }
            crate::cloud::Listing::Admits(_plan) => crate::cloud::Listed::Admitted,
        })
    }
}

/// A MONTHLY ceiling in SEK — what most of these tests exercise.
fn sek(minor: u64) -> Ceiling {
    Ceiling::monthly(minor, Currency::Sek)
}

/// A TOTAL (lifetime) ceiling in SEK.
fn total(minor: u64) -> Ceiling {
    Ceiling::total(minor, Currency::Sek)
}

fn disk(gib: u64) -> Resource {
    Resource::Disk {
        server: crate::ServerId("gunnar-se-sto1".into()),
        gib,
    }
}

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

/// `MockCloud` prices every resource; this is what it charges per GiB so the
/// arithmetic below is not guesswork.
fn price_of(c: &FakeCloud, r: &Resource) -> u64 {
    crate::Cloud::price(&c, &crate::cloud::Priced::Resource(r))
        .unwrap()
        .minor_per_month
}

#[test]
fn a_purchase_inside_the_ceiling_goes_through_and_is_counted() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    let cap = SpendCap::new(inner, sek(one * 2), tmp("under"))
        .unwrap()
        .with_month(|| "2026-09".into());

    assert_eq!(cap.spent_this_month(), 0);
    assert_eq!(cap.remaining(), one * 2);

    let first = crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    assert!(matches!(first, Ensured::Provisioned { .. }), "{first:?}");
    assert_eq!(cap.spent_this_month(), one, "a purchase is committed");
    assert_eq!(cap.remaining(), one);
}

#[test]
fn refuse_twin_the_purchase_that_would_cross_the_ceiling_never_reaches_the_provider() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    // Room for exactly one.
    let cap = SpendCap::new(inner, sek(one), tmp("cross"))
        .unwrap()
        .with_month(|| "2026-09".into());

    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    let before = cap.spent_this_month();

    let err = crate::ask::ensure(&cap, "gunnar/b/2027", &disk(10), None)
        .expect_err("the second must be refused");
    let msg = err.to_string();
    // The message must carry all three numbers and a way out, or an operator
    // cannot act on it.
    assert!(
        msg.contains(
            &monetize_pay::Money {
                minor: one,
                currency: Currency::Sek
            }
            .in_words()
        ),
        "the price, in words: {msg}"
    );
    assert!(msg.contains("the cap is"), "{msg}");
    assert!(
        msg.contains("OnHand") || msg.contains("destroy"),
        "a way out: {msg}"
    );
    // And nothing moved.
    assert_eq!(
        cap.spent_this_month(),
        before,
        "a refusal must not commit spend"
    );
    assert_eq!(
        cap.inner.ensure_calls_for("gunnar/b/2027"),
        0,
        "the provider was never asked"
    );
}

#[test]
fn on_hand_costs_nothing_so_it_must_not_consume_the_ceiling() {
    // A provider that always answers OnHand: nothing is ever bought.
    let inner = FakeCloud::new().always_on_hand();
    let one = price_of(&inner, &disk(10));
    let cap = SpendCap::new(inner, sek(one * 3), tmp("onhand"))
        .unwrap()
        .with_month(|| "2026-09".into());

    for i in 0..3 {
        let out = crate::ask::ensure(&cap, &format!("gunnar/{i}/2027"), &disk(10), None).unwrap();
        assert!(matches!(out, Ensured::OnHand { .. }), "{out:?}");
    }
    assert_eq!(
        cap.spent_this_month(),
        0,
        "OnHand bought nothing, so the month owes nothing"
    );
    assert_eq!(cap.remaining(), one * 3);
}

#[test]
fn refuse_twin_a_zero_ceiling_refuses_every_purchase_and_is_not_read_as_unlimited() {
    let inner = FakeCloud::new();
    let cap = SpendCap::new(inner, sek(0), tmp("zero"))
        .unwrap()
        .with_month(|| "2026-09".into());
    let err =
        crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).expect_err("zero means zero");
    assert!(err.to_string().contains("the cap is 0.00 SEK"), "{err}");
    assert_eq!(cap.spent_this_month(), 0);
    // The failure mode this guards: a misconfigured cap that opens itself.
    assert_eq!(cap.remaining(), 0);
}

#[test]
fn refuse_twin_a_price_in_another_currency_is_refused_not_converted() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    // MockCloud prices in Sek; set the ceiling in Eur.
    let cap = SpendCap::new(inner, Ceiling::monthly(one * 10, Currency::Eur), tmp("fx"))
        .unwrap()
        .with_month(|| "2026-09".into());
    let err = crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None)
        .expect_err("no FX inside a spending guard");
    assert!(err.to_string().contains("conversion"), "{err}");
    assert_eq!(cap.spent_this_month(), 0);
}

#[test]
fn the_total_survives_a_restart_and_resets_on_a_new_month() {
    let path = tmp("months");
    let one = price_of(&FakeCloud::new(), &disk(10));

    // September buys one.
    {
        let cap = SpendCap::new(FakeCloud::new(), sek(one), path.clone())
            .unwrap()
            .with_month(|| "2026-09".into());
        crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
        assert_eq!(cap.spent_this_month(), one);
    }
    // A NEW PROCESS in the same month still sees the commitment — the whole
    // reason the total is on disk rather than in memory.
    {
        let cap = SpendCap::new(FakeCloud::new(), sek(one), path.clone())
            .unwrap()
            .with_month(|| "2026-09".into());
        assert_eq!(
            cap.spent_this_month(),
            one,
            "a restart must not clear the month"
        );
        assert!(
            crate::ask::ensure(&cap, "gunnar/b/2027", &disk(10), None).is_err(),
            "still at the ceiling after a restart"
        );
    }
    // October starts from zero.
    {
        let cap = SpendCap::new(FakeCloud::new(), sek(one), path.clone())
            .unwrap()
            .with_month(|| "2026-10".into());
        assert_eq!(cap.spent_this_month(), 0, "a new month starts fresh");
        assert!(crate::ask::ensure(&cap, "gunnar/c/2027", &disk(10), None).is_ok());
    }
}

#[test]
fn destroy_is_never_capped_because_it_is_the_call_that_lowers_the_bill() {
    let inner = FakeCloud::new();
    let cap = SpendCap::new(inner, sek(0), tmp("destroy"))
        .unwrap()
        .with_month(|| "2026-09".into());
    // At a zero ceiling — the most locked-down state there is — giving something
    // back must still work, or a deployment is trapped with no way down.
    let id = ResourceId {
        provider: "mockcloud".into(),
        id: "storage/1".into(),
    };
    let _ = crate::ask::destroy_one(&cap, &id);
    assert!(
        cap.inner.destroy_calls() >= 1,
        "destroy must reach the provider even at a zero ceiling"
    );
}

/// **The rehearsal's actual cost story, in code.**
///
/// The numbers are MEASURED off `GET /1.3/price` (zone se-sto1, 2026-09-04), not
/// chosen: one UpCloud credit is one euro cent, monetize converts at 730 h/month,
/// so `storage_standard` at 0.0118 credits/h/GiB is 8.6 -> **9 cents per GiB per
/// month**. The rehearsal catalogue buys exactly 1 GiB per plan.
///
/// Against a EUR 10.00 ceiling that is 111 provisions in a month, and the 112th
/// is refused. If that arithmetic ever changes — a price move, a bigger disk in
/// the plans file, a tier switch to maxiops — this test says so instead of an
/// invoice saying it.
#[test]
fn a_ten_euro_ceiling_buys_111_one_gib_disks_and_refuses_the_112th() {
    const CENTS_PER_GIB_MONTH: u64 = 9; // storage_standard, se-sto1, measured
    const CEILING_CENTS: u64 = 1000; // EUR 10.00

    let mut inner = FakeCloud::new();
    inner.per_gib = CENTS_PER_GIB_MONTH;
    let cap = SpendCap::new(
        inner,
        Ceiling::monthly(CEILING_CENTS, Currency::Sek),
        tmp("tenner"),
    )
    .unwrap()
    .with_month(|| "2026-09".into());

    let affordable = CEILING_CENTS / CENTS_PER_GIB_MONTH;
    assert_eq!(
        affordable, 111,
        "if this changes, the rehearsal's cost story changed"
    );

    for i in 0..affordable {
        crate::ask::ensure(&cap, &format!("gunnar/t{i}/2026-09-04"), &disk(1), None)
            .unwrap_or_else(|e| panic!("provision {i} of {affordable} must fit: {e}"));
    }
    assert_eq!(cap.spent_this_month(), affordable * CENTS_PER_GIB_MONTH);
    assert!(
        cap.remaining() < CENTS_PER_GIB_MONTH,
        "less than one more disk of room"
    );

    let err = crate::ask::ensure(&cap, "gunnar/one-too-many/2026-09-04", &disk(1), None)
        .expect_err("the 112th must be refused");
    assert!(err.to_string().contains("the cap is"), "{err}");

    // AND the shape that matters most: one resource far over the ceiling is
    // refused outright, whatever the month has already spent. A 2xCPU-4GB box is
    // EUR 30.96/month at the same measured prices — 3096 cents against 1000.
    let fresh = SpendCap::new(
        FakeCloud::new(),
        Ceiling::monthly(CEILING_CENTS, Currency::Sek),
        tmp("big"),
    )
    .unwrap()
    .with_month(|| "2026-09".into());
    let a_whole_box = Resource::Disk {
        server: crate::ServerId("x".into()),
        gib: 3096 / 100 * 4,
    };
    assert!(
        crate::ask::ensure(&fresh, "gunnar/t/2026-09-04", &a_whole_box, None).is_err(),
        "a resource over the ceiling can never be bought, even first"
    );
    assert_eq!(fresh.spent_this_month(), 0);
}

#[test]
fn the_month_boundary_is_computed_by_integer_arithmetic_not_guessed() {
    // 2026-09-04T00:00:00Z and the instants either side of a month boundary.
    assert_eq!(year_month(1_788_480_000), (2026, 9));
    // 2026-01-01T00:00:00Z
    assert_eq!(year_month(1_767_225_600), (2026, 1));
    // 2025-12-31T23:59:59Z — the year rollover, the one this kind of code fails on.
    assert_eq!(year_month(1_767_225_599), (2025, 12));
    // The epoch itself.
    assert_eq!(year_month(0), (1970, 1));
    // A leap day: 2024-02-29T12:00:00Z
    assert_eq!(year_month(1_709_208_000), (2024, 2));
    // And the formatted form zero-pads, or a string compare sorts wrongly.
    assert_eq!(format!("{:04}-{:02}", 2026, 9), "2026-09");
}

/// **A TOTAL ceiling does not reset**, which is the whole difference from a
/// monthly one and the reason a rehearsal wants it.
///
/// A LIFETIME EUR 10 is what "cap it at ten euros" means about a test budget: a
/// monthly EUR 10 permits EUR 10 again in October and EUR 120 over a year.
#[test]
fn a_total_ceiling_survives_the_turn_of_the_month() {
    let path = tmp("lifetime");
    let one = price_of(&FakeCloud::new(), &disk(10));
    let month = std::sync::Arc::new(std::sync::Mutex::new("2026-09".to_string()));

    let m = month.clone();
    let cap = SpendCap::new(FakeCloud::new(), total(one * 2), path.clone())
        .unwrap()
        .with_month(move || m.lock().unwrap().clone());

    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    crate::ask::ensure(&cap, "gunnar/b/2027", &disk(10), None).unwrap();
    assert_eq!(cap.spent_total(), one * 2);
    assert!(
        crate::ask::ensure(&cap, "gunnar/c/2027", &disk(10), None).is_err(),
        "the total is reached"
    );

    // THE TURN OF THE MONTH. A monthly ceiling would open here; a total must not.
    *month.lock().unwrap() = "2026-10".to_string();
    assert_eq!(cap.spent_this_month(), 0, "the monthly WINDOW did reset");
    assert_eq!(cap.spent_total(), one * 2, "but the lifetime total did not");
    let err = crate::ask::ensure(&cap, "gunnar/c/2027", &disk(10), None)
        .expect_err("a new month must not reopen a total ceiling");
    assert!(
        err.to_string().contains("in total"),
        "the message must name which limit: {err}"
    );
}

/// The same property, but proven THROUGH a purchase that crosses the boundary.
///
/// The test above was blind on its own and a deliberate red found it: the
/// month-rollover reset happens when a purchase is RECORDED, which is after the
/// pre-check, so a cap that wrongly zeroed the lifetime total on rollover still
/// refused the next call and looked correct. Only a purchase that SUCCEEDS in
/// the new month exposes it.
#[test]
fn a_purchase_in_a_new_month_adds_to_the_lifetime_total_it_does_not_restart_it() {
    let one = price_of(&FakeCloud::new(), &disk(10));
    let month = std::sync::Arc::new(std::sync::Mutex::new("2026-09".to_string()));
    let m = month.clone();
    // Room for three in total, so the October purchase is affordable and the
    // fourth is not — which is only true if the total carried over.
    let cap = SpendCap::new(FakeCloud::new(), total(one * 3), tmp("carryover"))
        .unwrap()
        .with_month(move || m.lock().unwrap().clone());

    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    crate::ask::ensure(&cap, "gunnar/b/2027", &disk(10), None).unwrap();

    *month.lock().unwrap() = "2026-10".to_string();
    crate::ask::ensure(&cap, "gunnar/c/2027", &disk(10), None)
        .expect("the third fits within the lifetime total");
    assert_eq!(
        cap.spent_this_month(),
        one,
        "October's window holds only October's purchase"
    );
    assert_eq!(
        cap.spent_total(),
        one * 3,
        "the lifetime total carried September over"
    );

    let err = crate::ask::ensure(&cap, "gunnar/d/2027", &disk(10), None)
        .expect_err("the lifetime total is now spent");
    assert!(err.to_string().contains("in total"), "{err}");
}

#[test]
fn both_limits_hold_at_once_and_the_tighter_one_speaks() {
    let one = price_of(&FakeCloud::new(), &disk(10));
    let cap = SpendCap::new(
        FakeCloud::new(),
        Ceiling {
            monthly_minor: Some(one),
            total_minor: Some(one * 5),
            currency: Currency::Sek,
        },
        tmp("both"),
    )
    .unwrap()
    .with_month(|| "2026-09".into());

    assert_eq!(
        cap.remaining(),
        one,
        "the MONTHLY limit is tighter, so it is what remains"
    );
    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    let err =
        crate::ask::ensure(&cap, "gunnar/b/2027", &disk(10), None).expect_err("the month is full");
    assert!(err.to_string().contains("this month"), "{err}");
    assert_eq!(cap.spent_total(), one, "and the total counted it too");
}

/// **A ceiling that limits nothing cannot be constructed.** A guard that never
/// refuses is worse than no guard, because it reads like protection.
#[test]
fn refuse_twin_a_ceiling_with_no_limits_is_refused_at_construction() {
    let none = Ceiling {
        monthly_minor: None,
        total_minor: None,
        currency: Currency::Sek,
    };
    assert!(none.check().is_err());
    let err = SpendCap::new(FakeCloud::new(), none, tmp("nolimit")).expect_err("must refuse");
    assert!(err.to_string().contains("limits nothing"), "{err}");
    // And zero still means zero, not unlimited, in EITHER slot.
    assert!(SpendCap::new(FakeCloud::new(), total(0), tmp("zerototal")).is_ok());
    let z = SpendCap::new(FakeCloud::new(), total(0), tmp("zerototal2"))
        .unwrap()
        .with_month(|| "2026-09".into());
    assert!(
        crate::ask::ensure(&z, "gunnar/a/2027", &disk(1), None).is_err(),
        "a zero total refuses every purchase"
    );
}

/// **The stored file is a CACHE; the ledger is the truth.** This is the answer to
/// "how do we persist that and make sure nothing is lost": the total ever
/// committed is recomputable from the caller's append-only ledger, so a lost or
/// stale cache cannot quietly raise the budget.
#[test]
fn a_lost_cache_is_corrected_from_the_authoritative_total_and_only_upward() {
    let path = tmp("reconcile");
    let one = price_of(&FakeCloud::new(), &disk(10));
    let cap = SpendCap::new(FakeCloud::new(), total(one * 3), path.clone())
        .unwrap()
        .with_month(|| "2026-09".into());
    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    assert_eq!(cap.spent_total(), one);

    // The disk dies and the cache comes back EMPTY — the state this guards.
    std::fs::remove_file(&path).unwrap();
    let fresh = SpendCap::new(FakeCloud::new(), total(one * 3), path.clone())
        .unwrap()
        .with_month(|| "2026-09".into());
    assert_eq!(fresh.spent_total(), 0, "the cache really was lost");

    // The caller folds its ledger and says what was actually committed.
    fresh.reconcile_total(one * 3).unwrap();
    assert_eq!(fresh.spent_total(), one * 3);
    assert!(
        crate::ask::ensure(&fresh, "gunnar/b/2027", &disk(10), None).is_err(),
        "the budget is honoured again after a restore"
    );

    // UPWARD ONLY: a smaller authoritative number must not talk the cap down.
    // Under-counting spend is the only direction that costs money.
    fresh.reconcile_total(0).unwrap();
    assert_eq!(
        fresh.spent_total(),
        one * 3,
        "a lower figure must never lower the committed total"
    );

    // And it survives the next restart, because reconciling WROTE it.
    let again = SpendCap::new(FakeCloud::new(), total(one * 3), path)
        .unwrap()
        .with_month(|| "2026-09".into());
    assert_eq!(
        again.spent_total(),
        one * 3,
        "the correction was persisted, not just held"
    );
}

/// **The cap passes `inventory()` through unchanged.** Only the inner can produce
/// the list (one row per reference it was asked to ensure, `foreign: 7`), so a
/// list read through the cap that matches it is the inner's, not a default.
#[test]
fn inventory_passes_through_the_cap_unchanged() {
    let fake = FakeCloud::new();
    let per_gib = fake.per_gib;
    let cap = SpendCap::new(fake, sek(100_000), tmp("inventory")).unwrap();
    assert_eq!(
        crate::ask::account(&cap).unwrap(),
        crate::Inventory {
            owned: vec![],
            foreign: 7,
            currency: Currency::Sek
        },
        "the inner's foreign count and currency, not a default"
    );
    crate::ask::ensure(&cap, "gunnar/a/2026-09", &disk(1), None).unwrap();
    crate::ask::ensure(&cap, "gunnar/b/2026-09", &disk(1), None).unwrap();
    let inv = crate::ask::account(&cap).unwrap();
    assert_eq!(inv.foreign, 7);
    assert_eq!(
        inv.owned
            .iter()
            .map(|o| (
                o.id.id.as_str(),
                o.reference.as_deref(),
                o.provenance.as_str()
            ))
            .collect::<Vec<_>>(),
        vec![
            ("storage/gunnar/a/2026-09", Some("gunnar/a/2026-09"), "fake"),
            ("storage/gunnar/b/2026-09", Some("gunnar/b/2026-09"), "fake")
        ]
    );
    assert_eq!(
        inv.owned[0].monthly_minor,
        Some(per_gib),
        "the inner's price, untouched"
    );
    assert_eq!(
        (inv.owned[0].currency, inv.currency),
        (Currency::Sek, Currency::Sek),
        "and its currency, untouched"
    );
}

/// The console asks the PROVIDER SEAM for the month's spend, never the cap's
/// type: a bare provider must answer "no window" and a cap must answer the same
/// figure `ensure` refuses on. Red first: the seam did not exist.
#[test]
fn the_month_window_is_read_from_the_shared_ledger_the_host_holds() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    let cap = SpendCap::new(inner, sek(one * 3), tmp("seam"))
        .unwrap()
        .with_month(|| "2026-09".into());
    assert_eq!(cap.cap().spent_this_month(), 0);
    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    assert_eq!(
        cap.cap().spent_this_month(),
        one,
        "a purchase moves the window"
    );
    assert_eq!(
        cap.cap().spent_this_month(),
        cap.spent_this_month(),
        "the shared ledger and the cap's own method agree"
    );
    // Through an `Arc`, as the server holds it.
    let shared = std::sync::Arc::new(cap);
    assert_eq!(shared.cap().spent_this_month(), one);
}

/// **`CannotSpend` holds a provider to its promise.** A fake that DOES buy,
/// wrapped as one that cannot: the purchase is destroyed on the spot (the
/// fake's destroy count moves), the call is refused naming the provider and the
/// resource, and there is no ceiling to report. The twin: the same fake told to
/// answer OnHand passes straight through.
#[test]
fn cannot_spend_destroys_and_refuses_a_provisioned_answer_and_passes_on_hand_through() {
    let buys = CannotSpend::new(FakeCloud::new());
    let err = crate::ask::ensure(&buys, "gunnar/alice/2026-09", &disk(10), None)
        .err()
        .map(|e| e.to_string())
        .expect("a purchase must be refused");
    assert!(
        err.contains("fake is registered as a provider that cannot spend")
            && err.contains("storage/gunnar/alice/2026-09"),
        "{err}"
    );
    assert!(err.contains("destroyed it"), "{err}");
    assert_eq!(
        buys.inner().destroy_calls(),
        1,
        "the byte was given back before the refusal"
    );
    assert_eq!(buys.inner().ensure_calls_for("gunnar/alice/2026-09"), 1);
    assert_eq!(
        Cloud::price(&buys, &crate::cloud::Priced::Resource(&disk(10)))
            .unwrap()
            .minor_per_month,
        1000,
        "price forwards"
    );

    let honest = CannotSpend::new(FakeCloud::new().always_on_hand());
    assert_eq!(
        crate::ask::ensure(&honest, "gunnar/alice/2026-09", &disk(10), None).unwrap(),
        Ensured::OnHand {
            pool_free_after: 999
        }
    );
    assert_eq!(honest.inner().destroy_calls(), 0);
    assert_eq!(
        crate::ask::account(&honest).unwrap().owned.len(),
        1,
        "inventory forwards"
    );
    assert_eq!(
        crate::ask::status(
            &honest,
            &ResourceId {
                provider: "fake".into(),
                id: "x".into()
            }
        )
        .unwrap(),
        ResourceStatus::Active
    );
}

/// **A quote against the ceiling is the purchase's admission without the
/// purchase**: `can_serve` refuses what `ensure` would refuse, prices the delta
/// as a WHOLE, and commits nothing. Then the same delta bought really does move
/// the total — so the two cannot drift.
#[test]
fn can_serve_prices_the_whole_delta_against_the_ceiling_and_commits_nothing() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    let cap = SpendCap::new(inner, sek(one * 2), tmp("quote"))
        .unwrap()
        .with_month(|| "2026-09".into());
    assert_eq!(crate::ask::can_serve(&cap, &[disk(10)], None), Ok(()));
    assert_eq!(
        crate::ask::can_serve(&cap, &[disk(10), disk(10)], None),
        Ok(()),
        "exactly the ceiling fits"
    );
    let err = crate::ask::can_serve(&cap, &[disk(10), disk(10), disk(10)], None).unwrap_err();
    assert!(
        err.contains("spend cap refuses")
            && err.contains(&format!(
                "cost {}",
                monetize_pay::Money {
                    minor: one * 3,
                    currency: Currency::Sek
                }
                .in_words()
            )),
        "three that each fit and together do not: {err}"
    );
    assert_eq!(
        (cap.spent_this_month(), cap.spent_total()),
        (0, 0),
        "a quote commits nothing"
    );
    assert!(
        !tmp("quote").exists()
            || std::fs::read_to_string(tmp("quote"))
                .map(|s| !s.contains(&one.to_string()))
                .unwrap_or(true),
        "and writes nothing"
    );

    // Buying moves what the quote said it would, and the next quote sees it.
    crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None).unwrap();
    assert_eq!(cap.spent_total(), one);
    assert_eq!(crate::ask::can_serve(&cap, &[disk(10)], None), Ok(()));
    assert!(
        crate::ask::can_serve(&cap, &[disk(10), disk(10)], None).is_err(),
        "one left, two asked"
    );
    // A currency the cap is not set in is refused by the quote as by the purchase.
    struct EurCloud(FakeCloud);
    #[allow(dead_code)]
    impl EurCloud {
        pub fn provider(&self) -> &'static str {
            "eur"
        }
        pub fn zones(&self) -> &[Zone] {
            self.0.zones()
        }
        pub fn price_resource(&self, r: &Resource) -> Result<Price, CloudError> {
            Ok(Price {
                currency: Currency::Eur,
                ..crate::Cloud::price(&self.0, &crate::cloud::Priced::Resource(r))?
            })
        }
        pub fn ensure(&self, reference: &str, resource: &Resource) -> Result<Ensured, CloudError> {
            self.0.ensure(reference, resource)
        }
        pub fn destroy_id(&self, id: &ResourceId) -> Result<(), CloudError> {
            crate::ask::destroy_one(&self.0, id)
        }
        pub fn status(&self, id: &ResourceId) -> Result<ResourceStatus, CloudError> {
            self.0.status(id)
        }
        pub fn list_owned(&self) -> Result<crate::Inventory, CloudError> {
            crate::ask::account(&self.0)
        }
    }
    #[allow(unused_variables, unreachable_code)]
    impl crate::cloud::Cloud for EurCloud {
        fn manifest(&self) -> crate::cloud::CloudManifest {
            crate::cloud::CloudManifest {
                provider: self.provider().to_string(),
                zones: self.zones().to_vec(),
                settings: Vec::new(),
                grow_refused: Some(crate::grow::unsupported(self.provider()).to_string()),
            }
        }
        fn catalog(&self, zone: &crate::Zone) -> Result<crate::cloud::Catalog, crate::CloudError> {
            Err(crate::lay::unsupported(self.provider()))
        }
        fn price(
            &self,
            what: &crate::cloud::Priced<'_>,
        ) -> Result<crate::Price, crate::CloudError> {
            match what {
                crate::cloud::Priced::Resource(r) => self.price_resource(r),
                crate::cloud::Priced::Plan(p) => {
                    crate::price_estate_by_rows(p, |r| self.price_resource(r), None)
                }
                crate::cloud::Priced::Grow(set) => Err(crate::grow::unsupported(self.provider())),
            }
        }
        fn lay(
            &self,
            plan: &crate::lay::EstatePlan,
            journal: &crate::grow::Journal,
        ) -> Result<crate::lay::LaidEstate, crate::CloudError> {
            Err(crate::lay::unsupported(self.provider()))
        }
        fn change(
            &self,
            change: &crate::cloud::Change,
            journal: &crate::grow::Journal,
        ) -> Result<crate::cloud::Changed, crate::CloudError> {
            Ok(match change {
                crate::cloud::Change::Plan {
                    server,
                    plan,
                    delta,
                } => return Err(crate::lay::unsupported_verb(self.provider(), "change_plan")),
                crate::cloud::Change::Grow { set, estate } => {
                    return Err(crate::grow::unsupported(self.provider()))
                }
                crate::cloud::Change::Settle { set } => {
                    return Err(crate::grow::unsupported(self.provider()))
                }
                crate::cloud::Change::ForgetMedia { laid } => {
                    crate::cloud::Changed::Forgotten(Vec::new())
                }
                crate::cloud::Change::Ensure {
                    reference,
                    resource,
                    room,
                } => crate::cloud::Changed::Ensured(self.ensure(reference, resource)?),
                crate::cloud::Change::Retire { reference, machine } => {
                    crate::cloud::Changed::Retired(crate::cloud::retire_by_rows(
                        self, reference, machine,
                    )?)
                }
                crate::cloud::Change::LabelAdopted { .. } => {
                    return Err(crate::lay::unsupported_verb(
                        self.provider(),
                        "label_adopted",
                    ))
                }
            })
        }
        fn boot(
            &self,
            boot: &crate::cloud::Boot,
            journal: &crate::grow::Journal,
        ) -> Result<crate::cloud::Booted, crate::CloudError> {
            Ok(match boot {
                crate::cloud::Boot::Keep(k) => {
                    return Err(crate::lay::unsupported_verb(self.provider(), "keep_boot"))
                }
                crate::cloud::Boot::Power { server, on } => {
                    return Err(crate::lay::unsupported_verb(self.provider(), "set_power"))
                }
                crate::cloud::Boot::Restore(r) => {
                    return Err(crate::lay::unsupported_verb(
                        self.provider(),
                        "restore_from_backup",
                    ))
                }
            })
        }
        fn firewall(&self, laid: &crate::lay::LaidEstate) -> Result<(), crate::CloudError> {
            Err(crate::lay::unsupported_verb(
                self.provider(),
                "set_estate_firewall",
            ))
        }
        fn destroy(
            &self,
            what: &crate::cloud::Give<'_>,
        ) -> Result<crate::Reclaimed, crate::CloudError> {
            match what {
                crate::cloud::Give::References(refs) => {
                    Ok(crate::release_for_by_rows(self.list_owned()?, refs, |id| {
                        self.destroy_id(id)
                    }))
                }
                crate::cloud::Give::Resource(id) => {
                    self.destroy_id(id)?;
                    Ok(crate::Reclaimed {
                        destroyed: vec![(*id).clone()],
                        ..crate::Reclaimed::default()
                    })
                }
            }
        }
        fn inventory(
            &self,
            what: &crate::cloud::Listing<'_>,
        ) -> Result<crate::cloud::Listed, crate::CloudError> {
            Ok(match what {
                crate::cloud::Listing::Account => crate::cloud::Listed::Account(self.list_owned()?),
                crate::cloud::Listing::Set(set) => {
                    return Err(crate::grow::unsupported(self.provider()))
                }
                crate::cloud::Listing::Status(id) => crate::cloud::Listed::Status(self.status(id)?),
                crate::cloud::Listing::Under(refs) => {
                    crate::cloud::Listed::Under(crate::cloud::under(self.list_owned()?, refs))
                }
                crate::cloud::Listing::Room { delta, room } => {
                    crate::cloud::Listed::Room(crate::cloud::RoomLeft {
                        serve: Ok(()),
                        left_bytes: None,
                        ceilings: Default::default(),
                    })
                }
                crate::cloud::Listing::Admits(_plan) => crate::cloud::Listed::Admitted,
            })
        }
    }
    let cap = SpendCap::new(EurCloud(FakeCloud::new()), sek(one * 2), tmp("quote-fx")).unwrap();
    assert!(
        crate::ask::can_serve(&cap, &[disk(1)], None)
            .unwrap_err()
            .contains("no conversion"),
        "the quote refuses the fiction too"
    );
    // The CannotSpend guard passes the question through to the box.
    let inner = FakeCloud::new().always_on_hand();
    assert_eq!(
        crate::ask::can_serve(&CannotSpend::new(inner), &[disk(1)], None),
        Ok(())
    );
    assert!(crate::ask::ceilings(&CannotSpend::new(FakeCloud::new())).is_empty());
}

/// **A cap at its ceiling does not refuse a sale that buys nothing.** The
/// product measured the room (`Room::OnHand`), so there is no price to admit;
/// the same resource told `Room::Buy` is refused as before.
#[test]
fn on_hand_is_not_admitted_against_the_ceiling_and_buy_still_is() {
    let cap = SpendCap::new(
        FakeCloud::new().always_on_hand(),
        total(0),
        tmp("room-on-hand"),
    )
    .unwrap();
    assert_eq!(
        crate::ask::ensure(&cap, "gunnar/a/2026-09", &disk(10), Some(Room::OnHand)).unwrap(),
        Ensured::OnHand {
            pool_free_after: 999
        }
    );
    assert!(
        matches!(
            crate::ask::ensure(&cap, "gunnar/a/2026-09", &disk(10), Some(Room::Buy)),
            Err(CloudError::OverCap(_))
        ),
        "a zero ceiling still refuses a buy"
    );
    assert_eq!(
        cap.inner.ensure_calls_for("gunnar/a/2026-09"),
        1,
        "the refused buy never reached the provider"
    );
}

/// **What a provider bought against `Room::OnHand` is still counted.** The
/// cache records what was spent, not what was asked; the transaction undoes it.
#[test]
fn on_hand_that_bought_anyway_is_accounted() {
    let cap = SpendCap::new(FakeCloud::new(), total(10_000), tmp("room-bought-anyway")).unwrap();
    assert!(matches!(
        crate::ask::ensure(&cap, "gunnar/a/2026-09", &disk(10), Some(Room::OnHand)).unwrap(),
        Ensured::Provisioned { .. }
    ));
    assert_eq!(cap.spent_total(), 1_000);
}

// ── RULING S4: is `total_minor` a lifetime budget or a standing run-rate? ────
//
// OPEN on 2026-09-20, and deliberately NOT decided here. `Spent::total_minor`
// is documented "Committed EVER, across every month. Never reset"
// (`cap/mod.rs:65`) and all three commit sites add `cost.minor_per_month` — a
// monthly RATE — into it (`cap/mod.rs:336`, `:391`, `:428`). The two readings
// differ by a factor of twelve a year on the same estate, and the estate runs
// `cap = { total_minor = 2500, monthly_minor = 2500, currency = "Eur" }`.
//
// Neither the arithmetic nor the name is touched by these two tests. Exactly
// ONE of them survives the ruling: un-ignore the reading the owner picks and
// delete the other. Reading A passes against today's arithmetic; reading B is
// RED today, and that red is the size of the change the ruling would order.
//
// Verified against the rest of the system, which reads A at every point that
// recomputes the number: `Ledger::fold_provisioned`
// (`monetize/src/ledger.rs:706`, `:712`) adds each provision's and each
// growth's `minor_per_month` ONCE per event and has no time factor at all, and
// it is the AUTHORITY — `SpendCap::reconcile_total` (`cap/mod.rs:234`) and
// `monetize_server::grow::ceiling_remaining` (`grow.rs:491`) both take their
// total from that fold.

/// A month a test can move by hand, for the two ruling tests below.
fn month_dial(
    start: &str,
) -> (
    std::sync::Arc<Mutex<String>>,
    impl Fn() -> String + Send + Sync + 'static,
) {
    let dial = std::sync::Arc::new(Mutex::new(start.to_owned()));
    let read = dial.clone();
    (dial, move || read.lock().unwrap().clone())
}

/// **RULING S4 — MONTHLY, RESETTING (owner 2026-09-28, a-ceiling-s3).** The
/// ceiling an estate runs on is `monthly_minor`: what monetize may COMMIT in a
/// calendar month. Past it every purchase and growth is refused (by name, and
/// the grow batcher raises an ALERT); on the 1st it is free again. The two
/// readings of a lifetime `total_minor` that stood here `#[ignore]`d are gone
/// with the question: the kit emits `monthly_minor` only.
#[test]
fn ruling_s4_a_monthly_ceiling_refuses_past_it_this_month_and_resets_on_the_first() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    let (dial, month) = month_dial("2026-09");
    let cap = SpendCap::new(
        inner,
        Ceiling::monthly(one, Currency::Sek),
        tmp("s4-monthly"),
    )
    .unwrap()
    .with_month(month);
    crate::ask::ensure(&cap, "gunnar/s4/2026-09-01", &disk(10), None).unwrap();
    assert_eq!(cap.remaining(), 0, "September is spent");
    let refused = crate::ask::ensure(&cap, "gunnar/s4/2026-09-02", &disk(10), None)
        .unwrap_err()
        .to_string();
    assert!(
        refused.contains("this month"),
        "refused past the ceiling, by name: {refused}"
    );
    *dial.lock().unwrap() = "2026-10".into();
    assert_eq!(cap.remaining(), one, "October starts free");
    crate::ask::ensure(&cap, "gunnar/s4/2026-10-01", &disk(10), None)
        .expect("the ceiling reset on the 1st");
}

// ── growing a data set through the cap: the RESERVATION (T7) ─────────────────

mod grow_reservation {
    use super::*;
    use crate::grow::{DataSet, GrowOutcome, Journal, SetSurvey, VolumeId};
    use std::sync::atomic::AtomicU64;

    /// A provider that can grow: prices the delta per member, and answers
    /// `grow_set` / `resume_grow` from a script. Counts what reached it.
    #[derive(Default)]
    struct GrowFake {
        per_gib: u64,
        current_gib: u64,
        grow_answer: Mutex<Option<Result<GrowOutcome, CloudError>>>,
        resume_answer: Mutex<Option<Result<GrowOutcome, CloudError>>>,
        grow_calls: AtomicU64,
        resume_calls: AtomicU64,
        zones: Vec<Zone>,
    }

    impl GrowFake {
        fn new(current_gib: u64) -> Self {
            GrowFake {
                per_gib: 10,
                current_gib,
                zones: vec![Zone("se-sto1".into())],
                ..Default::default()
            }
        }
        fn answers(self, grow: Result<GrowOutcome, CloudError>) -> Self {
            *self.grow_answer.lock().unwrap() = Some(grow);
            self
        }
        fn resumes(self, resume: Result<GrowOutcome, CloudError>) -> Self {
            *self.resume_answer.lock().unwrap() = Some(resume);
            self
        }
    }

    #[allow(dead_code)]
    impl GrowFake {
        pub fn provider(&self) -> &'static str {
            "growfake"
        }
        pub fn zones(&self) -> &[Zone] {
            &self.zones
        }
        pub fn price_resource(&self, resource: &Resource) -> Result<Price, CloudError> {
            let gib = match resource {
                Resource::Disk { gib, .. } => *gib,
                _ => 1,
            };
            Ok(Price {
                minor_per_month: self.per_gib * gib,
                currency: Currency::Sek,
            })
        }
        pub fn ensure(
            &self,
            _reference: &str,
            _resource: &Resource,
        ) -> Result<Ensured, CloudError> {
            unreachable!("a growth never buys")
        }
        pub fn destroy_id(&self, _id: &ResourceId) -> Result<(), CloudError> {
            Ok(())
        }
        pub fn status(&self, _id: &ResourceId) -> Result<ResourceStatus, CloudError> {
            Ok(ResourceStatus::Active)
        }
        pub fn list_owned(&self) -> Result<crate::Inventory, CloudError> {
            Ok(crate::Inventory {
                owned: vec![],
                foreign: 0,
                currency: Currency::Sek,
            })
        }
        pub fn price_grow(&self, set: &DataSet) -> Result<Price, CloudError> {
            if set.target_gib < self.current_gib {
                return Err(CloudError::Refused("shrink".into()));
            }
            Ok(Price {
                minor_per_month: self.per_gib
                    * (set.target_gib - self.current_gib)
                    * set.members.len() as u64,
                currency: Currency::Sek,
            })
        }
        pub fn survey_set(&self, set: &DataSet) -> Result<SetSurvey, CloudError> {
            Ok(SetSurvey {
                volumes: set
                    .members
                    .iter()
                    .map(|m| crate::grow::VolumeFact {
                        id: m.clone(),
                        gib: self.current_gib,
                        state: "online".into(),
                        attached: true,
                        address: None,
                    })
                    .collect(),
                server_state: "started".into(),
                current_gib: self.current_gib,
                equal: true,
                all_attached: true,
            })
        }
        pub fn grow_set(
            &self,
            _set: &DataSet,
            _journal: &Journal,
        ) -> Result<GrowOutcome, CloudError> {
            self.grow_calls.fetch_add(1, Ordering::SeqCst);
            self.grow_answer
                .lock()
                .unwrap()
                .take()
                .expect("a scripted answer")
        }
        pub fn resume_grow(
            &self,
            _set: &DataSet,
            _journal: &Journal,
        ) -> Result<GrowOutcome, CloudError> {
            self.resume_calls.fetch_add(1, Ordering::SeqCst);
            self.resume_answer
                .lock()
                .unwrap()
                .take()
                .expect("a scripted resume")
        }
    }
    #[allow(unused_variables, unreachable_code)]
    impl crate::cloud::Cloud for GrowFake {
        fn manifest(&self) -> crate::cloud::CloudManifest {
            crate::cloud::CloudManifest {
                provider: self.provider().to_string(),
                zones: self.zones().to_vec(),
                settings: Vec::new(),
                grow_refused: Some(crate::grow::unsupported(self.provider()).to_string()),
            }
        }
        fn catalog(&self, zone: &crate::Zone) -> Result<crate::cloud::Catalog, crate::CloudError> {
            Err(crate::lay::unsupported(self.provider()))
        }
        fn price(
            &self,
            what: &crate::cloud::Priced<'_>,
        ) -> Result<crate::Price, crate::CloudError> {
            match what {
                crate::cloud::Priced::Resource(r) => self.price_resource(r),
                crate::cloud::Priced::Plan(p) => {
                    crate::price_estate_by_rows(p, |r| self.price_resource(r), None)
                }
                crate::cloud::Priced::Grow(set) => self.price_grow(set),
            }
        }
        fn lay(
            &self,
            plan: &crate::lay::EstatePlan,
            journal: &crate::grow::Journal,
        ) -> Result<crate::lay::LaidEstate, crate::CloudError> {
            Err(crate::lay::unsupported(self.provider()))
        }
        fn change(
            &self,
            change: &crate::cloud::Change,
            journal: &crate::grow::Journal,
        ) -> Result<crate::cloud::Changed, crate::CloudError> {
            Ok(match change {
                crate::cloud::Change::Plan {
                    server,
                    plan,
                    delta,
                } => return Err(crate::lay::unsupported_verb(self.provider(), "change_plan")),
                crate::cloud::Change::Grow { set, estate } => {
                    crate::cloud::Changed::Grown(self.grow_set(set, journal)?)
                }
                crate::cloud::Change::Settle { set } => {
                    crate::cloud::Changed::Grown(self.resume_grow(set, journal)?)
                }
                crate::cloud::Change::ForgetMedia { laid } => {
                    crate::cloud::Changed::Forgotten(Vec::new())
                }
                crate::cloud::Change::Ensure {
                    reference,
                    resource,
                    room,
                } => crate::cloud::Changed::Ensured(self.ensure(reference, resource)?),
                crate::cloud::Change::Retire { reference, machine } => {
                    crate::cloud::Changed::Retired(crate::cloud::retire_by_rows(
                        self, reference, machine,
                    )?)
                }
                crate::cloud::Change::LabelAdopted { .. } => {
                    return Err(crate::lay::unsupported_verb(
                        self.provider(),
                        "label_adopted",
                    ))
                }
            })
        }
        fn boot(
            &self,
            boot: &crate::cloud::Boot,
            journal: &crate::grow::Journal,
        ) -> Result<crate::cloud::Booted, crate::CloudError> {
            Ok(match boot {
                crate::cloud::Boot::Keep(k) => {
                    return Err(crate::lay::unsupported_verb(self.provider(), "keep_boot"))
                }
                crate::cloud::Boot::Power { server, on } => {
                    return Err(crate::lay::unsupported_verb(self.provider(), "set_power"))
                }
                crate::cloud::Boot::Restore(r) => {
                    return Err(crate::lay::unsupported_verb(
                        self.provider(),
                        "restore_from_backup",
                    ))
                }
            })
        }
        fn firewall(&self, laid: &crate::lay::LaidEstate) -> Result<(), crate::CloudError> {
            Err(crate::lay::unsupported_verb(
                self.provider(),
                "set_estate_firewall",
            ))
        }
        fn destroy(
            &self,
            what: &crate::cloud::Give<'_>,
        ) -> Result<crate::Reclaimed, crate::CloudError> {
            match what {
                crate::cloud::Give::References(refs) => {
                    Ok(crate::release_for_by_rows(self.list_owned()?, refs, |id| {
                        self.destroy_id(id)
                    }))
                }
                crate::cloud::Give::Resource(id) => {
                    self.destroy_id(id)?;
                    Ok(crate::Reclaimed {
                        destroyed: vec![(*id).clone()],
                        ..crate::Reclaimed::default()
                    })
                }
            }
        }
        fn inventory(
            &self,
            what: &crate::cloud::Listing<'_>,
        ) -> Result<crate::cloud::Listed, crate::CloudError> {
            Ok(match what {
                crate::cloud::Listing::Account => crate::cloud::Listed::Account(self.list_owned()?),
                crate::cloud::Listing::Set(set) => crate::cloud::Listed::Set(self.survey_set(set)?),
                crate::cloud::Listing::Status(id) => crate::cloud::Listed::Status(self.status(id)?),
                crate::cloud::Listing::Under(refs) => {
                    crate::cloud::Listed::Under(crate::cloud::under(self.list_owned()?, refs))
                }
                crate::cloud::Listing::Room { delta, room } => {
                    crate::cloud::Listed::Room(crate::cloud::RoomLeft {
                        serve: Ok(()),
                        left_bytes: None,
                        ceilings: Default::default(),
                    })
                }
                crate::cloud::Listing::Admits(_plan) => crate::cloud::Listed::Admitted,
            })
        }
    }

    fn set(target_gib: u64) -> DataSet {
        DataSet {
            product: "gunnar".into(),
            server: crate::ServerId("srv".into()),
            members: (1..=4).map(|i| VolumeId(format!("vol-{i}"))).collect(),
            target_gib,
        }
    }

    fn journal(name: &str) -> Journal {
        Journal::open(tmp(name).with_file_name("grow.journal")).unwrap()
    }

    fn sek_price(minor: u64) -> Price {
        Price {
            minor_per_month: minor,
            currency: Currency::Sek,
        }
    }

    fn done(cost: u64) -> GrowOutcome {
        GrowOutcome::Done {
            grown: (1..=4).map(|i| VolumeId(format!("vol-{i}"))).collect(),
            from_gib: 32,
            to_gib: 64,
            cost: sek_price(cost),
        }
    }

    /// The delta — 4 × 32 GiB × 10 = 1280 — is admitted and RESERVED before
    /// the provider is asked, and what the provider reports grown is what is
    /// committed; the reservation is gone afterwards.
    #[test]
    fn the_delta_is_reserved_before_the_provider_is_asked_and_committed_from_what_grew() {
        let inner = GrowFake::new(32).answers(Ok(done(1_280)));
        let cap = SpendCap::new(inner, sek(2_000), tmp("grow-ok"))
            .unwrap()
            .with_month(|| "2026-09".into());
        let outcome = crate::ask::grow(&cap, &set(64), false, &journal("grow-ok")).unwrap();
        assert_eq!(outcome.word(), "done");
        assert_eq!(cap.inner.grow_calls.load(Ordering::SeqCst), 1);
        assert_eq!(cap.reserved_minor(), 0, "settled");
        assert_eq!((cap.spent_this_month(), cap.spent_total()), (1_280, 1_280));
        assert_eq!(cap.remaining(), 720);
    }

    /// **T7: the ceiling is checked against the WHOLE delta before member 1.**
    /// A cap that would admit three members' delta but not four refuses with
    /// the provider never asked and nothing reserved.
    #[test]
    fn a_cap_too_small_for_the_delta_refuses_before_member_one() {
        let inner = GrowFake::new(32).answers(Ok(done(1_280)));
        let cap = SpendCap::new(inner, sek(1_279), tmp("grow-short")).unwrap();
        let err = crate::ask::grow(&cap, &set(64), false, &journal("grow-short"))
            .unwrap_err()
            .to_string();
        assert!(
            err.contains("12.80 SEK") && err.contains("12.79 SEK"),
            "names the numbers, in words: {err}"
        );
        assert_eq!(
            cap.inner.grow_calls.load(Ordering::SeqCst),
            0,
            "the provider was never asked"
        );
        assert_eq!(cap.reserved_minor(), 0);
        assert_eq!(cap.spent_total(), 0);
    }

    /// The provider refused before touching anything: the reservation is
    /// released, nothing is committed.
    #[test]
    fn a_refusal_before_anything_was_touched_releases_the_reservation() {
        let inner =
            GrowFake::new(32).answers(Err(CloudError::Refused("HOTPLUG_FAILED on vol-1".into())));
        let cap = SpendCap::new(inner, sek(2_000), tmp("grow-refused")).unwrap();
        assert!(crate::ask::grow(&cap, &set(64), false, &journal("grow-refused")).is_err());
        assert_eq!(cap.reserved_minor(), 0);
        assert_eq!(cap.spent_total(), 0);
    }

    /// **The crash story.** An outage mid-growth keeps the reservation — in
    /// the FILE, so a fresh process still sees the headroom held — and the
    /// resume's outcome settles it: a partial commits the delta of what grew.
    #[test]
    fn an_outage_keeps_the_reservation_across_a_reload_and_the_resume_settles_it() {
        let path = tmp("grow-crash");
        let inner = GrowFake::new(32).answers(Err(CloudError::Unavailable(
            "attach of vol-3 timed out; journal open".into(),
        )));
        let cap = SpendCap::new(inner, sek(2_000), path.clone()).unwrap();
        assert!(matches!(
            crate::ask::grow(&cap, &set(64), false, &journal("grow-crash")),
            Err(CloudError::Unavailable(_))
        ));
        assert_eq!(cap.reserved_minor(), 1_280, "held for the resume");
        assert_eq!(
            cap.remaining(),
            720,
            "a second growth cannot spend the held headroom"
        );
        drop(cap);
        // A fresh process.
        let partial = GrowOutcome::Partial {
            grown: vec![
                VolumeId("vol-1".into()),
                VolumeId("vol-2".into()),
                VolumeId("vol-3".into()),
            ],
            not_grown: vec![VolumeId("vol-4".into())],
            cost: sek_price(960),
            why: "3 of 4".into(),
        };
        let again =
            SpendCap::new(GrowFake::new(32).resumes(Ok(partial)), sek(2_000), path).unwrap();
        assert_eq!(
            again.reserved_minor(),
            1_280,
            "the reservation survived the restart"
        );
        assert_eq!(
            crate::ask::settle(&again, &set(64), &journal("grow-crash"))
                .unwrap()
                .word(),
            "partial"
        );
        assert_eq!(again.reserved_minor(), 0);
        assert_eq!(
            again.spent_total(),
            960,
            "what actually grew is what is committed"
        );
    }

    /// The idempotent repeat: the provider answers `Done` with nothing grown
    /// and a cost of zero, and the cap commits nothing.
    #[test]
    fn a_repeat_to_the_same_target_commits_nothing() {
        let inner = GrowFake::new(64).answers(Ok(GrowOutcome::Done {
            grown: vec![],
            from_gib: 64,
            to_gib: 64,
            cost: sek_price(0),
        }));
        let cap = SpendCap::new(inner, sek(100), tmp("grow-repeat")).unwrap();
        assert_eq!(
            crate::ask::grow(&cap, &set(64), false, &journal("grow-repeat"))
                .unwrap()
                .word(),
            "done"
        );
        assert_eq!((cap.spent_total(), cap.reserved_minor()), (0, 0));
    }

    /// A provider registered as unable to spend may grow only for free.
    #[test]
    fn cannot_spend_refuses_a_priced_growth_and_forwards_a_free_one() {
        let priced = CannotSpend::new(GrowFake::new(32).answers(Ok(done(1_280))));
        let err = crate::ask::grow(&priced, &set(64), false, &journal("grow-cannot"))
            .unwrap_err()
            .to_string();
        assert!(
            err.contains("cannot spend") && err.contains("12.80 SEK"),
            "{err}"
        );
        assert_eq!(priced.inner().grow_calls.load(Ordering::SeqCst), 0);
        let free = CannotSpend::new(GrowFake::new(64).answers(Ok(GrowOutcome::Done {
            grown: vec![],
            from_gib: 64,
            to_gib: 64,
            cost: sek_price(0),
        })));
        assert_eq!(
            crate::ask::grow(&free, &set(64), false, &journal("grow-free"))
                .unwrap()
                .word(),
            "done"
        );
        // The default: a provider with no growth path refuses by name.
        assert!(
            crate::ask::grow(&FakeCloud::new(), &set(64), false, &journal("grow-default"))
                .unwrap_err()
                .to_string()
                .contains("cannot grow a data set")
        );
    }
}

/// **A plan change is admitted by what it adds a month**, under one key per
/// server, and settled into the month when it is done; past the ceiling it is
/// refused before the provider is asked. Stopping and starting a server buys
/// nothing and is never capped — not even at a spent ceiling.
#[test]
fn a_plan_change_is_admitted_by_its_monthly_delta_and_power_is_never_capped() {
    let dir = std::env::temp_dir().join(format!("monetize-cap-plan-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let journal = crate::grow::Journal::open(dir.join("journal")).unwrap();
    let cap = SpendCap::new(FakeCloud::new(), sek(1_000), tmp("plan"))
        .unwrap()
        .with_month(|| "2026-10".into());
    let main = ResourceId {
        provider: "fake".into(),
        id: "server/main".into(),
    };
    let up = |minor: u64| Price {
        minor_per_month: minor,
        currency: Currency::Sek,
    };
    crate::ask::change_plan(&cap, &main, "BIGGER", up(600), &journal).unwrap();
    assert_eq!(
        cap.spent_this_month(),
        600,
        "the change's monthly delta is spent"
    );
    assert_eq!(cap.reserved_minor(), 0, "and nothing stays reserved");
    let e = crate::ask::change_plan(&cap, &main, "BIGGEST", up(600), &journal).unwrap_err();
    assert!(
        matches!(&e, CloudError::OverCap(w) if w.contains("spend cap")),
        "{e:?}"
    );
    assert_eq!(
        cap.inner.plan_calls.lock().unwrap().clone(),
        vec!["plan server/main BIGGER".to_string()],
        "the refused change never reached the provider"
    );
    // A smaller plan adds nothing: it is never refused.
    crate::ask::change_plan(&cap, &main, "SMALLER", up(0), &journal).unwrap();
    // Power: past the ceiling, still allowed.
    crate::ask::set_power(&cap, &main, false).unwrap();
    crate::ask::set_power(&cap, &main, true).unwrap();
    assert_eq!(cap.inner.plan_calls.lock().unwrap().len(), 4);
    let _ = std::fs::remove_dir_all(&dir);
}

/// Three machines, one 10 GiB system disk each, for `company`.
fn estate_plan(company: &str) -> crate::lay::EstatePlan {
    use crate::lay::{default_interfaces, reference_of, DiskPlan, EstatePlan, MachinePlan};
    let m = |role: &str| MachinePlan {
        role: role.into(),
        hostname: format!("gunnar-{role}-{company}"),
        title: format!("gunnar {role}-{company}"),
        plan: "1xCPU-2GB".into(),
        boot_order: "disk,cdrom".into(),
        labels: Default::default(),
        interfaces: default_interfaces(),
        medium: String::new(),
        medium_object: None,
        template: String::new(),
        user_data: Default::default(),
        disks: vec![DiskPlan {
            title: format!("gunnar-{role}-{company} system"),
            gib: 10,
            tier: "standard".into(),
            labels: Default::default(),
        }],
    };
    EstatePlan {
        reference: reference_of(company),
        company: company.into(),
        zone: Zone("se-sto1".into()),
        machines: vec![m("front"), m("main"), m("twin")],
        narrate_from: String::new(),
        shape: None,
    }
}

/// ★ **The cap is the one thing that stops a lay, and it says so in words**
/// (2026-10-08, test2's shop): an estate over the cloud's cap is refused
/// before the provider is asked, naming the estate, what it costs a month at
/// the provider and the cap, in money a reader can act on; one under it
/// reaches the provider, and a refusal there gives the reservation back.
#[test]
fn an_estate_over_the_cap_is_refused_in_words_before_the_provider_is_asked() {
    let journal =
        crate::grow::Journal::with_sink(vec![], Box::new(|_: &crate::grow::GrowRecord| Ok(())));
    // 3 × (a server at 1.00 SEK + a 10 GiB disk at 10.00 SEK) = 33.00 SEK a month.
    let tight = SpendCap::new(FakeCloud::new(), sek(2_000), tmp("lay-tight")).unwrap();
    let e = crate::Cloud::lay(&tight, &estate_plan("acme"), &journal)
        .unwrap_err()
        .to_string();
    assert!(e.contains("spend cap refuses estate/acme"), "{e}");
    assert!(
        e.contains("33.00 SEK a month at the provider")
            && e.contains("the cap is 20.00 SEK a month")
            && e.contains("0.00 SEK is already committed"),
        "{e}"
    );
    assert!(
        e.contains("nothing was bought") && !e.contains("cannot lay"),
        "refused by the cap, the provider never asked: {e}"
    );
    assert!(
        tight.cap.spent.lock().unwrap().reserved.is_empty(),
        "nothing reserved for a refused estate"
    );

    let roomy = SpendCap::new(FakeCloud::new(), total(5_000), tmp("lay-roomy")).unwrap();
    for _ in 0..2 {
        let e = crate::Cloud::lay(&roomy, &estate_plan("acme"), &journal)
            .unwrap_err()
            .to_string();
        assert!(
            e.contains("fake cloud cannot lay an estate"),
            "admitted, and the provider's own refusal comes back: {e}"
        );
    }
    assert!(
        roomy.cap.spent.lock().unwrap().reserved.is_empty(),
        "the provider's refusal gave the reservation back"
    );
}

/// ★ **Asked ahead, PURE** (the owner, 2026-10-08: "the customer must not know
/// about a cap! it must be queued"): `can_lay` answers what `lay_estate`'s own
/// admission would, in the same words, and reserves nothing — so the job can
/// keep the order in its queue until the cap is raised; under the cap it says
/// yes, and a resumed lay's own reservation does not count against it.
#[test]
fn can_lay_is_the_lays_admission_asked_ahead_and_reserves_nothing() {
    let tight = SpendCap::new(FakeCloud::new(), sek(2_000), tmp("canlay-tight")).unwrap();
    let e = tight.can_lay(&estate_plan("acme")).unwrap_err().to_string();
    assert!(
        e.contains(ESTATE_OVER_CAP)
            && e.contains("33.00 SEK a month at the provider")
            && e.contains("the cap is 20.00 SEK a month"),
        "{e}"
    );
    assert!(
        tight.cap.spent.lock().unwrap().reserved.is_empty(),
        "asked, not reserved"
    );
    let roomy = SpendCap::new(FakeCloud::new(), sek(5_000), tmp("canlay-roomy")).unwrap();
    assert!(roomy.can_lay(&estate_plan("acme")).is_ok());
    // A resumed lay: its own reservation (3300 of 5000) is not counted twice.
    roomy
        .cap
        .spent
        .lock()
        .unwrap()
        .reserved
        .insert("lay:estate/acme".into(), 3_300);
    assert!(
        roomy.can_lay(&estate_plan("acme")).is_ok(),
        "its own reservation is the one it replaces"
    );
    assert!(
        roomy.can_lay(&estate_plan("bolt")).is_err(),
        "another estate counts what acme holds"
    );
}

/// ★ **An estate given back in the month it was laid gives that month back**
/// (round 5.3.1; found by the UpCloud mockup's test2 row, 2026-10-08): a paid
/// order whose install failed is given back and laid again — counted twice,
/// it held the next order behind a cap it never spent. Its release credits
/// what its lay committed this month; nothing else moves.
#[test]
fn an_estate_given_back_in_its_month_gives_the_month_back() {
    let journal =
        crate::grow::Journal::with_sink(vec![], Box::new(|_: &crate::grow::GrowRecord| Ok(())));
    // Each estate: 33.00 SEK a month; the cap: 50.00 SEK this month.
    let cap = SpendCap::new(FakeCloud::new().laying(), sek(5_000), tmp("lay-credit")).unwrap();
    crate::Cloud::lay(&cap, &estate_plan("acme"), &journal).expect("acme fits");
    assert_eq!(cap.spent_this_month(), 3_300);
    assert!(
        crate::Cloud::lay(&cap, &estate_plan("bolt"), &journal)
            .unwrap_err()
            .to_string()
            .contains(ESTATE_OVER_CAP),
        "bolt does not fit beside acme"
    );
    crate::ask::release_for(&cap, &["estate/acme".to_string()]).unwrap();
    assert_eq!(
        cap.spent_this_month(),
        0,
        "acme's month came back with its iron"
    );
    crate::Cloud::lay(&cap, &estate_plan("bolt"), &journal).expect("now bolt fits");
    // A release of anything else moves nothing.
    crate::ask::release_for(&cap, &["estate/acme".to_string(), "room/x".to_string()]).unwrap();
    assert_eq!(cap.spent_this_month(), 3_300, "bolt stays committed");
}

/// The guardian's relay of ONE role of `company` (`estate_order::relay::relay_one`'s shape).
fn relay_plan(company: &str, role: &str) -> crate::lay::EstatePlan {
    let mut p = estate_plan(company);
    p.machines.retain(|m| m.role == role);
    for m in &mut p.machines {
        m.labels.insert(crate::lay::RELAY_LABEL.into(), role.into());
        m.title.push_str(" relay-1");
        for d in &mut m.disks {
            d.title.push_str(" relay-1");
        }
    }
    p
}

/// ★ **A relay is committed under its ROLE, and the fence of that box gives its month
/// back** (5.4 guardian, gap-2): the relay used to settle under the estate's own key, so
/// it overwrote what the estate's lay committed and its fence never gave anything back.
/// Now `estate/acme#twin` holds the relay; retiring that box this month credits it once;
/// the estate's own release still gives the whole estate's month back.
#[test]
fn a_relay_is_committed_under_its_role_and_its_retire_gives_its_month_back() {
    let journal =
        crate::grow::Journal::with_sink(vec![], Box::new(|_: &crate::grow::GrowRecord| Ok(())));
    let cap = SpendCap::new(FakeCloud::new().laying(), sek(10_000), tmp("relay-credit")).unwrap();
    crate::Cloud::lay(&cap, &estate_plan("acme"), &journal).unwrap();
    assert_eq!(cap.spent_this_month(), 3_300);
    let relay =
        crate::Cloud::lay(&cap, &relay_plan("acme", "twin"), &journal).expect("one box fits");
    assert_eq!(cap.spent_this_month(), 4_400);
    {
        let s = cap.cap.spent.lock().unwrap();
        assert_eq!(
            s.laid.get("estate/acme").map(|l| l.1),
            Some(3_300),
            "the estate's own lay is not overwritten: {:?}",
            s.laid
        );
        assert_eq!(
            s.laid.get("estate/acme#twin").map(|l| l.1),
            Some(1_100),
            "{:?}",
            s.laid
        );
        assert!(s.reserved.is_empty(), "{:?}", s.reserved);
    }
    crate::ask::retire_machine(&cap, "estate/acme", relay.machine("twin").unwrap()).unwrap();
    assert_eq!(
        cap.spent_this_month(),
        3_300,
        "the relay's month came back with its box"
    );
    crate::ask::retire_machine(&cap, "estate/acme", relay.machine("twin").unwrap()).unwrap();
    assert_eq!(cap.spent_this_month(), 3_300, "credited once");
    crate::ask::release_for(&cap, &["estate/acme".to_string()]).unwrap();
    assert_eq!(cap.spent_this_month(), 0);
}

/// A relay laid this month and still standing is given back with its estate.
#[test]
fn an_estates_release_gives_back_its_relays_month_too() {
    let journal =
        crate::grow::Journal::with_sink(vec![], Box::new(|_: &crate::grow::GrowRecord| Ok(())));
    let cap = SpendCap::new(FakeCloud::new().laying(), sek(10_000), tmp("relay-release")).unwrap();
    crate::Cloud::lay(&cap, &estate_plan("acme"), &journal).unwrap();
    crate::Cloud::lay(&cap, &relay_plan("acme", "main"), &journal).unwrap();
    crate::Cloud::lay(&cap, &estate_plan("bolt"), &journal).unwrap();
    assert_eq!(cap.spent_this_month(), 7_700);
    crate::ask::release_for(&cap, &["estate/acme".to_string()]).unwrap();
    assert_eq!(
        cap.spent_this_month(),
        3_300,
        "acme and its relay came back; bolt stays"
    );
}

/// ★ **An outage-kept relay reservation is dropped by the fence of its role** — and only
/// that one: the box it was for is the one being fenced, and the next lay reserves afresh.
#[test]
fn the_fence_drops_its_roles_kept_relay_reservation_only() {
    let cap = SpendCap::new(
        FakeCloud::new().laying(),
        sek(10_000),
        tmp("relay-unreserve"),
    )
    .unwrap();
    {
        let mut s = cap.cap.spent.lock().unwrap();
        s.reserved.insert("lay:estate/acme#main".into(), 1_100);
        s.reserved.insert("lay:estate/acme#twin".into(), 1_100);
        s.reserved.insert("lay:estate/acme".into(), 3_300);
    }
    let main = crate::lay::LaidMachine {
        role: "main".into(),
        hostname: "h".into(),
        title: "t".into(),
        plan: "p".into(),
        server: "estate/acme:main".into(),
        public_ipv4: String::new(),
        utility_ipv4: String::new(),
        disks: vec![],
        medium_clone: String::new(),
    };
    crate::ask::retire_machine(&cap, "estate/acme", &main).unwrap();
    let s = cap.cap.spent.lock().unwrap();
    assert_eq!(
        s.reserved.keys().cloned().collect::<Vec<_>>(),
        ["lay:estate/acme", "lay:estate/acme#twin"]
    );
}

/// The relay's admission is asked under the relay's own key, so a resumed relay does not
/// count its own reservation twice.
#[test]
fn can_lay_a_relay_excludes_its_own_reservation() {
    let cap = SpendCap::new(FakeCloud::new(), sek(1_500), tmp("relay-canlay")).unwrap();
    cap.cap
        .spent
        .lock()
        .unwrap()
        .reserved
        .insert("lay:estate/acme#main".into(), 1_100);
    assert!(
        cap.can_lay(&relay_plan("acme", "main")).is_ok(),
        "its own reservation is the one it replaces"
    );
    assert!(
        cap.can_lay(&relay_plan("acme", "twin")).is_err(),
        "the other role's counts"
    );
}

/// ★ **Money in the operator's words** (live 2026-10-14: "it would cost 20422 Eur/month, 10107
/// is already c…"): a refusal says every amount with its currency and two decimals, and the
/// cap's pure answer says the same numbers before anything is paid.
#[test]
fn a_refusal_over_the_cap_says_every_amount_in_eur_with_two_decimals() {
    let dir = std::env::temp_dir().join(format!("monetize-cap-words-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let journal = crate::grow::Journal::open(dir.join("journal")).unwrap();
    let cap = SpendCap::new(
        FakeCloud::new(),
        Ceiling::monthly(20_000, Currency::Eur),
        tmp("words"),
    )
    .unwrap()
    .with_month(|| "2026-10".into());
    let main = ResourceId {
        provider: "fake".into(),
        id: "server/main".into(),
    };
    let eur = |minor: u64| Price {
        minor_per_month: minor,
        currency: Currency::Eur,
    };
    crate::ask::change_plan(&cap, &main, "BIGGER", eur(10_107), &journal).unwrap();
    let e = crate::ask::change_plan(&cap, &main, "BIGGEST", eur(20_422), &journal).unwrap_err();
    let CloudError::OverCap(w) = &e else {
        panic!("{e:?}")
    };
    assert!(
        w.contains("204.22 EUR a month")
            && w.contains("the cap is 200.00 EUR a month")
            && w.contains("101.07 EUR is already committed"),
        "{w}"
    );
    assert!(
        !w.contains("Eur") && !w.contains("20422") && !w.contains("10107"),
        "no minor units, no debug currency: {w}"
    );
    let short = cap
        .cap_short(eur(20_422), &[])
        .unwrap()
        .expect("over the cap");
    assert_eq!(short.words(), "raise the cap first: this needs 204.22 EUR a month, the cap is 200.00 EUR a month, 101.07 EUR is committed; nothing was charged");
    assert_eq!(
        cap.cap_short(eur(9_893), &[]).unwrap(),
        None,
        "what still fits is not short"
    );
    let _ = std::fs::remove_dir_all(&dir);
}

/// R5a: the cap stands in front of every purchase on the box. A panic that once
/// held its book (a provider that panicked inside a call made under it) must not
/// make every later purchase, and every console page that reads the spend,
/// panic. The book a panic leaves is the last one written, and a reservation it
/// left behind only refuses more: the cap fails closed.
#[test]
fn a_poisoned_spend_book_still_answers_and_still_counts() {
    let inner = FakeCloud::new();
    let one = price_of(&inner, &disk(10));
    let cap = SpendCap::new(inner, sek(one * 2), tmp("poisoned"))
        .unwrap()
        .with_month(|| "2026-09".into());
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let _held = cap.cap.spent.lock();
        panic!("a panic while the spend book is held");
    }));
    assert!(cap.cap.spent.is_poisoned());
    assert_eq!(cap.spent_this_month(), 0);
    assert_eq!(cap.spent_total(), 0);
    assert_eq!(cap.remaining(), one * 2);
    assert_eq!(cap.reserved_minor(), 0);
    assert!(matches!(
        crate::ask::ensure(&cap, "gunnar/a/2027", &disk(10), None)
            .expect("a purchase after the panic"),
        Ensured::Provisioned { .. }
    ));
    assert_eq!(cap.spent_this_month(), one, "and it is counted");
}