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;
}