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/signup_start_latency.rs

//! **`Signup.Start` is the moment between "Continue" and Stripe's card form**,
//! and on the real test2 (UpCloud, live Stripe, 2026-09-30) it took 25.5 s, then
//! 33 s, then did not answer at all for more than 100 s. The bench — no grow
//! set, a fake Stripe — answered in 34-75 ms, so nothing on the bench could see it.
//!
//! MEASURED on test2's front, `monetize-server: signup capacity: gunnar: the
//! sale was allowed …` (the oversell gate's `admit`) is the LAST line before the
//! silence, and main's log shows monetize's `Admin.Capacity` answered 50 ms into
//! the request. After that line `admit` asks `oversell::past_the_wallet` — a
//! READING that refuses nothing — and that called `grow::affordable_bytes`
//! inline: a survey and a bisect of `price_grow`, about a dozen ops on the
//! UpCloud cloud GUEST, each op a fresh guest (so a fresh `GET /1.3/price` on
//! top of `GET /server/{uuid}` and one `GET /storage/{uuid}` per member). ~48
//! sequential UpCloud API calls on the customer's path, and every op of the
//! cloud guest queues behind the guest's one `serial` lock — which a growth
//! holds for up to 15 minutes.
//!
//! This file is that estate with latency: the cloud answers each op in
//! [`PER_OP`] (four UpCloud calls at 150 ms), the vendor mints in [`PER_MINT`]
//! (a request plus a TLS handshake at 150 ms each), and the cloud has one lock
//! every op takes, as the guest does.

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

use monetize::testing::{GrowScript, MockCloud, MockProduct};
use monetize_cloud::cap::{Ceiling, SpendCap};
use monetize_cloud::{grow, CloudError, Ensured, Inventory, Price, Resource, ResourceId, ResourceStatus, Room, Zone};
use monetize_pay::signup::{SignupCheckout, SignupOrder};
use monetize_pay::{CallbackEvent, ChargeHandle, ChargeRequest, ChargeStatus, Currency, Market, Money, PayError, RawCallback, Settlement};
use monetize_product::{Growth, Product, Servable};
use monetize_proto::signup_client::SignupClient;
use monetize_proto::{self as pb};
use monetize_server::config::{GrowSetConfig, SignupPlan};
use monetize_server::grow::GrowRuns;
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
use monetize_server::testing::{CallbackPay, TestServer};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;

const BEARER: &str = "test-bearer-token-of-sufficient-length";
const PLAN: &str = "gunnar/10gb-1m";
const GIB: u64 = 1 << 30;
/// One cloud-guest op on the real account: `GET /1.3/server/{uuid}`, two
/// `GET /1.3/storage/{uuid}`, and `GET /1.3/price` (the guest's price book
/// lives one op) — four calls at a realistic 150 ms each.
const PER_OP: Duration = Duration::from_millis(600);
/// Stripe's `POST /v1/checkout/sessions`: one request and one TLS handshake.
const PER_MINT: Duration = Duration::from_millis(300);
/// test2's `[clouds.upcloud] cap`: monthly 50.00 EUR.
const MONTHLY_MINOR: u64 = 5_000;

/// **The cloud guest, as far as latency goes**: every op takes [`PER_OP`] and
/// holds the one lock `GuestCloud::call_within` holds (`serial`).
struct SlowCloud {
    inner: Arc<MockCloud>,
    serial: Arc<Mutex<()>>,
    ops: Arc<AtomicU64>,
}

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

#[allow(dead_code)]
impl SlowCloud {
    pub fn provider(&self) -> &'static str {
        self.inner.provider()
    }
    pub fn zones(&self) -> &[Zone] {
        &[]
    }
    pub fn price_resource(&self, r: &Resource) -> Result<Price, CloudError> {
        self.op(|| monetize_cloud::Cloud::price(&self.inner, &monetize_cloud::cloud::Priced::Resource(r)))
    }
    pub fn ensure(&self, reference: &str, r: &Resource) -> Result<Ensured, CloudError> {
        self.op(|| self.inner.ensure(reference, r))
    }
    pub fn ensure_with(&self, reference: &str, r: &Resource, room: Room) -> Result<Ensured, CloudError> {
        self.op(|| self.inner.ensure_with(reference, r, room))
    }
    pub fn destroy_id(&self, id: &ResourceId) -> Result<(), CloudError> {
        self.op(|| monetize_cloud::ask::destroy_one(&self.inner, id))
    }
    pub fn status(&self, id: &ResourceId) -> Result<ResourceStatus, CloudError> {
        self.op(|| self.inner.status(id))
    }
    pub fn list_owned(&self) -> Result<Inventory, CloudError> {
        self.op(|| monetize_cloud::ask::account(&self.inner))
    }
    /// PURE in the guest too (the block's `allow_grow`): no op, no lock.
    pub fn grow_gate(&self) -> Result<(), CloudError> {
        self.inner.grow_gate()
    }
    pub fn price_grow(&self, set: &grow::DataSet) -> Result<Price, CloudError> {
        self.op(|| self.inner.price_grow(set))
    }
    pub fn survey_set(&self, set: &grow::DataSet) -> Result<grow::SetSurvey, CloudError> {
        self.op(|| self.inner.survey_set(set))
    }
    pub fn grow_set(&self, set: &grow::DataSet, journal: &grow::Journal) -> Result<grow::GrowOutcome, CloudError> {
        self.op(|| self.inner.grow_set(set, journal))
    }
    pub fn resume_grow(&self, set: &grow::DataSet, journal: &grow::Journal) -> Result<grow::GrowOutcome, CloudError> {
        self.op(|| self.inner.resume_grow(set, journal))
    }
}
#[allow(unused_variables, unreachable_code)]
impl monetize_cloud::cloud::Cloud for SlowCloud {
    fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
        monetize_cloud::cloud::CloudManifest { provider: self.provider().to_string(), zones: self.zones().to_vec(), settings: Vec::new(), grow_refused: self.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,
        })
    }
}

/// **Stripe, with a round trip.** Everything but the mint is the plain double.
struct SlowVendor {
    inner: CallbackPay,
    mint: Duration,
}

#[allow(dead_code)]
impl SlowVendor {
    pub fn vendor(&self) -> &'static str {
        self.inner.vendor()
    }
    pub fn markets(&self) -> &[Market] {
        self.inner.markets()
    }
    pub fn reserve(&self, req: &ChargeRequest) -> Result<ChargeHandle, PayError> {
        self.inner.reserve(req)
    }
    pub fn draw(&self, h: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.inner.draw(h)
    }
    pub fn unlock(&self, h: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.inner.unlock(h)
    }
    pub fn charge_status(&self, h: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.inner.charge_status(h)
    }
    pub fn parse_callback(&self, raw: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
        self.inner.parse_callback(raw)
    }
    pub fn give_back(&self, h: &ChargeHandle, amount: Money) -> Result<ChargeStatus, PayError> {
        self.inner.give_back(h, amount)
    }
    pub fn settled(&self, h: &ChargeHandle) -> Option<Settlement> {
        self.inner.settled(h)
    }
    pub fn redirect(&self, h: &ChargeHandle) -> Option<String> {
        self.inner.redirect(h)
    }
    pub fn start_signup(&self, order: &SignupOrder) -> Result<Option<SignupCheckout>, PayError> {
        std::thread::sleep(self.mint);
        self.inner.start_signup(order)
    }
}
#[allow(unused_variables)]
impl monetize_pay::payment::Payment for SlowVendor {
    fn manifest(&self) -> monetize_pay::payment::PayManifest {
        monetize_pay::payment::PayManifest { vendor: self.vendor().to_string(), markets: self.markets().to_vec(), settings: Vec::new(), holds: true, test_payer: None }
    }
    fn page(&self, charge: &monetize_pay::payment::Charge<'_>, back: &monetize_pay::payment::Return) -> Result<monetize_pay::payment::Page, monetize_pay::PayError> {
        Ok(match charge {
            monetize_pay::payment::Charge::Order(req) => monetize_pay::payment::Page::Order { handle: self.reserve(req)?, url: None },
            monetize_pay::payment::Charge::Signup(o) => monetize_pay::payment::Page::Signup(self.start_signup(o)?),
            monetize_pay::payment::Charge::CardCheck(o) => monetize_pay::payment::Page::CardCheck(None),
            monetize_pay::payment::Charge::TestSignup(o) => monetize_pay::payment::Page::TestSignup(None),
        })
    }
    fn status(&self, of: &monetize_pay::payment::Of<'_>) -> Result<monetize_pay::payment::Standing, monetize_pay::PayError> {
        Ok(match of {
            monetize_pay::payment::Of::Charge(h) => monetize_pay::payment::Standing::Charge(self.charge_status(h)?),
            monetize_pay::payment::Of::Page(h) => monetize_pay::payment::Standing::Page(self.redirect(h)),
            monetize_pay::payment::Of::Signup { session } => monetize_pay::payment::Standing::Signup(None),
            monetize_pay::payment::Of::CardCheck { session } => monetize_pay::payment::Standing::CardCheck(None),
        })
    }
    fn capture(&self, h: &monetize_pay::ChargeHandle) -> Result<monetize_pay::ChargeStatus, monetize_pay::PayError> {
        self.draw(h)
    }
    fn release(&self, h: &monetize_pay::ChargeHandle) -> Result<monetize_pay::ChargeStatus, monetize_pay::PayError> {
        self.unlock(h)
    }
    fn refund(&self, h: &monetize_pay::ChargeHandle, amount: monetize_pay::Money) -> Result<monetize_pay::ChargeStatus, monetize_pay::PayError> {
        self.give_back(h, amount)
    }
    fn callback(&self, raw: monetize_pay::RawCallback<'_>) -> Result<monetize_pay::CallbackEvent, monetize_pay::PayError> {
        self.parse_callback(raw)
    }
    fn settlement(&self, h: &monetize_pay::ChargeHandle) -> Option<monetize_pay::Settlement> {
        self.settled(h)
    }
    fn paid_since(&self, since_unix_ms: i64) -> Result<Vec<monetize_pay::reconcile::PaidAtVendor>, monetize_pay::PayError> {
        Err(monetize_pay::reconcile::cannot_be_asked())
    }
    fn livemode(&self) -> Option<bool> {
        None
    }
}

/// A sink that holds nobody, so every name is free.
struct FreeSink;
impl monetize_pay::SignupSink for FreeSink {
    fn paid(&self, s: &monetize_pay::PaidSignup) -> Result<monetize_pay::SignupOutcome, monetize_pay::SignupError> {
        Ok(monetize_pay::SignupOutcome::Created { namespace: s.username.clone(), username: s.username.clone() })
    }
    fn name_verdict(&self, _: &str) -> Result<monetize_pay::NameVerdict, monetize_pay::SignupError> {
        Ok(monetize_pay::NameVerdict::Free)
    }
}

struct Rig {
    state: Arc<AppState>,
    product: Arc<MockProduct>,
    pay: CallbackPay,
    serial: Arc<Mutex<()>>,
    ops: Arc<AtomicU64>,
    server: TestServer,
    channel: Channel,
    dir: std::path::PathBuf,
}

impl Rig {
    /// test2's front: gunnar linked, a 37 GiB two-member set the UpCloud guest
    /// can grow under a 50 EUR monthly cap, Stripe as the vendor — and the
    /// state ARMED exactly as `cli::serve` arms it (`grow::arm_reach`).
    async fn test2(name: &str) -> Rig {
        let dir = std::env::temp_dir().join(format!("monetize-signup-latency-{}-{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 mock = Arc::new(MockCloud::new());
        mock.grows(GrowScript { current_gib: 37, per_gib_minor: 23, ..GrowScript::default() });
        let serial = Arc::new(Mutex::new(()));
        let ops = Arc::new(AtomicU64::new(0));
        let slow = SlowCloud { inner: mock, serial: serial.clone(), ops: ops.clone() };
        let capped = SpendCap::new(slow, Ceiling::monthly(MONTHLY_MINOR, Currency::Eur), dir.join("cap.json")).unwrap();

        let pay = CallbackPay::with_checkout("stripe-shared-secret", "https://checkout.stripe.com/c/pay/");
        let vendor = SlowVendor { inner: pay.clone(), mint: PER_MINT };
        let product_dyn: Arc<dyn Product> = product.clone();
        let mut state = AppState::in_memory(Registry::new().sells("gunnar", vendor).shared_product(product_dyn).cloud_capped(capped));
        state.signup_plans = [(PLAN.to_string(), SignupPlan { amount_minor: 10_000, currency: Currency::Sek, cap_bytes: 10 * GIB, period_days: 30 })].into();
        state.signup_sink = Arc::new(FreeSink);
        state.signup_max_sold_percents = std::collections::BTreeMap::from([("gunnar".to_string(), 600)]);
        let mut runs = GrowRuns::default();
        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;
        // The poller's measurement: 37 GiB, all free, and the set grows at runtime.
        let growth = Growth::AtRuntime("this appliance grows at runtime".into());
        product.can_serve_growing(37 * GIB, 37 * GIB, growth.clone());
        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);
        monetize_server::grow::arm_reach(&state);
        let server = TestServer::spawn(state.clone(), BEARER).await;
        let channel = Channel::from_shared(server.endpoint()).unwrap().connect().await.expect("connect");
        Rig { state, product, pay, serial, ops, server, channel, dir }
    }

    fn client(&self) -> SignupClient<tonic::service::interceptor::InterceptedService<Channel, impl tonic::service::Interceptor>> {
        let bearer: MetadataValue<_> = format!("Bearer {BEARER}").parse().unwrap();
        SignupClient::with_interceptor(self.channel.clone(), move |mut r: Request<()>| {
            r.metadata_mut().insert("authorization", bearer.clone());
            Ok(r)
        })
    }

    /// One `Signup.Start` as the console sends it, timed, and cut off at
    /// `limit` so a RED run says how long it waited instead of hanging.
    async fn start(&self, user: &str, limit: Duration) -> (Duration, Result<pb::StartSignupResponse, tonic::Status>) {
        let began = Instant::now();
        let mut client = self.client();
        let call = client.start(pb::StartSignupRequest { product: "gunnar".into(),
            username: user.into(),
            email: format!("{user}@example.com"),
            success_url: "https://test2.gunnar.rs/welcome?session={session}".into(),
            cancel_url: "https://test2.gunnar.rs/signup".into(),
            ..Default::default()
        });
        let out = match tokio::time::timeout(limit, call).await {
            Ok(r) => r.map(|r| r.into_inner()),
            Err(_) => Err(tonic::Status::deadline_exceeded(format!("the test gave up after {limit:?}"))),
        };
        (began.elapsed(), out)
    }

    /// Wait (bounded) until nothing is being done at the cloud — so a
    /// measurement starts from a quiet estate.
    fn quiet(&self) {
        let began = Instant::now();
        loop {
            let before = self.ops.load(Ordering::SeqCst);
            std::thread::sleep(PER_OP * 2);
            if self.ops.load(Ordering::SeqCst) == before || began.elapsed() > Duration::from_secs(60) {
                return;
            }
        }
    }

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

/// ★ **THE CUSTOMER IS NOT MADE TO WAIT FOR A READING THAT REFUSES NOTHING.**
///
/// RED before the fix: 7.5 s on this bench (12 cloud ops × 600 ms, then the
/// mint), 25-33 s on test2's real UpCloud API. The target is under 2 s.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn signup_start_on_a_slow_cloud_answers_in_under_two_seconds() {
    let rig = Rig::test2("slow").await;
    rig.quiet();
    let (took, out) = rig.start("henrik", Duration::from_secs(60)).await;
    eprintln!("MEASURED signup_start on a slow cloud: {} ms", took.as_millis());
    let out = out.expect("a box with room sells");
    assert!(out.url.starts_with("https://checkout.stripe.com/"), "{}", out.url);
    assert!(took < Duration::from_secs(2), "Signup.Start took {took:?} on a cloud that answers an op in {PER_OP:?}");
    assert_eq!(rig.pay.signup_orders().len(), 1);
    rig.stop().await;
}

/// ★ **A CLOUD BUSY WITH A GROWTH DOES NOT HOLD THE CUSTOMER.**
///
/// The owner's third live signup (22:23:17) never answered: nothing after the
/// capacity line for more than 100 s. A growth holds the cloud guest's one lock
/// for as long as the provider takes to detach, resize and re-attach — minutes.
/// Here the lock is held for 30 s; the signup must still mint in under 2 s.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_cloud_busy_with_a_growth_does_not_hold_a_signup() {
    let rig = Rig::test2("busy").await;
    rig.quiet();
    let serial = rig.serial.clone();
    let (held_tx, held_rx) = std::sync::mpsc::channel();
    let growth = std::thread::spawn(move || {
        let _g = serial.lock().unwrap();
        held_tx.send(()).unwrap();
        std::thread::sleep(Duration::from_secs(30));
    });
    held_rx.recv().unwrap();
    // The reading goes stale while the growth runs: the next customer is the
    // one who would have asked the cloud.
    std::thread::sleep(monetize_server::grow::REACH_TTL.min(Duration::from_secs(1)));
    monetize_server::grow::expire_readings_for_test(&rig.state);
    let (took, out) = rig.start("olle", Duration::from_secs(20)).await;
    eprintln!("MEASURED signup_start while a growth holds the cloud: {} ms", took.as_millis());
    let out = out.expect("a box with room sells while the set grows");
    assert!(out.url.starts_with("https://checkout.stripe.com/"), "{}", out.url);
    assert!(took < Duration::from_secs(2), "Signup.Start waited {took:?} behind a growth");
    drop(growth);
    rig.stop().await;
}

/// **The reading still reaches the operator** — off the customer's path. The
/// RED `shop-past-the-wallet` row (STINK S2) is only worth anything if it is
/// still computed: here the background refresh fills it, and the number is
/// the same arithmetic as before (37 GiB, two members, 50 EUR a month at 23
/// EUR-minor a GiB-month: 108 GiB a member, 216 GiB for the set).
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_wallet_reading_is_still_made_off_the_customers_path() {
    let rig = Rig::test2("reading").await;
    let began = Instant::now();
    let affordable = loop {
        match monetize_server::grow::wallet_reading(&rig.state, "gunnar") {
            monetize_server::grow::Wallet::Read(Some(b)) => break b,
            _ if began.elapsed() > Duration::from_secs(60) => panic!("the wallet reading never arrived"),
            _ => std::thread::sleep(Duration::from_millis(100)),
        }
    };
    assert_eq!(affordable / GIB, 216, "{affordable}");
    let gap = monetize_server::oversell::past_the_wallet(&rig.state, "gunnar", 400 * GIB, 37 * GIB).expect("400 GiB of caps is past 37 + 216");
    assert_eq!(gap.over / GIB, 400 - 37 - 216, "{gap:?}");
    rig.stop().await;
}

/// **A FULL box reads the reach as a SNAPSHOT** (owner 2026-09-30: a signup's
/// capacity decision reads room on hand + promised + growing and never waits
/// for a growth). The set's growth is what makes room here, so the reach
/// decides the sale — from the last reading, while a growth holds the cloud.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_full_box_sells_on_the_reach_snapshot_while_a_growth_holds_the_cloud() {
    let rig = Rig::test2("full").await;
    rig.quiet();
    *rig.product.refuse_absorb.lock().unwrap() = Some("gunnar: the data set is full".into());
    let serial = rig.serial.clone();
    let (held_tx, held_rx) = std::sync::mpsc::channel();
    let growth = std::thread::spawn(move || {
        let _g = serial.lock().unwrap();
        held_tx.send(()).unwrap();
        std::thread::sleep(Duration::from_secs(30));
    });
    held_rx.recv().unwrap();
    monetize_server::grow::expire_readings_for_test(&rig.state);
    let (took, out) = rig.start("pelle", Duration::from_secs(20)).await;
    eprintln!("MEASURED signup_start on a full box while a growth holds the cloud: {} ms", took.as_millis());
    let out = out.expect("the last reading's reach (216 GiB) covers a 10 GiB plan on a full box");
    assert!(out.url.starts_with("https://checkout.stripe.com/"), "{}", out.url);
    assert!(took < Duration::from_secs(2), "a full box held the customer {took:?}");
    drop(growth);
    rig.stop().await;
}