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/src/estate_order/tests/site_dropped.rs

//! ★ **PROD-FIX 2 (prod ONE box 2026-10-26 17:19Z)**: `estate order estate/test3holgerrs:
//! REFUSED [estate-undeclared] test3-holger-rs is not a site this monetize sells GUNNAR for`,
//! every 2 s. A HOLGER site's run was looked up as GUNNAR: once its site left the feed, the
//! run's product fell back to the shop's default. And the refusal was asked again on every
//! tick. Now the run's product comes from its own site and order, never a default; a refusal
//! of that kind is final: the run is stopped by name, marked, ALERTed once, and looked at again
//! only after a back-off (or once the feed declares its site again).

use super::*;

/// A shop that sells Gunnar with Holger sites: `holger-rs` stays declared, `test3-holger-rs`
/// leaves the feed after its estate was laid.
fn dropped_rig(name: &str) -> Rig {
    let dir = tmp(name);
    let pay = HoldPay::default();
    let cloud = Arc::new(MockCloud::new());
    let kit = FakeKit::new("PASS");
    let site = |product: &str, site: &str| crate::config::EstateConfig {
        product: product.into(),
        vendor: "holdpay".into(),
        site: site.into(),
        ..Default::default()
    };
    let reg = Registry::new()
        .sells("gunnar", pay.clone())
        .cloud(cloud.clone())
        .shared_product(shop_product())
        .home("gunnar", "gunnar-rs")
        .estate("gunnar-rs", site("gunnar", "gunnar.rs"))
        .estate("holger-rs", site("holger", "holger.rs"))
        .estate("test3-holger-rs", site("holger", "test3.holger.rs"));
    let (state, clock, mails, health) = build_in(
        reg,
        &dir,
        EstateOrderConfig {
            vendor: String::new(),
            apex: String::new(),
            lifecycle: "gunnar".into(),
            ..cfg()
        },
        kit.clone(),
        None,
        None,
    );
    let r = Rig {
        state,
        pay,
        cloud,
        kit,
        dir,
        clock,
        mails,
        health,
    };
    start_on(
        &r.state,
        Some("test3-holger-rs"),
        "test3holgerrs",
        "ops@mock.invalid",
        "",
        "ignalina",
        &EstateSizes::default(),
    )
    .unwrap();
    r.pay.authorize(&handle_of(&r, "test3holgerrs"));
    drive(&r.state, "test3holgerrs");
    drive(&r.state, "test3holgerrs");
    assert_eq!(
        r.state.estates.get("test3holgerrs").unwrap().phase,
        phase::CAPTURED
    );
    // The feed no longer declares the site.
    r.state.registry.forget_estate("test3-holger-rs");
    r
}

/// ★ RED: the run's refusal names ITS product (holger), never the shop's default.
#[test]
fn a_run_whose_site_left_the_feed_is_refused_as_its_own_product_never_the_shops_default() {
    let r = dropped_rig("site-dropped-product");
    let run = r.state.estates.get("test3holgerrs").unwrap();
    let why = terms_of(&r.state, r.state.estates.cfg.as_ref().unwrap(), &run)
        .expect_err("its site is gone");
    assert!(
        why.contains("[estate-undeclared]") && why.contains("sells holger for"),
        "the Holger site's run is looked up as Holger: {why}"
    );
    assert!(!why.contains("gunnar"), "never as the shop's Gunnar: {why}");
}

/// ★ RED: the refusal is FINAL — a minute of 2 s ticks stops the run by name ONCE (marked,
/// one ALERT, said once), never asks again on every poll; the back-off looks again later, and
/// a site declared again resumes it.
#[test]
fn a_final_refusal_stops_its_run_once_alerts_once_and_backs_off() {
    let r = dropped_rig("site-dropped-stop");
    for _ in 0..30 {
        r.clock.fetch_add(2_000, Ordering::SeqCst);
        tick(&r.state);
    }
    let run = r.state.estates.get("test3holgerrs").unwrap();
    assert!(
        run.why.contains("[estate-undeclared]") && run.why.contains("stopped"),
        "the run is marked: {}",
        run.why
    );
    let alerts: Vec<_> = run
        .history
        .iter()
        .filter(|t| t.note.contains("ALERT"))
        .collect();
    assert_eq!(
        alerts.len(),
        1,
        "ALERTed once in a minute of ticks: {:#?}",
        run.history
    );
    assert_eq!(
        run.phase,
        phase::CAPTURED,
        "its phase is kept: nothing of it is given back or billed by the stop"
    );
    assert!(
        !r.state.estates.terms_said.lock().unwrap().is_empty()
            || run.history.iter().any(|t| t.note.contains("stopped")),
        "said once"
    );
    // The feed declares the site again: the next look after the back-off resumes the run.
    r.state.registry.declare_estate(
        "test3-holger-rs",
        crate::config::EstateConfig {
            product: "holger".into(),
            vendor: "holdpay".into(),
            site: "test3.holger.rs".into(),
            ..Default::default()
        },
    );
    r.clock.fetch_add(11 * 60_000, Ordering::SeqCst);
    tick(&r.state);
    let run = r.state.estates.get("test3holgerrs").unwrap();
    assert!(
        !run.why.contains("stopped"),
        "resumed once its site is declared again: {}",
        run.why
    );
}

/// ★ **OVW-2 (prod 2026-10-29): a GONE run's terms are never asked** — prod logged `REFUSED
/// [estate-undeclared] test3-holger-rs` once per process: the run of estate/test3holgerrs,
/// terminated on 2026-10-23, is kept, its site left the feed, and every tick's booking of every
/// run (`book`) still asked its terms. A gone run has nothing more to bill: the ticks say
/// nothing of it, stop nothing and book no row for it.
#[test]
fn a_gone_runs_terms_are_never_asked_once_its_site_left_the_feed() {
    let r = dropped_rig("site-dropped-gone");
    {
        let _pen = r.state.estates.pen("test3holgerrs");
        let mut run = r.state.estates.get("test3holgerrs").unwrap();
        run.phase = phase::TERMINATED.into();
        run.history.push(Transition {
            phase: phase::TERMINATED.into(),
            at_ms: r.state.now(),
            note: "terminated by the operator".into(),
        });
        save(&r.state, &run).unwrap();
    }
    let rows = |r: &Rig| -> usize {
        r.state
            .ledger
            .references_with_prefix("")
            .unwrap()
            .iter()
            .map(|re| r.state.ledger.rows(re).unwrap().len())
            .sum()
    };
    let before = rows(&r);
    for _ in 0..10 {
        r.clock.fetch_add(2_000, Ordering::SeqCst);
        tick(&r.state);
    }
    let said = r.state.estates.terms_said.lock().unwrap().clone();
    assert!(
        !said.contains_key("estate/test3holgerrs"),
        "a gone run's terms are not asked: {said:?}"
    );
    let run = r.state.estates.get("test3holgerrs").unwrap();
    assert!(
        run.stopped_why.is_empty() && run.phase == phase::TERMINATED,
        "a gone run is neither stopped nor moved: {run:?}"
    );
    assert_eq!(
        rows(&r),
        before,
        "no row is booked for a gone run whose site left the feed"
    );
}