Skip to main content

holger_fleet/
exec.rs

1//! **The order executor**: one verified order → one of holger's own verbs → one ack.
2//!
3//! The host hands a [`HolgerBox`] (holger-server-lib's `fleet_box`, a test's fake). Every
4//! verb defaults to a refusal BY NAME, so a box that cannot do one says why instead of
5//! the guardian waiting. The money is checked HERE as well as in the box: an entitlement,
6//! a snapshot and a growth step are verified under the monetize key before the box is
7//! asked to move anything.
8
9use std::collections::BTreeMap;
10
11use monetize_embed::fleet::{GrowGoAhead, OrderAck};
12use monetize_product::pull::{AddPrincipal, GrowRoom, PurgeAnswer, PurgeTenant};
13use monetize_product::EntitlementFact;
14
15use crate::order::Order;
16
17pub use crate::order::Recharge as RechargeAsk;
18
19/// 512-byte sectors in a GiB.
20pub const SECTORS_PER_GIB: u64 = 2_097_152;
21
22/// What an entitlement did on the box.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Applied {
25    /// Stored: the words say what.
26    New(String),
27    /// The box already holds this fact or a newer one: nothing moved.
28    Held(String),
29}
30
31/// Where the grow machine stands, as a pulled growth reads it.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum GrowState {
34    Running(String),
35    /// `/data` is off; the machine waits for the members to come back larger.
36    WaitingMembers(String),
37    Done(String),
38    Failed(String),
39}
40
41/// **`Fleet.Ask{grow_set}`**: monetize sizes THIS box's own data disks.
42pub trait GrowSet {
43    fn grow_set(&mut self, gib: u64) -> Result<String, String>;
44}
45
46/// A caller with no channel to ask on.
47pub struct NoAsk;
48impl GrowSet for NoAsk {
49    fn grow_set(&mut self, _: u64) -> Result<String, String> {
50        Err("this box has no pull channel to ask monetize for its disks".into())
51    }
52}
53
54/// **Holger's verbs, as an order reaches them.** Sync: the executor runs on a jera job's
55/// blocking thread.
56pub trait HolgerBox {
57    /// This box serves as the primary (not a twin, not awaiting its recharge).
58    fn serving(&self) -> bool;
59    /// The box's health words for `Fleet.Report` (`serving`, `data`, `serves_at`, `twin`, `epoch`).
60    fn health(&self) -> BTreeMap<String, String> {
61        BTreeMap::new()
62    }
63    /// `monetize_product::pull::ProductReport` as JSON — the main that serves sends it.
64    fn product_report(&self) -> Option<String> {
65        None
66    }
67    /// A twin reviews itself on every report (G12): its review as JSON.
68    fn own_review(&self) -> Option<String> {
69        None
70    }
71    /// Store one VERIFIED fact, newer wins.
72    fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
73        Err(format!("this box holds no entitlements, so the fact for {} is not applied", fact.tenant.0))
74    }
75    /// Purge one repository (or answer its purge's state).
76    fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
77        Err(format!("this box purges no repositories, so {} is not purged", p.tenant))
78    }
79    /// **A sold estate's resend sign-in link** (Gunnar's S10): mint a fresh first-login invite
80    /// for the order's account and mail it to the order's address. Default: this box mints none.
81    fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
82        let _ = (&s.to, &s.host, s.ttl_secs);
83        Err(format!("this box mints no sign-in links, so it cannot run sign_in_again for {}", s.account))
84    }
85    /// **Keep the estate keys fed again** (monetize MAIL-RELAY + NIGHT-PUBREPO 2026-10-28): kept
86    /// beside the feed the box was laid with, which they go over. Default: this box keeps none.
87    fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
88        Err(format!("this box keeps no estate keys fed again, so {} is not kept", r.keys.keys().cloned().collect::<Vec<_>>().join(", ")))
89    }
90    /// A paid signup's repository (`a.username`), bound to `a.signup` (idempotent); answers the
91    /// namespace made. A name bound to another signup is refused starting `taken`.
92    fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
93        Err(format!("this box makes no repositories for a signup, so {} is not made", a.username))
94    }
95    /// Keep the room wanted past the set and tell the operator.
96    fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
97        Err(format!("this box grows no data set, so {} bytes wanted cannot be kept", g.need_bytes))
98    }
99    /// Do the box's data disks already carry `data_gib` a disk?
100    fn grow_carried(&self, data_gib: u64) -> Result<bool, String> {
101        Err(format!("this box grows no data set, so it cannot grow to {data_gib} GiB"))
102    }
103    /// The twin first (a main only): Ok once the twin holds main's whole set.
104    fn twin_holds(&self, data_gib: u64) -> Result<String, String> {
105        Err(format!("this box cannot see whether its twin holds {data_gib} GiB a disk"))
106    }
107    /// Start the growth to `data_gib` a disk under a one-time `nonce` (the step was verified).
108    fn grow_start(&self, data_gib: u64, nonce: &str) -> Result<GrowState, String> {
109        let _ = (data_gib, nonce);
110        Err("this box grows no data set".into())
111    }
112    /// Wait for the grow machine to stop moving (past `waiting-members` when told).
113    fn grow_settle(&self, past_waiting: bool) -> GrowState {
114        let _ = past_waiting;
115        GrowState::Failed("this box grows no data set".into())
116    }
117    /// Look at the members again (`give_up`: mount back what is there).
118    fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
119        let _ = give_up;
120        Err("this box grows no data set".into())
121    }
122    /// Fill `r.main` from `r.twin` (holger's recharge lane).
123    fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
124        Err(format!("this box runs no recharge lane, so {} is not filled from {}", r.main, r.twin))
125    }
126    /// Fence the old primary at `primary` (this box itself when it names it): it refuses
127    /// every write from now on. Refused by name by default.
128    fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
129        let _ = epoch;
130        Err(format!("this box cannot fence the primary at {primary}: the guardian fences it at the cloud (the recharge's `fenced` proof)"))
131    }
132    /// Certify the twin at `twin` for this primary.
133    fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
134        let _ = principal;
135        Err(format!("this box cannot certify the twin at {twin}: the front's certification does"))
136    }
137    /// Take the twin role: follow `primary` (a FRESH box only, exactly once).
138    fn become_twin(&self, primary: &str) -> Result<String, String> {
139        Err(format!("this box cannot become the twin of {primary}"))
140    }
141    /// Review the twin (on a twin: itself), as JSON.
142    fn review(&self) -> Result<serde_json::Value, String> {
143        Err("this box cannot review a twin".into())
144    }
145}
146
147/// What the executor knows about the order's context: the shared pump's own (P4a).
148pub use monetize_estate_side::pump::Ctx;
149
150fn said(seq: u64, r: Result<String, String>) -> OrderAck {
151    match r {
152        Ok(detail) => OrderAck { seq, ok: true, detail, receipt_json: String::new() },
153        Err(detail) => OrderAck { seq, ok: false, detail, receipt_json: String::new() },
154    }
155}
156
157/// **Run one verified order** and say what came of it — the ack the next Report carries.
158/// Never panics, never retries: the guardian reads the ack.
159pub fn execute<B: HolgerBox + ?Sized>(b: &B, ctx: &Ctx, seq: u64, order: &Order, ask: &mut dyn GrowSet) -> OrderAck {
160    let not_serving = || Err::<String, String>("this box does not serve as the primary (a twin, or awaiting its recharge): nothing applied".into());
161    match order {
162        Order::Entitlement(fact) => {
163            if !b.serving() {
164                return said(seq, not_serving());
165            }
166            if let Err(e) = monetize_embed::signing::verify_fact(fact, ctx.key) {
167                return said(seq, Err(format!("the fact for {} does not verify under this box's monetize key ({e}): nothing moved", fact.tenant.0)));
168            }
169            said(seq, b.entitle(fact).map(|a| match a {
170                Applied::New(w) | Applied::Held(w) => w,
171            }))
172        }
173        Order::RestateEntitlements(snap) => {
174            if !b.serving() {
175                return said(seq, not_serving());
176            }
177            if let Err(e) = monetize_embed::signing::verify_snapshot(snap, ctx.key) {
178                return said(seq, Err(format!("the snapshot does not verify under this box's monetize key ({e}): nothing restated")));
179            }
180            let (mut new, mut held) = (0usize, 0usize);
181            for fact in &snap.facts {
182                match b.entitle(fact) {
183                    Ok(Applied::New(_)) => new += 1,
184                    Ok(Applied::Held(_)) => held += 1,
185                    Err(e) => return said(seq, Err(format!("restated {new} before {} was refused: {e}", fact.tenant.0))),
186                }
187            }
188            said(seq, Ok(format!("restated {new} entitlement fact(s) ({held} already held as new or newer); nothing deleted")))
189        }
190        Order::PurgeTenant(p) => {
191            if !b.serving() {
192                return said(seq, not_serving());
193            }
194            match b.purge(p) {
195                Ok(answer) => OrderAck { seq, ok: true, detail: serde_json::to_string(&answer).unwrap_or_default(), receipt_json: String::new() },
196                Err(e) => said(seq, Err(e)),
197            }
198        }
199        Order::GrowRoom(g) => said(seq, b.want_room(g)),
200        Order::AddPrincipal(a) => {
201            if !b.serving() {
202                return said(seq, not_serving());
203            }
204            said(seq, b.add_principal(a))
205        }
206        Order::SignInAgain(s) => {
207            if !b.serving() {
208                return said(seq, not_serving());
209            }
210            said(seq, b.sign_in_again(s))
211        }
212        // Any box the order is for keeps it (the pump checked the role): a twin that serves later
213        // mails through it too.
214        Order::Refeed(r) => said(seq, b.keep_refeed(r)),
215        Order::GrowGoAhead(g) => grow(b, ctx, seq, g, ask),
216        Order::RechargeFromTwin(r) => {
217            // Fence before recharge (Gunnar's order): the old primary the order names is
218            // fenced first, and only a fence that held lets the recharge run beside it. A
219            // guardian that fenced it AT THE CLOUD sends its proof (`fenced`) instead. With
220            // neither, the box's recharge lane refuses `not_fenced` (A7 row 4: the shared
221            // `nordisk_twin::fence::fence_gate`), so a bare order never makes two writers.
222            let fenced = match r.fence.as_deref().map(str::trim) {
223                Some(old) if old == r.main.trim() => {
224                    return said(seq, Err(format!("order {seq} names the main {} as the primary to fence: a recharge never fences the box it fills", r.main)));
225                }
226                Some(old) => match b.fence(old, r.epoch) {
227                    Ok(_) => Some(old.to_string()),
228                    Err(e) => return said(seq, Err(format!("order {seq} fences {old} first and that fence did not hold ({e}): nothing was recharged"))),
229                },
230                None => None,
231            };
232            let prefix = match (&fenced, r.fenced.as_deref()) {
233                (Some(f), _) => format!("fenced {f}; "),
234                (None, Some(p)) => format!("cloud-fenced ({p}); "),
235                (None, None) => String::new(),
236            };
237            said(seq, b.recharge(r).map(|w| format!("{prefix}{w}")).map_err(|e| format!("{prefix}{e}")))
238        }
239        Order::FencePrimary { primary, epoch } => said(seq, b.fence(primary, *epoch)),
240        Order::AdoptTwin { twin, principal } => said(seq, b.adopt(twin, principal)),
241        Order::BecomeTwin { primary } => said(seq, b.become_twin(primary)),
242        Order::ReviewTwin => match b.review() {
243            Ok(v) => OrderAck { seq, ok: true, detail: v.get("verdict").and_then(|w| w.as_str()).unwrap_or("reviewed").to_string(), receipt_json: v.to_string() },
244            Err(e) => said(seq, Err(e)),
245        },
246    }
247}
248
249/// **A pulled `grow_go_ahead`** (G16-d): verify monetize's step for THIS estate and role,
250/// the twin first, drain and unmount, ask monetize to size this box's own disks, look
251/// again, remount. A growth already at size is acked ok with nothing moved.
252fn grow<B: HolgerBox + ?Sized>(b: &B, ctx: &Ctx, seq: u64, g: &GrowGoAhead, ask: &mut dyn GrowSet) -> OrderAck {
253    let fail = |w: String| said(seq, Err(w));
254    let step = match monetize_embed::signing::verify_estate_growth(g.go_ahead.as_bytes(), ctx.key) {
255        Ok(s) => s,
256        Err(e) => return fail(format!("the paid step does not verify under this box's monetize key ({e}): nothing moved")),
257    };
258    if step.estate != ctx.me.instance {
259        return fail(format!("the paid step is estate {}'s; this box pulls as {}: nothing moved", step.estate, ctx.me.instance));
260    }
261    if step.until_unix_ms <= ctx.now_unix_ms as i64 {
262        return fail(format!("the paid step was good until {} (unix ms): its period is over, nothing moved", step.until_unix_ms));
263    }
264    let twin = ctx.me.role == "twin";
265    let main_gib = step.main_data_gib_per_member;
266    if !twin && g.data_gib != main_gib {
267        return fail(format!("the order sizes main's disks to {} GiB, the paid step to {main_gib} GiB: nothing moved", g.data_gib));
268    }
269    if twin && g.data_gib < main_gib {
270        return fail(format!("the order sizes the twin's disk to {} GiB, under main's {main_gib} GiB a disk: nothing moved", g.data_gib));
271    }
272    match b.grow_carried(g.data_gib) {
273        Ok(true) => return said(seq, Ok(format!("already {} GiB a data disk: nothing moved", g.data_gib))),
274        Ok(false) => {}
275        Err(e) => return fail(e),
276    }
277    if !twin {
278        if let Err(e) = b.twin_holds(g.data_gib) {
279            return fail(e);
280        }
281    }
282    match b.grow_start(g.data_gib, &format!("pull-{seq}-{}", step.reference)) {
283        Ok(GrowState::Done(d)) => return said(seq, Ok(d)),
284        Ok(GrowState::Failed(f)) => return fail(f),
285        Ok(_) => {}
286        Err(e) => return fail(e),
287    }
288    match b.grow_settle(false) {
289        GrowState::WaitingMembers(_) => {}
290        GrowState::Done(d) => return said(seq, Ok(d)),
291        GrowState::Failed(f) | GrowState::Running(f) => return fail(f),
292    }
293    let asked = ask.grow_set(g.data_gib);
294    if let Err(e) = b.grow_look_again(asked.is_err()) {
295        return fail(format!("the grow machine would not look again: {e}"));
296    }
297    let end = b.grow_settle(true);
298    match (asked, end) {
299        (Ok(words), GrowState::Done(d)) => said(seq, Ok(format!("{d} (monetize: {words})"))),
300        (Ok(words), GrowState::Failed(f) | GrowState::Running(f) | GrowState::WaitingMembers(f)) => fail(format!("{f} (monetize: {words})")),
301        (Err(e), GrowState::Done(s) | GrowState::Failed(s) | GrowState::Running(s) | GrowState::WaitingMembers(s)) => {
302            fail(format!("monetize did not size this box's disks ({e}); /data: {s}"))
303        }
304    }
305}
306
307#[cfg(test)]
308pub(crate) mod tests {
309    use super::*;
310    use crate::order::tests::{me, monetize_key, MONETIZE_SEED};
311    use ed25519_dalek::Signer as _;
312    use monetize_product::{State, TenantId};
313    use std::cell::RefCell;
314
315    /// A Holger box in memory: what it holds and every verb it was asked.
316    #[derive(Default)]
317    pub struct Fake {
318        pub twin: bool,
319        pub facts: RefCell<BTreeMap<String, EntitlementFact>>,
320        pub did: RefCell<Vec<String>>,
321        pub carried: bool,
322        pub settle: RefCell<Vec<GrowState>>,
323    }
324
325    impl HolgerBox for Fake {
326        fn serving(&self) -> bool {
327            !self.twin
328        }
329        fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
330            let mut f = self.facts.borrow_mut();
331            if f.get(&fact.tenant.0).is_some_and(|h| h.issued_unix_ms >= fact.issued_unix_ms) {
332                return Ok(Applied::Held(format!("{}: held", fact.tenant.0)));
333            }
334            f.insert(fact.tenant.0.clone(), fact.clone());
335            Ok(Applied::New(format!("{} is {:?}", fact.tenant.0, fact.state)))
336        }
337        fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
338            self.did.borrow_mut().push(format!("purge {}", p.tenant));
339            Ok(PurgeAnswer { job: format!("purge:{}", p.tenant), detail: "3 artifacts".into(), ..Default::default() })
340        }
341        fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
342            self.did.borrow_mut().push(format!("room {}", g.need_bytes));
343            Ok("kept".into())
344        }
345        fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
346            self.did.borrow_mut().push(format!("sign-in {} {} {} {}", s.account, s.to, s.host, s.ttl_secs));
347            Ok(format!("a new sign-in link for {} was mailed to {}", s.account, s.to))
348        }
349        fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
350            self.did.borrow_mut().push(format!("refeed {}", r.keys.keys().cloned().collect::<Vec<_>>().join(",")));
351            Ok(format!("kept {}", r.keys.keys().cloned().collect::<Vec<_>>().join(", ")))
352        }
353        fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
354            self.did.borrow_mut().push(format!("signup {} {}", a.username, a.signup));
355            if a.username == "theirs" { Err("taken: theirs is another signup's".into()) } else { Ok(a.username.clone()) }
356        }
357        fn grow_carried(&self, _: u64) -> Result<bool, String> {
358            Ok(self.carried)
359        }
360        fn twin_holds(&self, gib: u64) -> Result<String, String> {
361            self.did.borrow_mut().push(format!("twin_holds {gib}"));
362            Ok("the twin holds it".into())
363        }
364        fn grow_start(&self, gib: u64, nonce: &str) -> Result<GrowState, String> {
365            self.did.borrow_mut().push(format!("start {gib} {nonce}"));
366            Ok(GrowState::Running("draining".into()))
367        }
368        fn grow_settle(&self, past: bool) -> GrowState {
369            self.did.borrow_mut().push(format!("settle {past}"));
370            let mut s = self.settle.borrow_mut();
371            if s.is_empty() {
372                if past { GrowState::Done("grown".into()) } else { GrowState::WaitingMembers("waiting-members".into()) }
373            } else {
374                s.remove(0)
375            }
376        }
377        fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
378            self.did.borrow_mut().push(format!("look_again {give_up}"));
379            Ok(())
380        }
381        fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
382            self.did.borrow_mut().push(format!("recharge {} from {} lost={}", r.main, r.twin, r.accept_lost));
383            Ok("recharged".into())
384        }
385        fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
386            self.did.borrow_mut().push(format!("fence {primary} {epoch}"));
387            if primary.starts_with("stuck") { Err(format!("{primary} answers nothing")) } else { Ok(format!("{primary} is fenced at epoch {epoch}")) }
388        }
389        fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
390            self.did.borrow_mut().push(format!("adopt {twin} {principal}"));
391            Ok("follows".into())
392        }
393        fn review(&self) -> Result<serde_json::Value, String> {
394            Ok(serde_json::json!({"verdict": "the twin is whole", "complete": true}))
395        }
396        fn become_twin(&self, primary: &str) -> Result<String, String> {
397            self.did.borrow_mut().push(format!("become_twin {primary}"));
398            Ok("elected".into())
399        }
400    }
401
402    pub fn fact(repo: &str, issued: u64, signer: [u8; 32]) -> EntitlementFact {
403        let mut f = EntitlementFact {
404            tenant: TenantId(repo.into()),
405            plan: "o-1".into(),
406            state: State::Paid,
407            paid_until_unix_ms: Some(99_000_000),
408            caps: Default::default(),
409            source: "payment:stripe:o-1".into(),
410            signature: Vec::new(),
411            issued_unix_ms: (issued != 0).then_some(issued),
412            issued_signature: Vec::new(),
413        };
414        let key = ed25519_dalek::SigningKey::from_bytes(&signer);
415        f.signature = key.sign(&monetize_embed::signing::fact_message(&f)).to_bytes().to_vec();
416        if issued != 0 {
417            f.issued_signature = key.sign(&monetize_embed::signing::fact_message_issued(&f)).to_bytes().to_vec();
418        }
419        f
420    }
421
422    fn run(b: &dyn HolgerBox, role: &str, o: Order) -> OrderAck {
423        let (m, k) = (me(role), monetize_key());
424        execute(b, &Ctx { me: &m, key: &k, now_unix_ms: 10_000 }, 5, &o, &mut NoAsk)
425    }
426
427    struct Asked(Vec<u64>, bool);
428    impl GrowSet for Asked {
429        fn grow_set(&mut self, gib: u64) -> Result<String, String> {
430            self.0.push(gib);
431            if self.1 { Ok("resized".into()) } else { Err("no answer".into()) }
432        }
433    }
434
435    fn growth(estate: &str, gib: u64, until: i64) -> String {
436        let mut g = monetize_embed::signing::EstateGrowth { v: 1, signer: "shop".into(), estate: estate.into(), main_data_gib_per_member: gib, until_unix_ms: until, reference: "o-7".into(), issued_unix_ms: 1, signature: String::new() };
437        let sig = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).sign(&monetize_embed::signing::estate_growth_message(&g));
438        g.signature = monetize_embed::signing::base64_encode(&sig.to_bytes());
439        serde_json::to_string(&g).unwrap()
440    }
441
442    #[test]
443    fn entitlement_is_applied_newer_wins_and_a_forgery_moves_nothing() {
444        let b = Fake::default();
445        let a = run(&b, "main", Order::Entitlement(fact("libs", 5, MONETIZE_SEED)));
446        assert!(a.ok, "{a:?}");
447        assert!(b.facts.borrow().contains_key("libs"));
448        let again = run(&b, "main", Order::Entitlement(fact("libs", 4, MONETIZE_SEED)));
449        assert!(again.ok && again.detail.contains("held"), "an older fact is held, not refused: {again:?}");
450        let forged = run(&b, "main", Order::Entitlement(fact("other", 6, [9u8; 32])));
451        assert!(!forged.ok && forged.detail.contains("does not verify"), "{forged:?}");
452        assert!(!b.facts.borrow().contains_key("other"));
453        let twin = Fake { twin: true, ..Default::default() };
454        let refused = run(&twin, "twin", Order::Entitlement(fact("libs", 5, MONETIZE_SEED)));
455        assert!(!refused.ok && refused.detail.contains("does not serve"), "{refused:?}");
456    }
457
458    /// ★ **A resend sign-in link reaches the box that serves** (A16, Gunnar's S10): the order's
459    /// words go to the host as they are; a twin mints nothing; a box that cannot mint says so.
460    #[test]
461    fn a_sign_in_again_reaches_the_serving_box_and_a_twin_mints_nothing() {
462        let s = crate::order::SignInAgain { account: "acme".into(), to: "buyer@mock.invalid".into(), host: "acme.holger.mock.invalid".into(), ttl_secs: 86_400 };
463        let b = Fake::default();
464        let a = run(&b, "main", Order::SignInAgain(s.clone()));
465        assert!(a.ok && a.detail.contains("mailed to buyer@mock.invalid"), "{a:?}");
466        assert_eq!(b.did.borrow().as_slice(), ["sign-in acme buyer@mock.invalid acme.holger.mock.invalid 86400"]);
467        let twin = Fake { twin: true, ..Default::default() };
468        let refused = run(&twin, "twin", Order::SignInAgain(s.clone()));
469        assert!(!refused.ok && refused.detail.contains("does not serve") && twin.did.borrow().is_empty(), "{refused:?}");
470        struct Bare;
471        impl HolgerBox for Bare {
472            fn serving(&self) -> bool {
473                true
474            }
475        }
476        let none = run(&Bare, "main", Order::SignInAgain(s));
477        assert!(!none.ok && none.detail.contains("mints no sign-in link"), "{none:?}");
478    }
479
480    /// ★ **MAIL-RELAY: a refeed reaches the box's keep**; a box that keeps none says so.
481    #[test]
482    fn a_refeed_reaches_the_boxes_keep_and_a_box_that_keeps_none_refuses() {
483        let r = crate::order::Refeed { keys: [("secret.smtp_url", "smtps://u:tok@relay.mock.invalid:465"), ("secret.smtp_from", "holger@mock.invalid"), ("public_repositories", "crates")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() };
484        let b = Fake::default();
485        let a = run(&b, "main", Order::Refeed(r.clone()));
486        assert!(a.ok && a.detail.contains("public_repositories") && !a.detail.contains("tok@"), "{a:?}");
487        assert_eq!(b.did.borrow().as_slice(), ["refeed public_repositories,secret.smtp_from,secret.smtp_url"]);
488        struct Bare;
489        impl HolgerBox for Bare {
490            fn serving(&self) -> bool {
491                true
492            }
493        }
494        let none = run(&Bare, "main", Order::Refeed(r));
495        assert!(!none.ok && none.detail.contains("keeps no estate keys"), "{none:?}");
496    }
497
498    #[test]
499    fn restate_entitlements_verifies_the_snapshot_and_counts_new_and_held() {
500        let b = Fake::default();
501        run(&b, "main", Order::Entitlement(fact("a", 5, MONETIZE_SEED)));
502        let facts = vec![fact("a", 5, MONETIZE_SEED), fact("b", 5, MONETIZE_SEED)];
503        let mut snap = monetize_embed::Snapshot { issued_unix_ms: 7, facts, signature: Vec::new() };
504        snap.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).sign(&monetize_embed::signing::snapshot_message(snap.issued_unix_ms, &snap.facts)).to_bytes().to_vec();
505        let a = run(&b, "main", Order::RestateEntitlements(snap.clone()));
506        assert!(a.ok && a.detail.contains("restated 1") && a.detail.contains("1 already held"), "{a:?}");
507        snap.signature = vec![0; 64];
508        let bad = run(&b, "main", Order::RestateEntitlements(snap));
509        assert!(!bad.ok && bad.detail.contains("does not verify"), "{bad:?}");
510    }
511
512    #[test]
513    fn purge_tenant_answers_monetizes_purge_answer() {
514        let b = Fake::default();
515        let a = run(&b, "main", Order::PurgeTenant(PurgeTenant { tenant: "libs".into(), reason: "nonpay".into(), job: String::new() }));
516        assert!(a.ok, "{a:?}");
517        let answer: PurgeAnswer = serde_json::from_str(&a.detail).unwrap();
518        assert_eq!(answer.job, "purge:libs");
519        assert_eq!(b.did.borrow().as_slice(), ["purge libs"]);
520    }
521
522    #[test]
523    fn add_principal_makes_a_repository_on_a_plan_and_says_taken_by_name() {
524        let b = Fake::default();
525        let a = run(&b, "main", Order::AddPrincipal(AddPrincipal { username: "crates".into(), kind: "org".into(), signup: "s-1".into() }));
526        assert!(a.ok && a.detail == "crates", "the namespace made is the repository: {a:?}");
527        let t = run(&b, "main", Order::AddPrincipal(AddPrincipal { username: "theirs".into(), kind: "org".into(), signup: "s-2".into() }));
528        assert!(!t.ok && t.detail.starts_with("taken"), "{t:?}");
529        let twin = Fake { twin: true, ..Default::default() };
530        assert!(!run(&twin, "twin", Order::AddPrincipal(AddPrincipal { username: "crates".into(), kind: "org".into(), signup: "s-1".into() })).ok, "only the main that serves");
531    }
532
533    #[test]
534    fn grow_room_is_kept_for_the_operator() {
535        let b = Fake::default();
536        let a = run(&b, "main", Order::GrowRoom(GrowRoom { need_bytes: 1 << 30, reason: "libs".into() }));
537        assert!(a.ok, "{a:?}");
538        assert_eq!(b.did.borrow().as_slice(), ["room 1073741824"]);
539    }
540
541    #[test]
542    fn grow_go_ahead_runs_twin_first_asks_monetize_and_remounts() {
543        let b = Fake::default();
544        let (m, k) = (me("main"), monetize_key());
545        let mut ask = Asked(Vec::new(), true);
546        let o = Order::GrowGoAhead(GrowGoAhead { go_ahead: growth("acme", 120, 99_000), data_gib: 120 });
547        let a = execute(&b, &Ctx { me: &m, key: &k, now_unix_ms: 10_000 }, 6, &o, &mut ask);
548        assert!(a.ok, "{a:?}");
549        assert_eq!(b.did.borrow().as_slice(), ["twin_holds 120", "start 120 pull-6-o-7", "settle false", "look_again false", "settle true"]);
550        assert_eq!(ask.0, [120]);
551
552        // Monetize does not answer: /data is mounted back and the ack says why.
553        let b = Fake::default();
554        let mut deaf = Asked(Vec::new(), false);
555        b.settle.borrow_mut().extend([GrowState::WaitingMembers("w".into()), GrowState::Failed("mounted back".into())]);
556        let a = execute(&b, &Ctx { me: &m, key: &k, now_unix_ms: 10_000 }, 7, &o, &mut deaf);
557        assert!(!a.ok && a.detail.contains("no answer") && a.detail.contains("mounted back"), "{a:?}");
558        assert!(b.did.borrow().contains(&"look_again true".to_string()));
559    }
560
561    #[test]
562    fn grow_go_ahead_refuses_another_estate_a_lapsed_step_and_a_forgery_and_skips_a_done_one() {
563        let b = Fake::default();
564        for (go, why) in [(growth("other", 120, 99_000), "estate other"), (growth("acme", 120, 5_000), "period is over"), (growth("acme", 120, 99_000).replace("\"o-7\"", "\"o-8\""), "does not verify")] {
565            let a = run(&b, "main", Order::GrowGoAhead(GrowGoAhead { go_ahead: go, data_gib: 120 }));
566            assert!(!a.ok && a.detail.contains(why), "{why}: {a:?}");
567        }
568        assert!(b.did.borrow().is_empty(), "nothing moved");
569        let done = Fake { carried: true, ..Default::default() };
570        let a = run(&done, "main", Order::GrowGoAhead(GrowGoAhead { go_ahead: growth("acme", 120, 99_000), data_gib: 120 }));
571        assert!(a.ok && a.detail.contains("nothing moved"), "{a:?}");
572        let twin = Fake { twin: true, ..Default::default() };
573        let a = run(&twin, "twin", Order::GrowGoAhead(GrowGoAhead { go_ahead: growth("acme", 120, 99_000), data_gib: 240 }));
574        assert!(a.detail.contains("no pull channel"), "{a:?}");
575        assert!(!twin.did.borrow().iter().any(|d| d.starts_with("twin_holds")), "the twin grows first, asking no twin: {a:?}");
576    }
577
578    #[test]
579    fn recharge_from_twin_runs_the_lane_on_a_cloud_fence_proof() {
580        let b = Fake::default();
581        let r = RechargeAsk { main: "main:1".into(), twin: "twin:1".into(), accept_lost: true, fenced: Some("upcloud: gone".into()), ..Default::default() };
582        let a = run(&b, "twin", Order::RechargeFromTwin(r));
583        assert!(a.ok && a.detail.starts_with("cloud-fenced"), "{a:?}");
584        assert_eq!(b.did.borrow().as_slice(), ["recharge main:1 from twin:1 lost=true"]);
585    }
586
587    /// ★ **Fence before recharge, by the pulled order** (M2, Gunnar's pattern): the old
588    /// primary the order names is fenced first, and only a fence that held lets the
589    /// recharge run; a fence that did not hold runs nothing; the main it fills is never
590    /// the box it fences.
591    #[test]
592    fn recharge_from_twin_fences_the_old_primary_first() {
593        let b = Fake::default();
594        let r = RechargeAsk { main: "main:2".into(), twin: "twin:1".into(), accept_lost: false, epoch: 4, fence: Some("old:1".into()), ..Default::default() };
595        let a = run(&b, "twin", Order::RechargeFromTwin(r.clone()));
596        assert!(a.ok && a.detail.starts_with("fenced old:1; "), "{a:?}");
597        assert_eq!(b.did.borrow().as_slice(), ["fence old:1 4", "recharge main:2 from twin:1 lost=false"]);
598
599        let stuck = Fake::default();
600        let a = run(&stuck, "twin", Order::RechargeFromTwin(RechargeAsk { fence: Some("stuck:1".into()), ..r.clone() }));
601        assert!(!a.ok && a.detail.contains("stuck:1 answers nothing") && a.detail.contains("nothing was recharged"), "{a:?}");
602        assert_eq!(stuck.did.borrow().as_slice(), ["fence stuck:1 4"], "no recharge beside an unfenced primary");
603
604        let same = Fake::default();
605        let a = run(&same, "twin", Order::RechargeFromTwin(RechargeAsk { fence: Some("main:2".into()), ..r }));
606        assert!(!a.ok && a.detail.contains("never fences the box it fills"), "{a:?}");
607        assert!(same.did.borrow().is_empty(), "nothing ran");
608    }
609
610    #[test]
611    fn become_twin_runs_holgers_election() {
612        let b = Fake { twin: true, ..Default::default() };
613        assert!(run(&b, "twin", Order::BecomeTwin { primary: "main:1".into() }).ok);
614        assert_eq!(b.did.borrow().as_slice(), ["become_twin main:1"]);
615        struct Bare;
616        impl HolgerBox for Bare {
617            fn serving(&self) -> bool {
618                false
619            }
620        }
621        let none = run(&Bare, "twin", Order::BecomeTwin { primary: "main:1".into() });
622        assert!(!none.ok && none.detail.contains("cannot become"), "the default refuses by name: {none:?}");
623    }
624
625    #[test]
626    fn fence_primary_runs_the_boxes_fence_adopt_and_review_run() {
627        let b = Fake::default();
628        let a = run(&b, "twin", Order::FencePrimary { primary: "old:1".into(), epoch: 3 });
629        assert!(a.ok && b.did.borrow().contains(&"fence old:1 3".to_string()), "{a:?}");
630        struct Bare;
631        impl HolgerBox for Bare {
632            fn serving(&self) -> bool {
633                false
634            }
635        }
636        let none = run(&Bare, "twin", Order::FencePrimary { primary: "old:1".into(), epoch: 3 });
637        assert!(!none.ok && none.detail.contains("cannot fence"), "the default refuses by name: {none:?}");
638        let a = run(&b, "main", Order::AdoptTwin { twin: "twin:1".into(), principal: "twin".into() });
639        assert!(a.ok && b.did.borrow().contains(&"adopt twin:1 twin".to_string()), "{a:?}");
640        let a = run(&b, "twin", Order::ReviewTwin);
641        assert!(a.ok && a.detail == "the twin is whole" && a.receipt_json.contains("complete"), "{a:?}");
642    }
643}