crates/monetize-server/src/estate_order/tests/paired_update.rs
//! **F7 (2026-10-20): a paired estate takes an estate update.** test3-holger-rs was adopted by
//! F5's re-check into `paired`; its main runs r6 and can never report (the report-when-fed fix is
//! in r10). The update was refused as "not delivered", and it could not be delivered without it.
//!
//! * **Paired takes the update**: an estate whose every box PAIRED (the installer's PASS) or
//! that a re-check adopted takes the keep or live update of its boxes to the current release,
//! its data kept — the way a delivered estate does. It is no delivery and sends no ready mail;
//! the installed path (the main's report, the sale check, captured, ready) goes on after it.
//! * **An install still running is refused**, before any box is touched.
//! * **The capture deadline counts from the adoption** (and from the update's end), never while
//! the update runs: an estate adopted near its deadline still captures after its update.
//! * Gunnar's and Holger's alike: one lifecycle. A delivered estate is unchanged.
use super::recheck::{answers, install_failed_on, site_of};
use super::*;
const MINUTE: u64 = 60_000;
const HOUR: u64 = 60 * MINUTE;
/// The site's cloud name.
pub(super) const SITE: &str = "gunnarrs";
/// A site's release (`r10`): its media, both keep media, and the live apps of both kinds.
pub(super) fn site_release(release: &str) -> EstateOrderConfig {
release_cfg(release, true)
}
/// The estate's own client key, as the kit handed it to its front.
pub(super) fn front_key(kit: &FakeKit) -> monetize::Signer {
let hexed = kit
.handed
.lock()
.unwrap()
.get(crate::custody::handoff::ESTATE_CLIENT)
.cloned()
.expect("the estate was handed its client key");
let text = String::from_utf8(hexed).unwrap();
let seed: Vec<u8> = (0..text.trim().len())
.step_by(2)
.map(|i| u8::from_str_radix(&text.trim()[i..i + 2], 16).unwrap())
.collect();
monetize::Signer::from_bytes(&seed.try_into().expect("32 bytes"))
}
pub(super) fn call(
clock: &AtomicU64,
key: &monetize::Signer,
company: &str,
name: &str,
members: &[&str],
gib: u64,
carries: &str,
) -> (i64, Vec<u8>) {
let at = clock.load(Ordering::SeqCst) as i64;
let c = monetize::sign::EstateCall {
v: 1,
call: name.into(),
estate: company.into(),
members: members.iter().map(|m| m.to_string()).collect(),
gib,
at_unix_ms: at,
carries: carries.into(),
};
(at, key.sign_estate_call(&c))
}
pub(super) fn take_as(
state: &AppState,
clock: &AtomicU64,
key: &monetize::Signer,
company: &str,
) -> update::TakeAnswer {
let (at, sig) = call(clock, key, company, update::CALL_UPDATE_TAKE, &[], 0, "");
update::take(state, company, at, &sig).expect("the front's take")
}
fn report_as(
state: &AppState,
clock: &AtomicU64,
key: &monetize::Signer,
company: &str,
role: &str,
outcome: &update::Outcome,
) {
let carries = update::report_carries(outcome, "");
let (at, sig) = call(
clock,
key,
company,
update::CALL_UPDATE_REPORT,
&[role],
0,
&carries,
);
update::report(state, company, role, &carries, at, &sig)
.unwrap_or_else(|e| panic!("{role}: {e:?}"));
}
/// The front, main and twin's keep path: the shop keep-boots `role`, the front re-admits it.
fn keep_upgrade(
state: &AppState,
clock: &AtomicU64,
key: &monetize::Signer,
company: &str,
role: &str,
) -> Result<sold::UpgradeAnswer, sold::SoldRefusal> {
let (at, sig) = call(clock, key, company, sold::CALL_UPGRADE, &[role], 0, "");
sold::upgrade(state, company, role, false, at, &sig)
}
/// Every box of `company` made to run an older release than the one sold (test3: r6).
pub(super) fn runs_older(state: &AppState, company: &str) {
let _pen = state.estates.pen(company);
let mut run = state.estates.get(company).unwrap();
for role in update::ORDER {
run.release_of.insert(role.into(), "r6-old".into());
}
save(state, &run).unwrap();
}
/// The update to the current release, twin and main and front, the main through its keep medium.
pub(super) fn update_every_box(
state: &AppState,
clock: &AtomicU64,
key: &monetize::Signer,
company: &str,
estate: &str,
app: &str,
) {
let u = update::start(state, estate, "", "operator").expect("a paired estate takes the update");
assert_eq!(u.release, format!("r10-{app}"), "{}", u.line());
finish_every_box(state, clock, key, company, app);
}
/// The open update done box by box: twin live, main through its keep medium, front live.
pub(super) fn finish_every_box(
state: &AppState,
clock: &AtomicU64,
key: &monetize::Signer,
company: &str,
app: &str,
) {
// Its front keeps its installer material (F8): the front drives every box. A front that
// never took it is the ONE box's to drive (F9, tests/custody_step.rs).
{
let _pen = state.estates.pen(company);
let mut run = state.estates.get(company).unwrap();
run.material_taken_at_ms = run.material_taken_at_ms.max(1);
save(state, &run).unwrap();
}
let t = take_as(state, clock, key, company);
assert!(t.open && t.role == "twin", "{t:?}");
report_as(
state,
clock,
key,
company,
"twin",
&update::Outcome::Done("live".into()),
);
assert_eq!(take_as(state, clock, key, company).role, "main");
let up = keep_upgrade(state, clock, key, company, "main")
.expect("the shop keep-boots the main of a paired estate");
assert!(up.upgraded, "{}", up.words);
report_as(
state,
clock,
key,
company,
"main",
&update::Outcome::Done("keep".into()),
);
report_as(
state,
clock,
key,
company,
"front",
&update::Outcome::Done("live".into()),
);
let run = state.estates.get(company).unwrap();
assert!(run.update.is_none(), "closed");
assert_eq!(
run.updates.last().unwrap().state,
update::state::DONE,
"{}",
run.updates.last().unwrap().line()
);
assert_eq!(
run.release_of.get("main").map(String::as_str),
Some(format!("r10-{app}").as_str())
);
}
/// ★ **An adopted paired estate takes an estate update, then goes to ready once its main
/// reports** — adopted near its capture deadline, which it outlives: the same boxes, nothing
/// given back, no ready mail before its main reports, one after.
#[test]
fn an_adopted_paired_estate_is_updated_then_ready_once_its_main_reports_even_near_its_deadline() {
let (state, cloud, kit, mails, clock) =
install_failed_on("f7-adopted", true, site_release("r10"));
let key = front_key(&kit);
runs_older(&state, SITE);
// A failed install is no estate to update.
match update::start(&state, "gunnar-rs", "", "operator") {
Err(sold::SoldRefusal::Refused(w)) => assert!(w.contains("not delivered (failed)"), "{w}"),
other => panic!("{other:?}"),
}
// Adopted 23 h 30 min after its lay: half an hour before the deadline the lay set.
clock.fetch_add(22 * HOUR + 29 * MINUTE, Ordering::SeqCst);
answers(&kit, true);
assert_eq!(
settle(&state, "gunnar-rs", RECHECK).expect("adopted").phase,
phase::PAIRED
);
crate::estate_order::drive(&state, SITE);
assert_eq!(
state.estates.get(SITE).unwrap().phase,
phase::PAIRED,
"its main never reports on r6"
);
assert_eq!(
state.estates.get(SITE).unwrap().client_key,
key.public_key()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>(),
"the client key its stopped install never wrote is named again"
);
assert!(
!take_as(&state, &clock, &key, SITE).open,
"the front has asked before: nothing open"
);
let keep_before = cloud.keep_booted.lock().unwrap().len();
let u =
update::start(&state, "gunnar-rs", "", "operator").expect("the operator's update is taken");
assert_eq!(u.release, "r10-gunnar", "{}", u.line());
// The update runs past the lay's deadline: the boxes are never given back meanwhile.
clock.fetch_add(2 * HOUR, Ordering::SeqCst);
crate::estate_order::drive(&state, SITE);
let run = state.estates.get(SITE).unwrap();
assert_eq!(run.phase, phase::PAIRED, "{:#?}", run.history);
assert_eq!(
run.update.as_ref().map(|u| u.release.clone()),
Some(u.release.clone()),
"the update still runs"
);
finish_every_box(&state, &clock, &key, SITE, "gunnar");
assert_eq!(
cloud.keep_booted.lock().unwrap().len(),
keep_before + 1,
"the main, from the keep medium, its data kept"
);
// No delivery and no ready mail from the update: it waits for its main's report.
clock.fetch_add(2 * HOUR, Ordering::SeqCst);
crate::estate_order::drive(&state, SITE);
let run = state.estates.get(SITE).unwrap();
assert_eq!(run.phase, phase::PAIRED, "{:#?}", run.history);
assert_eq!(mails.count("estate.ready"), 0, "{:?}", mails.kinds());
// The main on r10 reports: the sale check, captured, ready, its mail once.
let g = state.fleet.guardian().unwrap();
g.store
.record_report("gunnar-rs", "main", state.now(), &running("main"))
.unwrap();
crate::estate_order::drive(&state, SITE);
let run = state.estates.get(SITE).unwrap();
assert_eq!(run.phase, phase::CAPTURED, "{:#?}", run.history);
assert_eq!(site_of(&state).state, "ready");
assert_eq!(mails.count("estate.ready"), 1, "{:?}", mails.kinds());
assert_eq!(cloud.lays().len(), 1, "no new machine was laid");
assert!(
cloud.destroys().is_empty(),
"nothing was given back: {:?}",
cloud.destroys()
);
}
/// ★ **An installing estate whose boxes are not paired is still refused**, before any box is
/// touched; its install then goes on.
#[test]
fn an_installing_estate_is_refused_an_update() {
let (state, pay, cloud, kit, _dir, _clock, _mails) =
site_rig_with_cfg("f7-installing", true, site_release("r10"));
let run = site::start_site(
&state,
"gunnar-rs",
"ops@example.org",
&EstateSizes::default(),
)
.unwrap();
pay.authorize(&run.handle.clone().unwrap().id);
kit.install_gate.arm();
let s = state.clone();
let driver = std::thread::spawn(move || super::drive(&s, SITE));
kit.install_gate.reached();
assert_eq!(state.estates.get(SITE).unwrap().phase, phase::INSTALLING);
let calls_before = cloud.calls.lock().unwrap().len();
for r in [
update::start(&state, "gunnar-rs", "", "operator"),
update::start_front_first(&state, "gunnar-rs", "", "operator"),
] {
match r {
Err(sold::SoldRefusal::Refused(w)) => assert!(
w.contains("not delivered (installing)") && w.contains("still running"),
"{w}"
),
other => panic!("{other:?}"),
}
}
assert_eq!(
cloud.calls.lock().unwrap().len(),
calls_before,
"no box was touched, not even its firewall"
);
assert!(state.estates.get(SITE).unwrap().update.is_none());
kit.install_gate.open();
driver.join().unwrap();
assert_eq!(
state.estates.get(SITE).unwrap().phase,
phase::CAPTURED,
"its install went on"
);
}
/// ★ **A delivered estate is unchanged**: its update runs as before, it stays captured, and no
/// ready mail is sent again.
#[test]
fn a_delivered_estate_is_updated_as_before() {
let r = delivered("f7-delivered", release_cfg("r10", true));
let key = client_of(&r, "acme");
runs_older(&r.state, "acme");
let ready = r.mails.count("estate.ready");
assert!(!take(&r, &key).unwrap().open);
update_every_box(&r.state, &r.clock, &key, "acme", "acme", "gunnar");
drive(&r.state, "acme");
let run = r.state.estates.get("acme").unwrap();
assert_eq!(run.phase, phase::CAPTURED, "{:#?}", run.history);
assert_eq!(r.mails.count("estate.ready"), ready, "no second ready mail");
}
/// ★ **Holger, one lifecycle**: a Holger estate paired by its installer's PASS (every box PAIRED)
/// whose boxes have not reported takes the update, its main keep-booted from `holger-keep`, and
/// is captured once they report.
#[test]
fn a_paired_holger_estate_takes_the_update_then_is_captured_once_it_reports() {
let mut c = EstateOrderConfig {
lifecycle: "holger".into(),
..cfg()
};
c.media = [
("front", "front"),
("holger-front", "holger-front"),
("holger", "holger"),
("holger-keep", "holger-keep"),
("holger-front-keep", "holger-front-keep"),
]
.into_iter()
.map(|(k, v)| (k.to_string(), format!("r10-{v}")))
.collect();
for kind in ["holger", "holger-front"] {
c.live_apps.insert(
kind.into(),
update::LiveRecord {
base: "b".repeat(64),
app: "a".repeat(64),
app_len: 9,
sig: "5".repeat(128),
..Default::default()
},
);
c.media_objects.insert(
format!("{kind}{}", update::LIVE_SUFFIX),
monetize_cloud::lay::MediaObject {
url: format!("https://objsto.example/release/{kind}/r10.live-app.znippy"),
sha256: "c".repeat(64),
bytes: 9,
},
);
}
let r = rig_with("f7-holger-paired", c, "PASS");
r.kit.silent.store(true, Ordering::SeqCst);
start_for(
&r.state,
"acme",
"buyer@example.org",
"",
"alice",
&EstateSizes::default(),
)
.unwrap();
r.pay.authorize(&handle_of(&r, "acme"));
crate::estate_order::drive(&r.state, "acme");
assert_eq!(
r.state.estates.get("acme").unwrap().phase,
phase::PAIRED,
"no box reported yet"
);
let key = client_of(&r, "acme");
runs_older(&r.state, "acme");
assert!(!take(&r, &key).unwrap().open);
let keep_before = r.cloud.keep_booted.lock().unwrap().len();
update_every_box(&r.state, &r.clock, &key, "acme", "acme", "holger");
let booted = r.cloud.keep_booted.lock().unwrap().clone();
assert_eq!(booted.len(), keep_before + 1);
assert_eq!(
booted.last().unwrap().1,
"r10-holger-keep",
"Holger's main from Holger's keep medium"
);
let run = r.state.estates.get("acme").unwrap();
assert_eq!(
run.phase,
phase::PAIRED,
"the update is no delivery: {:#?}",
run.history
);
assert_eq!(r.mails.count("estate.ready"), 0);
r.clock.fetch_add(MINUTE, Ordering::SeqCst);
drive(&r.state, "acme");
let run = r.state.estates.get("acme").unwrap();
assert_eq!(run.phase, phase::CAPTURED, "{:#?}", run.history);
assert_eq!(r.mails.count("estate.ready"), 1);
}