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

nordisk/monetize

crates/monetize-server/tests/shop_window_reading.rs

//! **The shop window while the cloud reading is slow** (test2, 2026-10-05).
//!
//! The owner's first visit to You → Storage after a night's idle drew the
//! typed size field ("New storage limit (GiB)") instead of the offer buttons; a
//! reload a minute later drew the buttons. test2's front said why, in three
//! lines a second apart:
//!
//! * `the wallet reading (the set's survey and the price bisect at the cloud)
//!   took 19575 ms — off every customer's path`;
//! * `[clouds.upcloud] op grow_gate took 19478 ms (19478 ms of it queued behind
//!   another op on this guest)` — the WINDOW's own reach asked the UpCloud
//!   guest for its grow gate, and that op queued behind the reading;
//! * the window was ready about 20 s later.
//!
//! The reading is ~a dozen ops on the cloud guest, each a fresh guest behind
//! its one `serial` lock — and `grow_gate` is one of those ops, not the pure
//! read `signup_start_latency.rs`'s double makes it. The last reading was
//! older than `WALLET_STALE_MAX`, so the window had nothing to answer from,
//! queued behind the reading, and a console that cannot tell "not read yet"
//! from "nothing on sale" fell back to the typed order.
//!
//! Owner's spec for the fix (2026-10-05 06:25), and what each test here holds:
//!
//! 1. **Queue the questions**: every window asked while the reading runs waits
//!    on that ONE reading — no second survey, no second bisect — and they all
//!    get its result ([`concurrent_windows_wait_on_one_reading_and_all_get_its_rows`]);
//! 2. the window SAYS it is still being read (`ListBundlesResponse.reading`),
//!    at once, and the rows follow when the reading lands
//!    ([`the_first_window_while_the_cloud_reading_is_slow_says_reading_at_once_then_the_rows`]);
//! 3. **the provider's price and the bisect are cached for at least 10
//!    minutes** — a window inside that answers from the cache with no cloud
//!    call, one after it refreshes
//!    ([`the_window_answers_from_its_cache_for_ten_minutes_and_refreshes_after`]);
//!    and the reading is kept warm in the background before it expires
//!    ([`the_keeper_refreshes_the_reading_before_it_expires_without_anybody_asking`]).
//!
//! The cache is for SHOWING offers only: the grow batcher prices and surveys
//! live before it spends, and nothing here touches that path.

use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use monetize::testing::{Answer, GrowScript, MockCloud, MockPay, MockProduct};
use monetize::BundleSpec;
use monetize_cloud::cap::{Ceiling, SpendCap};
use monetize_cloud::{grow, CloudError, Ensured, Inventory, Price, Resource, ResourceId, ResourceStatus, Room, Zone};
use monetize_pay::Currency;
use monetize_product::{Growth, Product, Servable};
use monetize_proto::purchase_client::PurchaseClient;
use monetize_proto::{self as pb};
use monetize_server::config::GrowSetConfig;
use monetize_server::grow::GrowRuns;
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
use monetize_server::testing::TestServer;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;

const BEARER: &str = "test-bearer-token-of-sufficient-length";
const GIB: u64 = 1 << 30;
const MIN: Duration = Duration::from_secs(60);
/// test2's `[clouds.upcloud] cap`: monthly 50.00 EUR.
const MONTHLY_MINOR: u64 = 5_000;

/// What the cloud guest was asked, by op.
#[derive(Default)]
struct Ops {
    gate: AtomicU64,
    survey: AtomicU64,
    price: AtomicU64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Seen {
    gate: u64,
    survey: u64,
    price: u64,
}

impl Ops {
    fn seen(&self) -> Seen {
        Seen { gate: self.gate.load(Ordering::SeqCst), survey: self.survey.load(Ordering::SeqCst), price: self.price.load(Ordering::SeqCst) }
    }
}

/// **The UpCloud cloud guest, as far as the grow ops go**: every one of them —
/// `grow_gate` INCLUDED, as `monetize_cloud_wasm::GuestCloud::grow_gate` is —
/// is an op that takes [`GuestLike::per_op`] and holds the guest's one
/// `serial` lock. The order path (`price`, `ensure`, …) passes straight
/// through: on test2 orders are served by the box's own `server` cloud, which
/// asks nobody.
struct GuestLike {
    inner: Arc<MockCloud>,
    serial: Mutex<()>,
    per_op: Duration,
    ops: Arc<Ops>,
}

impl GuestLike {
    fn op<T>(&self, count: &AtomicU64, f: impl FnOnce() -> T) -> T {
        let _one = self.serial.lock().unwrap_or_else(|p| p.into_inner());
        count.fetch_add(1, Ordering::SeqCst);
        std::thread::sleep(self.per_op);
        f()
    }
}

#[allow(dead_code)]
impl GuestLike {
    pub fn provider(&self) -> &'static str {
        self.inner.provider()
    }
    pub fn zones(&self) -> &[Zone] {
        self.inner.zones()
    }
    pub fn price_resource(&self, r: &Resource) -> Result<Price, CloudError> {
        monetize_cloud::Cloud::price(&self.inner, &monetize_cloud::cloud::Priced::Resource(r))
    }
    pub fn ensure(&self, reference: &str, r: &Resource) -> Result<Ensured, CloudError> {
        self.inner.ensure(reference, r)
    }
    pub fn ensure_with(&self, reference: &str, r: &Resource, room: Room) -> Result<Ensured, CloudError> {
        self.inner.ensure_with(reference, r, room)
    }
    pub fn destroy_id(&self, id: &ResourceId) -> Result<(), CloudError> {
        monetize_cloud::ask::destroy_one(&self.inner, id)
    }
    pub fn status(&self, id: &ResourceId) -> Result<ResourceStatus, CloudError> {
        self.inner.status(id)
    }
    pub fn list_owned(&self) -> Result<Inventory, CloudError> {
        monetize_cloud::ask::account(&self.inner)
    }
    /// AN OP, as on test2: `op grow_gate took 19478 ms (19478 ms of it queued
    /// behind another op on this guest)`.
    pub fn grow_gate(&self) -> Result<(), CloudError> {
        self.op(&self.ops.gate, || self.inner.grow_gate())
    }
    pub fn price_grow(&self, set: &grow::DataSet) -> Result<Price, CloudError> {
        self.op(&self.ops.price, || self.inner.price_grow(set))
    }
    pub fn survey_set(&self, set: &grow::DataSet) -> Result<grow::SetSurvey, CloudError> {
        self.op(&self.ops.survey, || self.inner.survey_set(set))
    }
    pub fn grow_set(&self, set: &grow::DataSet, journal: &grow::Journal) -> Result<grow::GrowOutcome, CloudError> {
        self.op(&self.ops.price, || self.inner.grow_set(set, journal))
    }
    pub fn resume_grow(&self, set: &grow::DataSet, journal: &grow::Journal) -> Result<grow::GrowOutcome, CloudError> {
        self.op(&self.ops.price, || self.inner.resume_grow(set, journal))
    }
}
#[allow(unused_variables, unreachable_code)]
impl monetize_cloud::cloud::Cloud for GuestLike {
    fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
        // As the real guest host does: the switch it asked once at load, never an op per manifest.
        monetize_cloud::cloud::CloudManifest { provider: self.provider().to_string(), zones: self.zones().to_vec(), settings: Vec::new(), grow_refused: self.inner.grow_gate().err().map(|e| e.to_string()) }
    }
    fn catalog(&self, zone: &monetize_cloud::Zone) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
        Err(monetize_cloud::lay::unsupported(self.provider()))
    }
    fn price(&self, what: &monetize_cloud::cloud::Priced<'_>) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
        match what {
            monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
            monetize_cloud::cloud::Priced::Plan(p) => monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None),
            monetize_cloud::cloud::Priced::Grow(set) => self.price_grow(set),
        }
    }
    fn lay(&self, plan: &monetize_cloud::lay::EstatePlan, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
        Err(monetize_cloud::lay::unsupported(self.provider()))
    }
    fn change(&self, change: &monetize_cloud::cloud::Change, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
        Ok(match change {
            monetize_cloud::cloud::Change::Plan { server, plan, delta } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "change_plan")),
            monetize_cloud::cloud::Change::Grow { set, estate } => monetize_cloud::cloud::Changed::Grown(self.grow_set(set, journal)?),
            monetize_cloud::cloud::Change::Settle { set } => monetize_cloud::cloud::Changed::Grown(self.resume_grow(set, journal)?),
            monetize_cloud::cloud::Change::ForgetMedia { laid } => monetize_cloud::cloud::Changed::Forgotten(Vec::new()),
            monetize_cloud::cloud::Change::Ensure { reference, resource, room } => monetize_cloud::cloud::Changed::Ensured(match room { Some(r) => self.ensure_with(reference, resource, *r)?, None => self.ensure(reference, resource)? }),
            monetize_cloud::cloud::Change::Retire { reference, machine } => monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(self, reference, machine)?),
            monetize_cloud::cloud::Change::LabelAdopted { .. } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "label_adopted")),
        })
    }
    fn boot(&self, boot: &monetize_cloud::cloud::Boot, journal: &monetize_cloud::grow::Journal) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
        Ok(match boot {
            monetize_cloud::cloud::Boot::Keep(k) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "keep_boot")),
            monetize_cloud::cloud::Boot::Power { server, on } => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_power")),
            monetize_cloud::cloud::Boot::Restore(r) => return Err(monetize_cloud::lay::unsupported_verb(self.provider(), "restore_from_backup")),
        })
    }
    fn firewall(&self, laid: &monetize_cloud::lay::LaidEstate) -> Result<(), monetize_cloud::CloudError> {
        Err(monetize_cloud::lay::unsupported_verb(self.provider(), "set_estate_firewall"))
    }
    fn destroy(&self, what: &monetize_cloud::cloud::Give<'_>) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
        match what {
            monetize_cloud::cloud::Give::References(refs) => Ok(monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| self.destroy_id(id))),
            monetize_cloud::cloud::Give::Resource(id) => {
                self.destroy_id(id)?;
                Ok(monetize_cloud::Reclaimed { destroyed: vec![(*id).clone()], ..monetize_cloud::Reclaimed::default() })
            }
        }
    }
    fn inventory(&self, what: &monetize_cloud::cloud::Listing<'_>) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
        Ok(match what {
            monetize_cloud::cloud::Listing::Account => monetize_cloud::cloud::Listed::Account(self.list_owned()?),
            monetize_cloud::cloud::Listing::Set(set) => monetize_cloud::cloud::Listed::Set(self.survey_set(set)?),
            monetize_cloud::cloud::Listing::Status(id) => monetize_cloud::cloud::Listed::Status(self.status(id)?),
            monetize_cloud::cloud::Listing::Under(refs) => monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(self.list_owned()?, refs)),
            monetize_cloud::cloud::Listing::Room { delta, room } => monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft { serve: Ok(()), left_bytes: None, ceilings: Default::default() }),
            monetize_cloud::cloud::Listing::Admits(_plan) => monetize_cloud::cloud::Listed::Admitted,
        })
    }
}

fn bundle(gib: u64) -> BundleSpec {
    BundleSpec { cap_delta: [("pack_bytes".to_string(), gib * GIB)].into(), period_days: Some(30) }
}

struct Rig {
    state: Arc<AppState>,
    ops: Arc<Ops>,
    per_op: Duration,
    server: TestServer,
    channel: Channel,
    dir: std::path::PathBuf,
}

impl Rig {
    /// test2's shape: the gunnar.rs shop window (10 / 50 / 100 GiB), a FULL
    /// 37 GiB two-member set the UpCloud guest can grow under a 50 EUR monthly
    /// cap — so which rows can be served is the set's growable reach, i.e. the
    /// wallet reading — and the state NOT yet armed: [`Rig::arm`] is
    /// `cli::serve`'s `grow::arm_reach`, which starts the first reading.
    async fn up(name: &str, per_op: Duration, keep_warm: Option<Duration>) -> Rig {
        let dir = std::env::temp_dir().join(format!("monetize-window-reading-{}-{name}", std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();

        let product = Arc::new(MockProduct::gunnar());
        product.grows(vec!["done"], true);
        let growth = Growth::AtRuntime("this appliance grows at runtime".into());
        product.can_serve_growing(37 * GIB, 37 * GIB, growth.clone());
        // Full: the room every row needs is the set's growth.
        product.cannot_absorb("gunnar: the data set is full");
        let mock = Arc::new(MockCloud::new());
        mock.answer_all(Answer::OnHand);
        mock.grows(GrowScript { current_gib: 37, per_gib_minor: 23, ..GrowScript::default() });
        let ops = Arc::new(Ops::default());
        let guest = GuestLike { inner: mock, serial: Mutex::new(()), per_op, ops: ops.clone() };
        let capped = SpendCap::new(guest, Ceiling::monthly(MONTHLY_MINOR, Currency::Eur), dir.join("cap.json")).unwrap();

        let product_dyn: Arc<dyn Product> = product.clone();
        let mut state = AppState::in_memory(Registry::new().shared_product(product_dyn).sells("gunnar", MockPay::new()).cloud_capped(capped));
        *state.terms.write().unwrap() = monetize_server::state::TermsStore::fixture();
        state.signup_max_sold_percents = std::collections::BTreeMap::from([("gunnar".to_string(), 600)]);
        let shop: BTreeMap<String, BundleSpec> = [("10gb".to_string(), bundle(10)), ("50gb".to_string(), bundle(50)), ("100gb".to_string(), bundle(100))].into();
        *state.bundles.write().unwrap() = [("gunnar".to_string(), shop)].into();
        let mut runs = GrowRuns::default();
        runs.keep_warm = keep_warm;
        runs.add("gunnar", GrowSetConfig { cloud: "mockcloud".into(), server: "srv-1".into(), members: vec!["vol-1".into(), "vol-2".into()] }, None, None).unwrap();
        state.grow = runs;
        state.health.set_servable("gunnar", Servable::Measured { servable_bytes: 37 * GIB, total_bytes: 37 * GIB, growth, ceilings: Default::default() }, 0);

        let state = Arc::new(state);
        let server = TestServer::spawn(state.clone(), BEARER).await;
        let channel = Channel::from_shared(server.endpoint()).unwrap().connect().await.expect("connect");
        Rig { state, ops, per_op, server, channel, dir }
    }

    /// `cli::serve`'s arming: the reach answers from here on, and the first
    /// wallet reading starts on its own thread.
    fn arm(&self) {
        monetize_server::grow::arm_reach(&self.state);
    }

    /// One window, as gunnar-ui asks it for the tenant's own account, timed.
    async fn window(&self) -> (Duration, pb::ListBundlesResponse) {
        let began = Instant::now();
        let mut r = Request::new(pb::ListBundlesRequest { product: "gunnar".into(), tenant: "alice".into(), market: 0, actor_ticket: Vec::new() });
        r.metadata_mut().insert("authorization", MetadataValue::try_from(format!("Bearer {BEARER}")).unwrap());
        let mut client = PurchaseClient::new(self.channel.clone());
        let call = client.bundles(r);
        let answer = tokio::time::timeout(Duration::from_secs(60), call).await.expect("the window answered within 60 s").expect("the window is a read; it refuses nothing here").into_inner();
        (began.elapsed(), answer)
    }

    /// Ask as the console does — again every 500 ms while it says `reading` —
    /// until it answers with rows. **Never an empty answer that is not
    /// `reading`**: that is the shape the console falls back to the typed
    /// order on.
    async fn rows(&self, limit: Duration) -> (usize, pb::ListBundlesResponse) {
        let began = Instant::now();
        let mut reading = 0;
        loop {
            let (_, w) = self.window().await;
            if !w.reading {
                assert!(!w.bundles.is_empty(), "an EMPTY window that does not say `reading` after {reading} reading answer(s) — the console draws the typed order on that");
                return (reading, w);
            }
            assert!(w.bundles.is_empty(), "a `reading` window carries no rows: {:?}", w.bundles);
            reading += 1;
            assert!(began.elapsed() < limit, "still `reading` after {:?}", began.elapsed());
            tokio::time::sleep(Duration::from_millis(500)).await;
        }
    }

    /// Wait (bounded) until nothing is being done at the cloud.
    fn quiet(&self) -> Seen {
        let began = Instant::now();
        loop {
            let before = self.ops.seen();
            std::thread::sleep(self.per_op * 3 + Duration::from_millis(300));
            let now = self.ops.seen();
            if now == before || began.elapsed() > Duration::from_secs(60) {
                return now;
            }
        }
    }

    async fn stop(self) {
        self.server.stop().await;
        let _ = std::fs::remove_dir_all(&self.dir);
    }
}

fn all_servable(w: &pb::ListBundlesResponse) {
    let ids: Vec<&str> = w.bundles.iter().map(|b| b.id.as_str()).collect();
    assert_eq!(ids.len(), 3, "the whole shop: {ids:?}");
    assert!(w.bundles.iter().all(|b| b.can_serve), "the set's reach covers every row once the reading is in: {:?}", w.bundles);
}

/// ★ **THE OWNER'S MORNING: the first window while the cloud reading is slow
/// says `reading` AT ONCE, and then the rows — never an empty answer.**
///
/// RED before the fix: the window's reach asked the guest's `grow_gate`, which
/// queued behind the ~6 s reading, and the answer was a plain list with no way
/// to say it had not been read.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_first_window_while_the_cloud_reading_is_slow_says_reading_at_once_then_the_rows() {
    let rig = Rig::up("first", Duration::from_millis(500), None).await;
    rig.arm();
    let (took, first) = rig.window().await;
    eprintln!("MEASURED the first window during a slow reading: {} ms, reading={}, {} row(s)", took.as_millis(), first.reading, first.bundles.len());
    assert!(first.reading, "the window during the reading must SAY it is being read; it answered {} row(s) after {took:?}", first.bundles.len());
    assert!(first.bundles.is_empty());
    assert!(took < Duration::from_millis(3_500), "the window waited {took:?} — behind the reading, as on test2");
    let (reading, w) = rig.rows(Duration::from_secs(30)).await;
    eprintln!("MEASURED {reading} more `reading` answer(s), then {} row(s)", w.bundles.len());
    all_servable(&w);
    rig.stop().await;
}

/// ★ **QUEUE THE QUESTIONS: eight windows asked while the reading runs wait on
/// that ONE reading and all get its rows.** One survey, one gate, one bisect;
/// and the next eight asks cost the cloud nothing.
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
async fn concurrent_windows_wait_on_one_reading_and_all_get_its_rows() {
    let rig = Arc::new(Rig::up("flight", Duration::from_millis(60), None).await);
    rig.arm();
    let asks: Vec<_> = (0..8)
        .map(|_| {
            let rig = rig.clone();
            tokio::spawn(async move { rig.window().await })
        })
        .collect();
    for a in asks {
        let (took, w) = a.await.unwrap();
        assert!(!w.reading, "a reading this short lands inside the wait; this ask was told `reading` after {took:?}");
        all_servable(&w);
    }
    let once = rig.quiet();
    eprintln!("MEASURED eight concurrent windows: {once:?}");
    assert_eq!(once.survey, 1, "ONE survey for eight asks: {once:?}");
    assert!(once.gate <= 1, "the windows queued their own `grow_gate` ops behind the reading: {once:?}");
    assert!(once.price > 0, "the bisect ran (once): {once:?}");
    for _ in 0..8 {
        let (_, w) = rig.window().await;
        all_servable(&w);
    }
    assert_eq!(rig.quiet(), once, "eight more windows inside the cache's life asked the cloud");
    match Arc::try_unwrap(rig) {
        Ok(rig) => rig.stop().await,
        Err(_) => panic!("the rig is still shared"),
    }
}

/// ★ **THE CACHE: ten minutes with no cloud call, then a refresh.** The
/// reading (the survey and the bisect's result) is fresh for 10 minutes and
/// the provider's prices for an hour; within that a window answers from the
/// cache at once. After it, the window still answers at once from the last
/// reading while ONE refresh runs — a survey, the prices from their cache —
/// and past the hour the prices are asked again.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_window_answers_from_its_cache_for_ten_minutes_and_refreshes_after() {
    let rig = Rig::up("ttl", Duration::from_millis(40), None).await;
    rig.arm();
    let (_, w) = rig.rows(Duration::from_secs(30)).await;
    all_servable(&w);
    let made = rig.quiet();
    eprintln!("MEASURED the first reading: {made:?}");

    // 9 min 50 s later: the cache's own answer, nothing asked.
    monetize_server::grow::age_readings_for_test(&rig.state, 9 * MIN + Duration::from_secs(50));
    let (took, w) = rig.window().await;
    all_servable(&w);
    assert!(took < Duration::from_secs(1), "{took:?}");
    assert_eq!(rig.quiet(), made, "a window inside the 10 minutes asked the cloud");

    // 11 min: answered at once from the last reading, ONE refresh behind it —
    // a survey, and the prices still from their cache.
    monetize_server::grow::age_readings_for_test(&rig.state, 70 * Duration::from_secs(1));
    let (took, w) = rig.window().await;
    assert!(!w.reading && took < Duration::from_secs(1), "past 10 minutes the last reading still answers at once: {took:?} reading={}", w.reading);
    all_servable(&w);
    let refreshed = rig.quiet();
    eprintln!("MEASURED the refresh at 11 min: {refreshed:?}");
    assert_eq!(refreshed.survey, made.survey + 1, "past 10 minutes the reading is refreshed: {made:?} → {refreshed:?}");
    assert_eq!(refreshed.price, made.price, "the provider's prices are cached for longer than 10 minutes: {made:?} → {refreshed:?}");

    // Past the hour: the reading is too old to answer from, and so are the
    // prices — asked again.
    monetize_server::grow::age_readings_for_test(&rig.state, 61 * MIN);
    let (_, w) = rig.rows(Duration::from_secs(30)).await;
    all_servable(&w);
    let again = rig.quiet();
    eprintln!("MEASURED past the hour: {again:?}");
    assert_eq!(again.survey, refreshed.survey + 1);
    assert!(again.price > refreshed.price, "past the prices' life the bisect asks the provider again: {refreshed:?} → {again:?}");
    rig.stop().await;
}

/// ★ **WARM, NOT WAITED FOR: the reading is refreshed in the background before
/// it expires**, with nobody asking — so a customer normally never finds the
/// window being read. (`keep_warm` shortened to a second here; the estate's is
/// `KEEP_WARM_EVERY`, inside `WALLET_FRESH`.)
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_keeper_refreshes_the_reading_before_it_expires_without_anybody_asking() {
    assert!(monetize_server::grow::KEEP_WARM_EVERY < monetize_server::grow::WALLET_FRESH, "the keeper refreshes BEFORE the reading expires");
    assert!(monetize_server::grow::WALLET_FRESH >= Duration::from_secs(600), "the owner's floor: at least 10 minutes");
    let rig = Rig::up("keeper", Duration::from_millis(20), Some(Duration::from_secs(1))).await;
    rig.arm();
    let began = Instant::now();
    while rig.ops.seen().survey < 3 {
        assert!(began.elapsed() < Duration::from_secs(20), "no background refresh in 20 s: {:?}", rig.ops.seen());
        tokio::time::sleep(Duration::from_millis(100)).await;
    }
    let (took, w) = rig.window().await;
    assert!(!w.reading && took < Duration::from_secs(1), "{took:?}");
    all_servable(&w);
    rig.stop().await;
}

/// ★ **Round 5.3 (owner 2026-10-08, angry): "the customer must not know about
/// a cap! it must be queued and resized later".** The window offered the
/// 500 GiB row greyed, "Larger than the room left here (…) — ask us to make
/// room": a room left and a cap, told to a customer. Every size is offered
/// now: a row past the room on hand AND past what the spend cap can grow the
/// set by is `can_serve` with no reason, its order is paid and QUEUED for the
/// set's resize, and only the operator is told what waits.
///
/// Was `an_offer_past_the_growable_room_is_marked_with_that_room_and_no_order_refusal`.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn an_offer_past_the_growable_room_is_offered_and_its_order_queued_with_no_room_or_cap_words() {
    let rig = Rig::up("past", Duration::from_millis(10), None).await;
    rig.state.bundles.write().unwrap().get_mut("gunnar").unwrap().insert("500gb".to_string(), bundle(500));
    rig.arm();
    let (_, w) = rig.rows(Duration::from_secs(30)).await;
    let product = rig.state.registry.find_product("gunnar").unwrap();
    let reach = monetize_server::grow::growable_reach(&rig.state, product).expect("the reading is in: the set's growable reach is measured");
    assert!(reach > 0 && 37 * GIB + reach < 500 * GIB, "the rig's shape: some growth, less than 500 GiB in all ({reach} B of growth)");
    assert!(monetize_server::grow::queues_for_resize(&rig.state, product), "the set is resized later: the queue is open");
    for id in ["10gb", "50gb", "100gb", "500gb"] {
        let b = w.bundles.iter().find(|b| b.id == id).unwrap();
        assert!(b.can_serve, "{id} is offered — every size is buyable: {b:?}");
        assert!(b.reason.is_empty(), "{id}: no room, cap or limit words on an offer: {:?}", b.reason);
    }
    // The order itself is SOLD from the box (no iron bought), and queued.
    let q = monetize_server::services::purchase::quote_order_for_test(&rig.state, product, "alice", [("pack_bytes".to_string(), 500 * GIB)].into());
    assert!(q.serve.is_ok(), "past the room and the cap, the order is accepted: {:?}", q.serve);
    assert_eq!(q.purchase.absorb, Some(monetize_product::Absorb::OnHand), "served from the box once the batcher resizes it; nothing is bought for it");
    rig.stop().await;
}