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    /// **`Fleet.Ask{fetch}`**: the file a product order carries, PULLED from monetize (the ONE
45    /// box never pushes into an estate; monetize MONETIZE-FIX).
46    fn fetch(&mut self, f: &monetize_embed::fleet::Fetch) -> Result<Vec<u8>, String> {
47        Err(format!(
48            "this box has no pull channel to fetch {} on",
49            f.param
50        ))
51    }
52}
53
54/// A caller with no channel to ask on.
55pub struct NoAsk;
56impl GrowSet for NoAsk {
57    fn grow_set(&mut self, _: u64) -> Result<String, String> {
58        Err("this box has no pull channel to ask monetize for its disks".into())
59    }
60}
61
62/// **Holger's verbs, as an order reaches them.** Sync: the executor runs on a jera job's
63/// blocking thread.
64pub trait HolgerBox {
65    /// This box serves as the primary (not a twin, not awaiting its recharge).
66    fn serving(&self) -> bool;
67    /// The box's health words for `Fleet.Report` (`serving`, `data`, `serves_at`, `twin`, `epoch`).
68    fn health(&self) -> BTreeMap<String, String> {
69        BTreeMap::new()
70    }
71    /// `monetize_product::pull::ProductReport` as JSON — the main that serves sends it.
72    fn product_report(&self) -> Option<String> {
73        None
74    }
75    /// A twin reviews itself on every report (G12): its review as JSON.
76    fn own_review(&self) -> Option<String> {
77        None
78    }
79    /// Store one VERIFIED fact, newer wins.
80    fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
81        Err(format!(
82            "this box holds no entitlements, so the fact for {} is not applied",
83            fact.tenant.0
84        ))
85    }
86    /// Purge one repository (or answer its purge's state).
87    fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
88        Err(format!(
89            "this box purges no repositories, so {} is not purged",
90            p.tenant
91        ))
92    }
93    /// **A sold estate's resend sign-in link** (Gunnar's S10): mint a fresh first-login invite
94    /// for the order's account and mail it to the order's address. Default: this box mints none.
95    fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
96        let _ = (&s.to, &s.host, s.ttl_secs);
97        Err(format!(
98            "this box mints no sign-in links, so it cannot run sign_in_again for {}",
99            s.account
100        ))
101    }
102    /// **Keep the estate keys fed again** (monetize MAIL-RELAY + NIGHT-PUBREPO 2026-10-28): kept
103    /// beside the feed the box was laid with, which they go over. Default: this box keeps none.
104    fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
105        Err(format!(
106            "this box keeps no estate keys fed again, so {} is not kept",
107            r.keys.keys().cloned().collect::<Vec<_>>().join(", ")
108        ))
109    }
110    /// **Keep one generic setting** (HOL-PUBSITE-FEED: a refeed's public-site switch, kept as
111    /// [`crate::order::PUBLIC_SITE_SETTING`]): stored as an administrator's write to
112    /// `/-/settings/<name>` is, audited, answering what it replaced. Default: this box keeps none.
113    fn keep_setting(&self, name: &str, value: &[u8]) -> Result<String, String> {
114        let _ = value;
115        Err(format!("this box keeps no settings, so {name} is not kept"))
116    }
117    /// A paid signup's repository (`a.username`), bound to `a.signup` (idempotent); answers the
118    /// namespace made. A name bound to another signup is refused starting `taken`.
119    fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
120        Err(format!(
121            "this box makes no repositories for a signup, so {} is not made",
122            a.username
123        ))
124    }
125    /// Keep the room wanted past the set and tell the operator.
126    fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
127        Err(format!(
128            "this box grows no data set, so {} bytes wanted cannot be kept",
129            g.need_bytes
130        ))
131    }
132    /// Do the box's data disks already carry `data_gib` a disk?
133    fn grow_carried(&self, data_gib: u64) -> Result<bool, String> {
134        Err(format!(
135            "this box grows no data set, so it cannot grow to {data_gib} GiB"
136        ))
137    }
138    /// The twin first (a main only): Ok once the twin holds main's whole set.
139    fn twin_holds(&self, data_gib: u64) -> Result<String, String> {
140        Err(format!(
141            "this box cannot see whether its twin holds {data_gib} GiB a disk"
142        ))
143    }
144    /// Start the growth to `data_gib` a disk under a one-time `nonce` (the step was verified).
145    fn grow_start(&self, data_gib: u64, nonce: &str) -> Result<GrowState, String> {
146        let _ = (data_gib, nonce);
147        Err("this box grows no data set".into())
148    }
149    /// Wait for the grow machine to stop moving (past `waiting-members` when told).
150    fn grow_settle(&self, past_waiting: bool) -> GrowState {
151        let _ = past_waiting;
152        GrowState::Failed("this box grows no data set".into())
153    }
154    /// Look at the members again (`give_up`: mount back what is there).
155    fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
156        let _ = give_up;
157        Err("this box grows no data set".into())
158    }
159    /// Fill `r.main` from `r.twin` (holger's recharge lane).
160    fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
161        Err(format!(
162            "this box runs no recharge lane, so {} is not filled from {}",
163            r.main, r.twin
164        ))
165    }
166    /// Fence the old primary at `primary` (this box itself when it names it): it refuses
167    /// every write from now on. Refused by name by default.
168    fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
169        let _ = epoch;
170        Err(format!("this box cannot fence the primary at {primary}: the guardian fences it at the cloud (the recharge's `fenced` proof)"))
171    }
172    /// Certify the twin at `twin` for this primary.
173    fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
174        let _ = principal;
175        Err(format!(
176            "this box cannot certify the twin at {twin}: the front's certification does"
177        ))
178    }
179    /// Take the twin role: follow `primary` (a FRESH box only, exactly once).
180    /// **One of Holger's own order kinds** (`crate::order::holger_kind::PRODUCT`, queued by the
181    /// operator on his site and pulled): its params as the operator typed them, its file pulled.
182    /// `by` is the VERIFIED order it came in (its sequence and the monetize key it verified
183    /// under): what the box's own audit names the write by. The answer is the ack's detail.
184    fn product_order(
185        &self,
186        by: &OrderedBy,
187        kind: &str,
188        params: &BTreeMap<String, String>,
189        file: Option<&[u8]>,
190    ) -> Result<String, String> {
191        let _ = (by, params, file);
192        Err(format!("this box runs no {kind} order"))
193    }
194    fn become_twin(&self, primary: &str) -> Result<String, String> {
195        Err(format!("this box cannot become the twin of {primary}"))
196    }
197    /// Review the twin (on a twin: itself), as JSON.
198    fn review(&self) -> Result<serde_json::Value, String> {
199        Err("this box cannot review a twin".into())
200    }
201}
202
203/// What the executor knows about the order's context: the shared pump's own (P4a).
204pub use monetize_estate_side::pump::Ctx;
205
206/// **A `refeed`, kept group by group** (Gunnar's `gunnar-repair` `exec::refeed`): the keys kept
207/// beside the feed first ([`HolgerBox::keep_refeed`]: the relay, the public repositories), then
208/// each generic setting it sets ([`HolgerBox::keep_setting`]: the public-site switch). The first
209/// refusal is the answer, saying what was kept before it; else every group's words.
210fn refeed<B: HolgerBox + ?Sized>(b: &B, r: &crate::order::Refeed) -> Result<String, String> {
211    let mut said: Vec<String> = Vec::new();
212    let kept = r.kept_keys();
213    if !kept.keys.is_empty() {
214        said.push(b.keep_refeed(&kept)?);
215    }
216    for (name, value) in r.settings() {
217        match b.keep_setting(name, &value) {
218            Ok(words) => said.push(words),
219            Err(why) if !said.is_empty() => {
220                return Err(format!("{why} (kept before it: {})", said.join("; ")))
221            }
222            Err(why) => return Err(why),
223        }
224    }
225    Ok(said.join("; "))
226}
227
228fn said(seq: u64, r: Result<String, String>) -> OrderAck {
229    match r {
230        Ok(detail) => OrderAck {
231            seq,
232            ok: true,
233            detail,
234            receipt_json: String::new(),
235        },
236        Err(detail) => OrderAck {
237            seq,
238            ok: false,
239            detail,
240            receipt_json: String::new(),
241        },
242    }
243}
244
245/// **The verified order a product order came in**: its sequence and the monetize key (hex) it
246/// verified under. Made only by [`execute`] for an order the pump checked; never read off the
247/// wire, so nothing a caller presents can be one.
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct OrderedBy {
250    pub seq: u64,
251    pub signer: String,
252}
253
254impl OrderedBy {
255    /// The principal a box's audit names the order's write by: `fleet-order <seq> signed <key>`.
256    pub fn principal(&self) -> String {
257        format!("fleet-order {} signed {}", self.seq, self.signer)
258    }
259}
260
261/// **Run one verified order** and say what came of it — the ack the next Report carries.
262/// Never panics, never retries: the guardian reads the ack.
263pub fn execute<B: HolgerBox + ?Sized>(
264    b: &B,
265    ctx: &Ctx,
266    seq: u64,
267    order: &Order,
268    ask: &mut dyn GrowSet,
269) -> OrderAck {
270    let not_serving = || {
271        Err::<String, String>("this box does not serve as the primary (a twin, or awaiting its recharge): nothing applied".into())
272    };
273    match order {
274        Order::Entitlement(fact) => {
275            if !b.serving() {
276                return said(seq, not_serving());
277            }
278            if let Err(e) = monetize_embed::signing::verify_fact(fact, ctx.key) {
279                return said(seq, Err(format!("the fact for {} does not verify under this box's monetize key ({e}): nothing moved", fact.tenant.0)));
280            }
281            said(
282                seq,
283                b.entitle(fact).map(|a| match a {
284                    Applied::New(w) | Applied::Held(w) => w,
285                }),
286            )
287        }
288        Order::RestateEntitlements(snap) => {
289            if !b.serving() {
290                return said(seq, not_serving());
291            }
292            if let Err(e) = monetize_embed::signing::verify_snapshot(snap, ctx.key) {
293                return said(seq, Err(format!("the snapshot does not verify under this box's monetize key ({e}): nothing restated")));
294            }
295            let (mut new, mut held) = (0usize, 0usize);
296            for fact in &snap.facts {
297                match b.entitle(fact) {
298                    Ok(Applied::New(_)) => new += 1,
299                    Ok(Applied::Held(_)) => held += 1,
300                    Err(e) => {
301                        return said(
302                            seq,
303                            Err(format!(
304                                "restated {new} before {} was refused: {e}",
305                                fact.tenant.0
306                            )),
307                        )
308                    }
309                }
310            }
311            said(seq, Ok(format!("restated {new} entitlement fact(s) ({held} already held as new or newer); nothing deleted")))
312        }
313        Order::PurgeTenant(p) => {
314            if !b.serving() {
315                return said(seq, not_serving());
316            }
317            match b.purge(p) {
318                Ok(answer) => OrderAck {
319                    seq,
320                    ok: true,
321                    detail: serde_json::to_string(&answer).unwrap_or_default(),
322                    receipt_json: String::new(),
323                },
324                Err(e) => said(seq, Err(e)),
325            }
326        }
327        Order::GrowRoom(g) => said(seq, b.want_room(g)),
328        Order::AddPrincipal(a) => {
329            if !b.serving() {
330                return said(seq, not_serving());
331            }
332            said(seq, b.add_principal(a))
333        }
334        Order::SignInAgain(s) => {
335            if !b.serving() {
336                return said(seq, not_serving());
337            }
338            said(seq, b.sign_in_again(s))
339        }
340        // Any box the order is for keeps it (the pump checked the role): a twin that serves later
341        // mails through it too.
342        Order::Refeed(r) => said(seq, refeed(b, r)),
343        Order::GrowGoAhead(g) => grow(b, ctx, seq, g, ask),
344        Order::RechargeFromTwin(r) => {
345            // Fence before recharge (Gunnar's order): the old primary the order names is
346            // fenced first, and only a fence that held lets the recharge run beside it. A
347            // guardian that fenced it AT THE CLOUD sends its proof (`fenced`) instead. With
348            // neither, the box's recharge lane refuses `not_fenced` (A7 row 4: the shared
349            // `nordisk_twin::fence::fence_gate`), so a bare order never makes two writers.
350            let fenced = match r.fence.as_deref().map(str::trim) {
351                Some(old) if old == r.main.trim() => {
352                    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)));
353                }
354                Some(old) => match b.fence(old, r.epoch) {
355                    Ok(_) => Some(old.to_string()),
356                    Err(e) => return said(seq, Err(format!("order {seq} fences {old} first and that fence did not hold ({e}): nothing was recharged"))),
357                },
358                None => None,
359            };
360            let prefix = match (&fenced, r.fenced.as_deref()) {
361                (Some(f), _) => format!("fenced {f}; "),
362                (None, Some(p)) => format!("cloud-fenced ({p}); "),
363                (None, None) => String::new(),
364            };
365            said(
366                seq,
367                b.recharge(r)
368                    .map(|w| format!("{prefix}{w}"))
369                    .map_err(|e| format!("{prefix}{e}")),
370            )
371        }
372        Order::FencePrimary { primary, epoch } => said(seq, b.fence(primary, *epoch)),
373        Order::AdoptTwin { twin, principal } => said(seq, b.adopt(twin, principal)),
374        Order::BecomeTwin { primary } => said(seq, b.become_twin(primary)),
375        Order::Product { kind, body } => {
376            if !b.serving() {
377                return said(seq, not_serving());
378            }
379            let file = match &body.fetch {
380                None => None,
381                Some(f) => match ask.fetch(f) {
382                    Ok(bytes) => Some(bytes),
383                    Err(e) => {
384                        return said(
385                            seq,
386                            Err(format!(
387                                "order {seq} ({kind}): its file {} was not pulled: {e}",
388                                f.param
389                            )),
390                        )
391                    }
392                },
393            };
394            let by = OrderedBy {
395                seq,
396                signer: crate::hex(&ctx.key.to_bytes()),
397            };
398            said(
399                seq,
400                b.product_order(&by, kind, &body.params, file.as_deref()),
401            )
402        }
403        Order::ReviewTwin => match b.review() {
404            Ok(v) => OrderAck {
405                seq,
406                ok: true,
407                detail: v
408                    .get("verdict")
409                    .and_then(|w| w.as_str())
410                    .unwrap_or("reviewed")
411                    .to_string(),
412                receipt_json: v.to_string(),
413            },
414            Err(e) => said(seq, Err(e)),
415        },
416    }
417}
418
419/// **A pulled `grow_go_ahead`** (G16-d): verify monetize's step for THIS estate and role,
420/// the twin first, drain and unmount, ask monetize to size this box's own disks, look
421/// again, remount. A growth already at size is acked ok with nothing moved.
422fn grow<B: HolgerBox + ?Sized>(
423    b: &B,
424    ctx: &Ctx,
425    seq: u64,
426    g: &GrowGoAhead,
427    ask: &mut dyn GrowSet,
428) -> OrderAck {
429    let fail = |w: String| said(seq, Err(w));
430    let step = match monetize_embed::signing::verify_estate_growth(g.go_ahead.as_bytes(), ctx.key) {
431        Ok(s) => s,
432        Err(e) => {
433            return fail(format!(
434                "the paid step does not verify under this box's monetize key ({e}): nothing moved"
435            ))
436        }
437    };
438    if step.estate != ctx.me.instance {
439        return fail(format!(
440            "the paid step is estate {}'s; this box pulls as {}: nothing moved",
441            step.estate, ctx.me.instance
442        ));
443    }
444    if step.until_unix_ms <= ctx.now_unix_ms as i64 {
445        return fail(format!(
446            "the paid step was good until {} (unix ms): its period is over, nothing moved",
447            step.until_unix_ms
448        ));
449    }
450    let twin = ctx.me.role == "twin";
451    let main_gib = step.main_data_gib_per_member;
452    if !twin && g.data_gib != main_gib {
453        return fail(format!("the order sizes main's disks to {} GiB, the paid step to {main_gib} GiB: nothing moved", g.data_gib));
454    }
455    if twin && g.data_gib < main_gib {
456        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));
457    }
458    match b.grow_carried(g.data_gib) {
459        Ok(true) => {
460            return said(
461                seq,
462                Ok(format!(
463                    "already {} GiB a data disk: nothing moved",
464                    g.data_gib
465                )),
466            )
467        }
468        Ok(false) => {}
469        Err(e) => return fail(e),
470    }
471    if !twin {
472        if let Err(e) = b.twin_holds(g.data_gib) {
473            return fail(e);
474        }
475    }
476    match b.grow_start(g.data_gib, &format!("pull-{seq}-{}", step.reference)) {
477        Ok(GrowState::Done(d)) => return said(seq, Ok(d)),
478        Ok(GrowState::Failed(f)) => return fail(f),
479        Ok(_) => {}
480        Err(e) => return fail(e),
481    }
482    match b.grow_settle(false) {
483        GrowState::WaitingMembers(_) => {}
484        GrowState::Done(d) => return said(seq, Ok(d)),
485        GrowState::Failed(f) | GrowState::Running(f) => return fail(f),
486    }
487    let asked = ask.grow_set(g.data_gib);
488    if let Err(e) = b.grow_look_again(asked.is_err()) {
489        return fail(format!("the grow machine would not look again: {e}"));
490    }
491    let end = b.grow_settle(true);
492    match (asked, end) {
493        (Ok(words), GrowState::Done(d)) => said(seq, Ok(format!("{d} (monetize: {words})"))),
494        (
495            Ok(words),
496            GrowState::Failed(f) | GrowState::Running(f) | GrowState::WaitingMembers(f),
497        ) => fail(format!("{f} (monetize: {words})")),
498        (
499            Err(e),
500            GrowState::Done(s)
501            | GrowState::Failed(s)
502            | GrowState::Running(s)
503            | GrowState::WaitingMembers(s),
504        ) => fail(format!(
505            "monetize did not size this box's disks ({e}); /data: {s}"
506        )),
507    }
508}
509
510#[cfg(test)]
511pub(crate) mod tests {
512    use super::*;
513    use crate::order::tests::{me, monetize_key, MONETIZE_SEED};
514    use ed25519_dalek::Signer as _;
515    use monetize_product::{State, TenantId};
516    use std::cell::RefCell;
517
518    /// A Holger box in memory: what it holds and every verb it was asked.
519    #[derive(Default)]
520    pub struct Fake {
521        pub twin: bool,
522        pub facts: RefCell<BTreeMap<String, EntitlementFact>>,
523        pub did: RefCell<Vec<String>>,
524        pub carried: bool,
525        pub settle: RefCell<Vec<GrowState>>,
526    }
527
528    impl HolgerBox for Fake {
529        fn serving(&self) -> bool {
530            !self.twin
531        }
532        fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
533            let mut f = self.facts.borrow_mut();
534            if f.get(&fact.tenant.0)
535                .is_some_and(|h| h.issued_unix_ms >= fact.issued_unix_ms)
536            {
537                return Ok(Applied::Held(format!("{}: held", fact.tenant.0)));
538            }
539            f.insert(fact.tenant.0.clone(), fact.clone());
540            Ok(Applied::New(format!(
541                "{} is {:?}",
542                fact.tenant.0, fact.state
543            )))
544        }
545        fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
546            self.did.borrow_mut().push(format!("purge {}", p.tenant));
547            Ok(PurgeAnswer {
548                job: format!("purge:{}", p.tenant),
549                detail: "3 artifacts".into(),
550                ..Default::default()
551            })
552        }
553        fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
554            self.did.borrow_mut().push(format!("room {}", g.need_bytes));
555            Ok("kept".into())
556        }
557        fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
558            self.did.borrow_mut().push(format!(
559                "sign-in {} {} {} {}",
560                s.account, s.to, s.host, s.ttl_secs
561            ));
562            Ok(format!(
563                "a new sign-in link for {} was mailed to {}",
564                s.account, s.to
565            ))
566        }
567        fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
568            self.did.borrow_mut().push(format!(
569                "refeed {}",
570                r.keys.keys().cloned().collect::<Vec<_>>().join(",")
571            ));
572            Ok(format!(
573                "kept {}",
574                r.keys.keys().cloned().collect::<Vec<_>>().join(", ")
575            ))
576        }
577        fn keep_setting(&self, name: &str, value: &[u8]) -> Result<String, String> {
578            let v = String::from_utf8_lossy(value).into_owned();
579            self.did.borrow_mut().push(format!("setting {name} {v}"));
580            Ok(format!("{name} is {v}"))
581        }
582        fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
583            self.did
584                .borrow_mut()
585                .push(format!("signup {} {}", a.username, a.signup));
586            if a.username == "theirs" {
587                Err("taken: theirs is another signup's".into())
588            } else {
589                Ok(a.username.clone())
590            }
591        }
592        fn grow_carried(&self, _: u64) -> Result<bool, String> {
593            Ok(self.carried)
594        }
595        fn twin_holds(&self, gib: u64) -> Result<String, String> {
596            self.did.borrow_mut().push(format!("twin_holds {gib}"));
597            Ok("the twin holds it".into())
598        }
599        fn grow_start(&self, gib: u64, nonce: &str) -> Result<GrowState, String> {
600            self.did.borrow_mut().push(format!("start {gib} {nonce}"));
601            Ok(GrowState::Running("draining".into()))
602        }
603        fn grow_settle(&self, past: bool) -> GrowState {
604            self.did.borrow_mut().push(format!("settle {past}"));
605            let mut s = self.settle.borrow_mut();
606            if s.is_empty() {
607                if past {
608                    GrowState::Done("grown".into())
609                } else {
610                    GrowState::WaitingMembers("waiting-members".into())
611                }
612            } else {
613                s.remove(0)
614            }
615        }
616        fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
617            self.did.borrow_mut().push(format!("look_again {give_up}"));
618            Ok(())
619        }
620        fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
621            self.did.borrow_mut().push(format!(
622                "recharge {} from {} lost={}",
623                r.main, r.twin, r.accept_lost
624            ));
625            Ok("recharged".into())
626        }
627        fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
628            self.did
629                .borrow_mut()
630                .push(format!("fence {primary} {epoch}"));
631            if primary.starts_with("stuck") {
632                Err(format!("{primary} answers nothing"))
633            } else {
634                Ok(format!("{primary} is fenced at epoch {epoch}"))
635            }
636        }
637        fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
638            self.did
639                .borrow_mut()
640                .push(format!("adopt {twin} {principal}"));
641            Ok("follows".into())
642        }
643        fn review(&self) -> Result<serde_json::Value, String> {
644            Ok(serde_json::json!({"verdict": "the twin is whole", "complete": true}))
645        }
646        fn become_twin(&self, primary: &str) -> Result<String, String> {
647            self.did.borrow_mut().push(format!("become_twin {primary}"));
648            Ok("elected".into())
649        }
650        fn product_order(
651            &self,
652            by: &OrderedBy,
653            kind: &str,
654            params: &BTreeMap<String, String>,
655            file: Option<&[u8]>,
656        ) -> Result<String, String> {
657            self.did.borrow_mut().push(format!(
658                "{kind} {params:?} {:?} by {}",
659                file.map(<[u8]>::len),
660                by.seq
661            ));
662            Ok(format!("{{\"ran\":\"{kind}\"}}"))
663        }
664    }
665
666    pub fn fact(repo: &str, issued: u64, signer: [u8; 32]) -> EntitlementFact {
667        let mut f = EntitlementFact {
668            tenant: TenantId(repo.into()),
669            plan: "o-1".into(),
670            state: State::Paid,
671            paid_until_unix_ms: Some(99_000_000),
672            caps: Default::default(),
673            source: "payment:stripe:o-1".into(),
674            signature: Vec::new(),
675            issued_unix_ms: (issued != 0).then_some(issued),
676            issued_signature: Vec::new(),
677        };
678        let key = ed25519_dalek::SigningKey::from_bytes(&signer);
679        f.signature = key
680            .sign(&monetize_embed::signing::fact_message(&f))
681            .to_bytes()
682            .to_vec();
683        if issued != 0 {
684            f.issued_signature = key
685                .sign(&monetize_embed::signing::fact_message_issued(&f))
686                .to_bytes()
687                .to_vec();
688        }
689        f
690    }
691
692    fn run(b: &dyn HolgerBox, role: &str, o: Order) -> OrderAck {
693        let (m, k) = (me(role), monetize_key());
694        execute(
695            b,
696            &Ctx {
697                me: &m,
698                key: &k,
699                now_unix_ms: 10_000,
700            },
701            5,
702            &o,
703            &mut NoAsk,
704        )
705    }
706
707    struct Asked(Vec<u64>, bool);
708    impl GrowSet for Asked {
709        fn grow_set(&mut self, gib: u64) -> Result<String, String> {
710            self.0.push(gib);
711            if self.1 {
712                Ok("resized".into())
713            } else {
714                Err("no answer".into())
715            }
716        }
717    }
718
719    fn growth(estate: &str, gib: u64, until: i64) -> String {
720        let mut g = monetize_embed::signing::EstateGrowth {
721            v: 1,
722            signer: "shop".into(),
723            estate: estate.into(),
724            main_data_gib_per_member: gib,
725            until_unix_ms: until,
726            reference: "o-7".into(),
727            issued_unix_ms: 1,
728            signature: String::new(),
729        };
730        let sig = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
731            .sign(&monetize_embed::signing::estate_growth_message(&g));
732        g.signature = monetize_embed::signing::base64_encode(&sig.to_bytes());
733        serde_json::to_string(&g).unwrap()
734    }
735
736    #[test]
737    fn entitlement_is_applied_newer_wins_and_a_forgery_moves_nothing() {
738        let b = Fake::default();
739        let a = run(
740            &b,
741            "main",
742            Order::Entitlement(fact("libs", 5, MONETIZE_SEED)),
743        );
744        assert!(a.ok, "{a:?}");
745        assert!(b.facts.borrow().contains_key("libs"));
746        let again = run(
747            &b,
748            "main",
749            Order::Entitlement(fact("libs", 4, MONETIZE_SEED)),
750        );
751        assert!(
752            again.ok && again.detail.contains("held"),
753            "an older fact is held, not refused: {again:?}"
754        );
755        let forged = run(&b, "main", Order::Entitlement(fact("other", 6, [9u8; 32])));
756        assert!(
757            !forged.ok && forged.detail.contains("does not verify"),
758            "{forged:?}"
759        );
760        assert!(!b.facts.borrow().contains_key("other"));
761        let twin = Fake {
762            twin: true,
763            ..Default::default()
764        };
765        let refused = run(
766            &twin,
767            "twin",
768            Order::Entitlement(fact("libs", 5, MONETIZE_SEED)),
769        );
770        assert!(
771            !refused.ok && refused.detail.contains("does not serve"),
772            "{refused:?}"
773        );
774    }
775
776    /// ★ **A resend sign-in link reaches the box that serves** (A16, Gunnar's S10): the order's
777    /// words go to the host as they are; a twin mints nothing; a box that cannot mint says so.
778    #[test]
779    fn a_sign_in_again_reaches_the_serving_box_and_a_twin_mints_nothing() {
780        let s = crate::order::SignInAgain {
781            account: "acme".into(),
782            to: "buyer@mock.invalid".into(),
783            host: "acme.holger.mock.invalid".into(),
784            ttl_secs: 86_400,
785        };
786        let b = Fake::default();
787        let a = run(&b, "main", Order::SignInAgain(s.clone()));
788        assert!(
789            a.ok && a.detail.contains("mailed to buyer@mock.invalid"),
790            "{a:?}"
791        );
792        assert_eq!(
793            b.did.borrow().as_slice(),
794            ["sign-in acme buyer@mock.invalid acme.holger.mock.invalid 86400"]
795        );
796        let twin = Fake {
797            twin: true,
798            ..Default::default()
799        };
800        let refused = run(&twin, "twin", Order::SignInAgain(s.clone()));
801        assert!(
802            !refused.ok
803                && refused.detail.contains("does not serve")
804                && twin.did.borrow().is_empty(),
805            "{refused:?}"
806        );
807        struct Bare;
808        impl HolgerBox for Bare {
809            fn serving(&self) -> bool {
810                true
811            }
812        }
813        let none = run(&Bare, "main", Order::SignInAgain(s));
814        assert!(
815            !none.ok && none.detail.contains("mints no sign-in link"),
816            "{none:?}"
817        );
818    }
819
820    /// ★ **HOL-PUBSITE-FEED (Gunnar's GRS-PUBLIC): a refeed's public-site switch is kept as the
821    /// box's generic setting** `console.public-site` (where an administrator's flip is kept), after
822    /// the keys kept beside the feed; the switch alone reaches only the setting. A box that keeps
823    /// no settings refuses by name and says what it kept before.
824    #[test]
825    fn a_refeeds_public_site_switch_is_kept_as_the_consoles_setting() {
826        let keys = |kv: &[(&str, &str)]| crate::order::Refeed {
827            keys: kv
828                .iter()
829                .map(|(k, v)| (k.to_string(), v.to_string()))
830                .collect(),
831        };
832        let b = Fake::default();
833        let a = run(
834            &b,
835            "main",
836            Order::Refeed(keys(&[
837                ("console_public_site", "on"),
838                ("public_repositories", "crates"),
839            ])),
840        );
841        assert!(a.ok && a.detail.contains("console.public-site"), "{a:?}");
842        assert_eq!(
843            b.did.borrow().as_slice(),
844            [
845                "refeed public_repositories",
846                "setting console.public-site on"
847            ]
848        );
849        let b = Fake::default();
850        let a = run(
851            &b,
852            "twin",
853            Order::Refeed(keys(&[("console_public_site", "off")])),
854        );
855        assert!(a.ok, "{a:?}");
856        assert_eq!(
857            b.did.borrow().as_slice(),
858            ["setting console.public-site off"],
859            "the switch alone keeps no key beside the feed"
860        );
861        struct KeepsKeysOnly;
862        impl HolgerBox for KeepsKeysOnly {
863            fn serving(&self) -> bool {
864                true
865            }
866            fn keep_refeed(&self, _: &crate::order::Refeed) -> Result<String, String> {
867                Ok("kept public_repositories".into())
868            }
869        }
870        let none = run(
871            &KeepsKeysOnly,
872            "main",
873            Order::Refeed(keys(&[
874                ("console_public_site", "on"),
875                ("public_repositories", "crates"),
876            ])),
877        );
878        assert!(
879            !none.ok
880                && none.detail.contains("keeps no settings")
881                && none
882                    .detail
883                    .contains("kept before it: kept public_repositories"),
884            "{none:?}"
885        );
886    }
887
888    /// ★ **MAIL-RELAY: a refeed reaches the box's keep**; a box that keeps none says so.
889    #[test]
890    fn a_refeed_reaches_the_boxes_keep_and_a_box_that_keeps_none_refuses() {
891        let r = crate::order::Refeed {
892            keys: [
893                ("secret.smtp_url", "smtps://u:tok@relay.mock.invalid:465"),
894                ("secret.smtp_from", "holger@mock.invalid"),
895                ("public_repositories", "crates"),
896            ]
897            .iter()
898            .map(|(k, v)| (k.to_string(), v.to_string()))
899            .collect(),
900        };
901        let b = Fake::default();
902        let a = run(&b, "main", Order::Refeed(r.clone()));
903        assert!(
904            a.ok && a.detail.contains("public_repositories") && !a.detail.contains("tok@"),
905            "{a:?}"
906        );
907        assert_eq!(
908            b.did.borrow().as_slice(),
909            ["refeed public_repositories,secret.smtp_from,secret.smtp_url"]
910        );
911        struct Bare;
912        impl HolgerBox for Bare {
913            fn serving(&self) -> bool {
914                true
915            }
916        }
917        let none = run(&Bare, "main", Order::Refeed(r));
918        assert!(
919            !none.ok && none.detail.contains("keeps no estate keys"),
920            "{none:?}"
921        );
922    }
923
924    #[test]
925    fn restate_entitlements_verifies_the_snapshot_and_counts_new_and_held() {
926        let b = Fake::default();
927        run(&b, "main", Order::Entitlement(fact("a", 5, MONETIZE_SEED)));
928        let facts = vec![fact("a", 5, MONETIZE_SEED), fact("b", 5, MONETIZE_SEED)];
929        let mut snap = monetize_embed::Snapshot {
930            issued_unix_ms: 7,
931            facts,
932            signature: Vec::new(),
933        };
934        snap.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
935            .sign(&monetize_embed::signing::snapshot_message(
936                snap.issued_unix_ms,
937                &snap.facts,
938            ))
939            .to_bytes()
940            .to_vec();
941        let a = run(&b, "main", Order::RestateEntitlements(snap.clone()));
942        assert!(
943            a.ok && a.detail.contains("restated 1") && a.detail.contains("1 already held"),
944            "{a:?}"
945        );
946        snap.signature = vec![0; 64];
947        let bad = run(&b, "main", Order::RestateEntitlements(snap));
948        assert!(!bad.ok && bad.detail.contains("does not verify"), "{bad:?}");
949    }
950
951    #[test]
952    fn purge_tenant_answers_monetizes_purge_answer() {
953        let b = Fake::default();
954        let a = run(
955            &b,
956            "main",
957            Order::PurgeTenant(PurgeTenant {
958                tenant: "libs".into(),
959                reason: "nonpay".into(),
960                job: String::new(),
961            }),
962        );
963        assert!(a.ok, "{a:?}");
964        let answer: PurgeAnswer = serde_json::from_str(&a.detail).unwrap();
965        assert_eq!(answer.job, "purge:libs");
966        assert_eq!(b.did.borrow().as_slice(), ["purge libs"]);
967    }
968
969    #[test]
970    fn add_principal_makes_a_repository_on_a_plan_and_says_taken_by_name() {
971        let b = Fake::default();
972        let a = run(
973            &b,
974            "main",
975            Order::AddPrincipal(AddPrincipal {
976                username: "crates".into(),
977                kind: "org".into(),
978                signup: "s-1".into(),
979            }),
980        );
981        assert!(
982            a.ok && a.detail == "crates",
983            "the namespace made is the repository: {a:?}"
984        );
985        let t = run(
986            &b,
987            "main",
988            Order::AddPrincipal(AddPrincipal {
989                username: "theirs".into(),
990                kind: "org".into(),
991                signup: "s-2".into(),
992            }),
993        );
994        assert!(!t.ok && t.detail.starts_with("taken"), "{t:?}");
995        let twin = Fake {
996            twin: true,
997            ..Default::default()
998        };
999        assert!(
1000            !run(
1001                &twin,
1002                "twin",
1003                Order::AddPrincipal(AddPrincipal {
1004                    username: "crates".into(),
1005                    kind: "org".into(),
1006                    signup: "s-1".into()
1007                })
1008            )
1009            .ok,
1010            "only the main that serves"
1011        );
1012    }
1013
1014    #[test]
1015    fn grow_room_is_kept_for_the_operator() {
1016        let b = Fake::default();
1017        let a = run(
1018            &b,
1019            "main",
1020            Order::GrowRoom(GrowRoom {
1021                need_bytes: 1 << 30,
1022                reason: "libs".into(),
1023            }),
1024        );
1025        assert!(a.ok, "{a:?}");
1026        assert_eq!(b.did.borrow().as_slice(), ["room 1073741824"]);
1027    }
1028
1029    #[test]
1030    fn grow_go_ahead_runs_twin_first_asks_monetize_and_remounts() {
1031        let b = Fake::default();
1032        let (m, k) = (me("main"), monetize_key());
1033        let mut ask = Asked(Vec::new(), true);
1034        let o = Order::GrowGoAhead(GrowGoAhead {
1035            go_ahead: growth("acme", 120, 99_000),
1036            data_gib: 120,
1037        });
1038        let a = execute(
1039            &b,
1040            &Ctx {
1041                me: &m,
1042                key: &k,
1043                now_unix_ms: 10_000,
1044            },
1045            6,
1046            &o,
1047            &mut ask,
1048        );
1049        assert!(a.ok, "{a:?}");
1050        assert_eq!(
1051            b.did.borrow().as_slice(),
1052            [
1053                "twin_holds 120",
1054                "start 120 pull-6-o-7",
1055                "settle false",
1056                "look_again false",
1057                "settle true"
1058            ]
1059        );
1060        assert_eq!(ask.0, [120]);
1061
1062        // Monetize does not answer: /data is mounted back and the ack says why.
1063        let b = Fake::default();
1064        let mut deaf = Asked(Vec::new(), false);
1065        b.settle.borrow_mut().extend([
1066            GrowState::WaitingMembers("w".into()),
1067            GrowState::Failed("mounted back".into()),
1068        ]);
1069        let a = execute(
1070            &b,
1071            &Ctx {
1072                me: &m,
1073                key: &k,
1074                now_unix_ms: 10_000,
1075            },
1076            7,
1077            &o,
1078            &mut deaf,
1079        );
1080        assert!(
1081            !a.ok && a.detail.contains("no answer") && a.detail.contains("mounted back"),
1082            "{a:?}"
1083        );
1084        assert!(b.did.borrow().contains(&"look_again true".to_string()));
1085    }
1086
1087    #[test]
1088    fn grow_go_ahead_refuses_another_estate_a_lapsed_step_and_a_forgery_and_skips_a_done_one() {
1089        let b = Fake::default();
1090        for (go, why) in [
1091            (growth("other", 120, 99_000), "estate other"),
1092            (growth("acme", 120, 5_000), "period is over"),
1093            (
1094                growth("acme", 120, 99_000).replace("\"o-7\"", "\"o-8\""),
1095                "does not verify",
1096            ),
1097        ] {
1098            let a = run(
1099                &b,
1100                "main",
1101                Order::GrowGoAhead(GrowGoAhead {
1102                    go_ahead: go,
1103                    data_gib: 120,
1104                }),
1105            );
1106            assert!(!a.ok && a.detail.contains(why), "{why}: {a:?}");
1107        }
1108        assert!(b.did.borrow().is_empty(), "nothing moved");
1109        let done = Fake {
1110            carried: true,
1111            ..Default::default()
1112        };
1113        let a = run(
1114            &done,
1115            "main",
1116            Order::GrowGoAhead(GrowGoAhead {
1117                go_ahead: growth("acme", 120, 99_000),
1118                data_gib: 120,
1119            }),
1120        );
1121        assert!(a.ok && a.detail.contains("nothing moved"), "{a:?}");
1122        let twin = Fake {
1123            twin: true,
1124            ..Default::default()
1125        };
1126        let a = run(
1127            &twin,
1128            "twin",
1129            Order::GrowGoAhead(GrowGoAhead {
1130                go_ahead: growth("acme", 120, 99_000),
1131                data_gib: 240,
1132            }),
1133        );
1134        assert!(a.detail.contains("no pull channel"), "{a:?}");
1135        assert!(
1136            !twin
1137                .did
1138                .borrow()
1139                .iter()
1140                .any(|d| d.starts_with("twin_holds")),
1141            "the twin grows first, asking no twin: {a:?}"
1142        );
1143    }
1144
1145    #[test]
1146    fn recharge_from_twin_runs_the_lane_on_a_cloud_fence_proof() {
1147        let b = Fake::default();
1148        let r = RechargeAsk {
1149            main: "main:1".into(),
1150            twin: "twin:1".into(),
1151            accept_lost: true,
1152            fenced: Some("upcloud: gone".into()),
1153            ..Default::default()
1154        };
1155        let a = run(&b, "twin", Order::RechargeFromTwin(r));
1156        assert!(a.ok && a.detail.starts_with("cloud-fenced"), "{a:?}");
1157        assert_eq!(
1158            b.did.borrow().as_slice(),
1159            ["recharge main:1 from twin:1 lost=true"]
1160        );
1161    }
1162
1163    /// ★ **Fence before recharge, by the pulled order** (M2, Gunnar's pattern): the old
1164    /// primary the order names is fenced first, and only a fence that held lets the
1165    /// recharge run; a fence that did not hold runs nothing; the main it fills is never
1166    /// the box it fences.
1167    #[test]
1168    fn recharge_from_twin_fences_the_old_primary_first() {
1169        let b = Fake::default();
1170        let r = RechargeAsk {
1171            main: "main:2".into(),
1172            twin: "twin:1".into(),
1173            accept_lost: false,
1174            epoch: 4,
1175            fence: Some("old:1".into()),
1176            ..Default::default()
1177        };
1178        let a = run(&b, "twin", Order::RechargeFromTwin(r.clone()));
1179        assert!(a.ok && a.detail.starts_with("fenced old:1; "), "{a:?}");
1180        assert_eq!(
1181            b.did.borrow().as_slice(),
1182            ["fence old:1 4", "recharge main:2 from twin:1 lost=false"]
1183        );
1184
1185        let stuck = Fake::default();
1186        let a = run(
1187            &stuck,
1188            "twin",
1189            Order::RechargeFromTwin(RechargeAsk {
1190                fence: Some("stuck:1".into()),
1191                ..r.clone()
1192            }),
1193        );
1194        assert!(
1195            !a.ok
1196                && a.detail.contains("stuck:1 answers nothing")
1197                && a.detail.contains("nothing was recharged"),
1198            "{a:?}"
1199        );
1200        assert_eq!(
1201            stuck.did.borrow().as_slice(),
1202            ["fence stuck:1 4"],
1203            "no recharge beside an unfenced primary"
1204        );
1205
1206        let same = Fake::default();
1207        let a = run(
1208            &same,
1209            "twin",
1210            Order::RechargeFromTwin(RechargeAsk {
1211                fence: Some("main:2".into()),
1212                ..r
1213            }),
1214        );
1215        assert!(
1216            !a.ok && a.detail.contains("never fences the box it fills"),
1217            "{a:?}"
1218        );
1219        assert!(same.did.borrow().is_empty(), "nothing ran");
1220    }
1221
1222    #[test]
1223    fn become_twin_runs_holgers_election() {
1224        let b = Fake {
1225            twin: true,
1226            ..Default::default()
1227        };
1228        assert!(
1229            run(
1230                &b,
1231                "twin",
1232                Order::BecomeTwin {
1233                    primary: "main:1".into()
1234                }
1235            )
1236            .ok
1237        );
1238        assert_eq!(b.did.borrow().as_slice(), ["become_twin main:1"]);
1239        struct Bare;
1240        impl HolgerBox for Bare {
1241            fn serving(&self) -> bool {
1242                false
1243            }
1244        }
1245        let none = run(
1246            &Bare,
1247            "twin",
1248            Order::BecomeTwin {
1249                primary: "main:1".into(),
1250            },
1251        );
1252        assert!(
1253            !none.ok && none.detail.contains("cannot become"),
1254            "the default refuses by name: {none:?}"
1255        );
1256    }
1257
1258    #[test]
1259    fn fence_primary_runs_the_boxes_fence_adopt_and_review_run() {
1260        let b = Fake::default();
1261        let a = run(
1262            &b,
1263            "twin",
1264            Order::FencePrimary {
1265                primary: "old:1".into(),
1266                epoch: 3,
1267            },
1268        );
1269        assert!(
1270            a.ok && b.did.borrow().contains(&"fence old:1 3".to_string()),
1271            "{a:?}"
1272        );
1273        struct Bare;
1274        impl HolgerBox for Bare {
1275            fn serving(&self) -> bool {
1276                false
1277            }
1278        }
1279        let none = run(
1280            &Bare,
1281            "twin",
1282            Order::FencePrimary {
1283                primary: "old:1".into(),
1284                epoch: 3,
1285            },
1286        );
1287        assert!(
1288            !none.ok && none.detail.contains("cannot fence"),
1289            "the default refuses by name: {none:?}"
1290        );
1291        let a = run(
1292            &b,
1293            "main",
1294            Order::AdoptTwin {
1295                twin: "twin:1".into(),
1296                principal: "twin".into(),
1297            },
1298        );
1299        assert!(
1300            a.ok && b.did.borrow().contains(&"adopt twin:1 twin".to_string()),
1301            "{a:?}"
1302        );
1303        let a = run(&b, "twin", Order::ReviewTwin);
1304        assert!(
1305            a.ok && a.detail == "the twin is whole" && a.receipt_json.contains("complete"),
1306            "{a:?}"
1307        );
1308    }
1309
1310    /// ★ MONETIZE-FIX: **one of Holger's own kinds runs through the box's verb** with its params;
1311    /// its file is PULLED first (a fetch that fails is said, and nothing runs); a twin runs none.
1312    #[test]
1313    fn a_product_order_runs_with_its_params_and_its_pulled_file() {
1314        use monetize_embed::fleet::{Fetch, ProductOrder};
1315        struct Shop(Result<Vec<u8>, String>);
1316        impl GrowSet for Shop {
1317            fn grow_set(&mut self, _: u64) -> Result<String, String> {
1318                Err("no".into())
1319            }
1320            fn fetch(&mut self, _: &Fetch) -> Result<Vec<u8>, String> {
1321                self.0.clone()
1322            }
1323        }
1324        let (m, k) = (me("main"), monetize_key());
1325        let ctx = Ctx {
1326            me: &m,
1327            key: &k,
1328            now_unix_ms: 10_000,
1329        };
1330        let b = Fake::default();
1331        let params: BTreeMap<String, String> = [("name".to_string(), "lab-1".to_string())].into();
1332        let enrol = Order::Product {
1333            kind: "downstream_enrol".into(),
1334            body: ProductOrder {
1335                params: params.clone(),
1336                fetch: None,
1337            },
1338        };
1339        let a = execute(&b, &ctx, 7, &enrol, &mut NoAsk);
1340        assert!(a.ok && a.detail.contains("downstream_enrol"), "{a:?}");
1341        let put = Order::Product {
1342            kind: "bundle_put".into(),
1343            body: ProductOrder {
1344                params,
1345                fetch: Some(Fetch {
1346                    id: "ab".repeat(32),
1347                    bytes: 3,
1348                    param: "bundle".into(),
1349                }),
1350            },
1351        };
1352        assert!(execute(&b, &ctx, 8, &put, &mut Shop(Ok(b"abc".to_vec()))).ok);
1353        let failed = execute(&b, &ctx, 9, &put, &mut Shop(Err("gone".into())));
1354        assert!(
1355            !failed.ok && failed.detail.contains("was not pulled"),
1356            "{failed:?}"
1357        );
1358        assert_eq!(
1359            b.did.borrow().len(),
1360            2,
1361            "a file not pulled runs nothing: {:?}",
1362            b.did.borrow()
1363        );
1364        assert!(b.did.borrow()[1].contains("Some(3)"));
1365        assert!(
1366            b.did.borrow()[1].ends_with("by 8"),
1367            "the box is told the verified order it runs for: {:?}",
1368            b.did.borrow()
1369        );
1370        let twin = Fake {
1371            twin: true,
1372            ..Fake::default()
1373        };
1374        assert!(
1375            !execute(&twin, &ctx, 10, &enrol, &mut NoAsk).ok,
1376            "a twin runs none"
1377        );
1378    }
1379
1380    /// ★ MONETIZE-FIX: Holger's own kinds decode as product orders; a bundle_put with no file,
1381    /// and a body that is not a product order, are refused by name.
1382    #[test]
1383    fn holgers_own_kinds_decode_and_a_bundle_needs_its_file() {
1384        use crate::order::{decode, holger_kind};
1385        for k in holger_kind::PRODUCT {
1386            if k == holger_kind::BUNDLE_PUT {
1387                continue;
1388            }
1389            assert!(
1390                matches!(
1391                    decode(1, k, r#"{"params":{"name":"x"}}"#),
1392                    Ok(Order::Product { .. })
1393                ),
1394                "{k}"
1395            );
1396        }
1397        assert!(
1398            decode(1, holger_kind::BUNDLE_PUT, r#"{"params":{"path":"b"}}"#)
1399                .unwrap_err()
1400                .contains("no file")
1401        );
1402        assert!(decode(1, holger_kind::DOWNSTREAM_ENROL, r#"[1]"#)
1403            .unwrap_err()
1404            .contains("does not decode"));
1405        assert!(decode(1, "downstream_destroy", "{}")
1406            .unwrap_err()
1407            .contains("does not know"));
1408    }
1409}