monetize-estate-side 0.3.3

The estate side of the ONE monetize's wire, shared by every product: the box pump (Fleet.Report / Orders / Ask: the box key, verify, run through the product's executor, ack; the jera round) and the front's signed SoldEstate client (Upgrade, Recharge, TakeMaterial, UpdateTake, UpdateReport) behind a swappable transport. The product writes only its executor and its words.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
//! **The pull channel as a jera job** (house rule: server background work is a jera
//! ChainQueue job, never a raw thread). One job = one round: report, long-poll the
//! orders (≤ 25 s), verify and run each, and report again at once when anything ran so
//! the acks reach monetize. A small ticker submits the round under ONE idempotency key,
//! so a round is never queued twice; the job's ledger row says what it did.

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

use jera::jobs::{ChainQueue, ChainSpec, JobSink, Priority};
use monetize_embed::fleet::LONG_POLL_MAX_MS;

use super::puller::PumpBox;
use super::puller::{Link, Puller};

/// The ledger kind of one round.
pub const KIND: &str = "fleet.round";
/// The one key every round is submitted under.
pub const KEY: &str = "fleet-round";

/// **The role this box pulls as, asked at every round** (A7 row 1): nordisk-pairing
/// `elect::pull_role` over what the box knows NOW. `None`: not elected yet, the round waits
/// and reports nothing.
pub type RoleNow = Arc<dyn Fn() -> Option<String> + Send + Sync>;

/// Everything one round needs.
pub struct Duty {
    pub puller: Mutex<Puller>,
    pub link: Mutex<Box<dyn Link + Send>>,
    pub boxed: Arc<dyn PumpBox + Send + Sync>,
    /// The key orders verify under: replaced live when a later feed names one
    /// ([`Duty::set_key`]).
    monetize_pubkey: RwLock<Option<[u8; 32]>>,
    /// The role at each round; `None` = the puller's own, fixed (tests, an enrolment).
    role: Option<RoleNow>,
    /// The pause before the next round, as monetize asked (ms).
    pub pause_ms: AtomicU64,
    /// When this channel started: an `unauthenticated` refusal inside
    /// `nordisk_pairing::fleet::NOT_YET_KNOWN_GRACE` of it is "not known yet" ([`after_round`]).
    started: std::time::Instant,
    /// The `serving` and `data` words of the last report monetize TOOK (`None`: none taken
    /// yet): the first one taken, and every change of either word, is said at [`SAID_AT`]
    /// ([`accepted_line`]; lane F12: test3-holger-rs's main said nothing once its reports landed).
    accepted: Mutex<Option<(String, String)>>,
}

impl Duty {
    pub fn new(
        puller: Puller,
        link: Box<dyn Link + Send>,
        boxed: Arc<dyn PumpBox + Send + Sync>,
        monetize_pubkey: Option<[u8; 32]>,
    ) -> Arc<Duty> {
        Duty::with_role(puller, link, boxed, monetize_pubkey, None)
    }

    /// [`Duty::new`] whose role is asked at every round (`role`).
    pub fn with_role(
        puller: Puller,
        link: Box<dyn Link + Send>,
        boxed: Arc<dyn PumpBox + Send + Sync>,
        monetize_pubkey: Option<[u8; 32]>,
        role: Option<RoleNow>,
    ) -> Arc<Duty> {
        Arc::new(Duty {
            puller: Mutex::new(puller),
            link: Mutex::new(link),
            boxed,
            monetize_pubkey: RwLock::new(monetize_pubkey),
            role,
            pause_ms: AtomicU64::new(super::config::DEFAULT_EVERY.as_millis() as u64),
            started: std::time::Instant::now(),
            accepted: Mutex::new(None),
        })
    }

    /// The key orders verify under now.
    pub fn monetize_pubkey(&self) -> Option<[u8; 32]> {
        *self
            .monetize_pubkey
            .read()
            .unwrap_or_else(|e| e.into_inner())
    }

    /// **A key fed after the start**, applied from the next round on (no restart).
    pub fn set_key(&self, key: Option<[u8; 32]>) {
        *self
            .monetize_pubkey
            .write()
            .unwrap_or_else(|e| e.into_inner()) = key;
    }

    /// **A door fed after the start** (another `fleet`, `fleet_at`, `fleet_ca` or estate):
    /// the next round dials `link` as `instance`.
    pub fn relink(&self, link: Box<dyn Link + Send>, instance: &str) {
        *self.link.lock().unwrap_or_else(|e| e.into_inner()) = link;
        self.puller
            .lock()
            .unwrap_or_else(|e| e.into_inner())
            .me
            .instance = instance.to_string();
    }

    /// **One round**, and what it did (the job row's detail).
    pub fn round(&self, wait_ms: u64) -> anyhow::Result<serde_json::Value> {
        let mut p = self.puller.lock().unwrap_or_else(|e| e.into_inner());
        if let Some(role) = &self.role {
            match role() {
                None => {
                    return Ok(
                        serde_json::json!({ "reported": false, "waiting": "the installer's door has not elected this box's role yet: nothing is reported under a guess" }),
                    )
                }
                Some(r) if r != p.me.role => {
                    log::warn!(
                        "fleet: this box now reports as `{r}` (it reported as `{}`)",
                        p.me.role
                    );
                    p.me.role = r;
                }
                Some(_) => {}
            }
        }
        let mut link = self.link.lock().unwrap_or_else(|e| e.into_inner());
        let b = self.boxed.as_ref();
        let now = crate::now_unix_ms;
        let key = self.monetize_pubkey();
        let health = b.health();
        let words = (
            health.get("serving").cloned().unwrap_or_default(),
            health.get("data").cloned().unwrap_or_default(),
        );
        let reported = p.report(
            link.as_mut(),
            health,
            b.product_report(),
            b.own_review(),
            now(),
        );
        if reported.is_ok() {
            let mut last = self.accepted.lock().unwrap_or_else(|e| e.into_inner());
            if let Some(line) = accepted_line(last.as_ref(), &words, &p.me.role) {
                log::log!(SAID_AT, "{line}");
            }
            *last = Some(words);
        }
        if let Ok(next) = &reported {
            if *next > 0 {
                self.pause_ms.store(
                    (*next).clamp(1_000, super::config::DEFAULT_EVERY.as_millis() as u64),
                    Ordering::Relaxed,
                );
            }
        }
        let pulled = p.pull(
            link.as_mut(),
            b,
            key.as_ref(),
            wait_ms.min(LONG_POLL_MAX_MS),
            now(),
        );
        let ran = pulled.as_ref().map(|x| !x.acks.is_empty()).unwrap_or(false);
        // The acks ride a report at once: the guardian waits on them. One that does not land
        // is said (the acks stay pending and ride the next round's report).
        let acks_reported = ran.then(|| {
            p.report(
                link.as_mut(),
                b.health(),
                b.product_report(),
                b.own_review(),
                now(),
            )
        });
        let acks_report_error = acks_reported
            .as_ref()
            .and_then(|r| r.as_ref().err())
            .map(|f| f.to_string());
        if let Some(e) = &acks_report_error {
            log::log!(SAID_AT, "fleet: the report carrying this round's acks FAILED: {e} — they ride the next round's report");
        }
        if let Ok(x) = &pulled {
            for a in &x.acks {
                if a.ok {
                    log::info!("fleet: monetize's order {} ran: {}", a.seq, a.detail);
                } else {
                    log::warn!(
                        "fleet: monetize's order {} was refused: {}",
                        a.seq,
                        a.detail
                    );
                }
            }
            for f in &x.forged {
                log::error!("fleet: an order that is NOT monetize's was offered and ignored: {f}");
            }
        }
        if let Some(line) = report_round_line(&reported) {
            // A7 row 31 (Gunnar 59fa3800): "not known yet" is said at INFO and asked again
            // within 10 s; past the grace, or any other refusal, at WARN.
            let pause = Duration::from_millis(self.pause_ms.load(Ordering::Relaxed));
            let (level, pause) = after_round(&reported, self.started.elapsed(), pause);
            self.pause_ms
                .store(pause.as_millis() as u64, Ordering::Relaxed);
            if level == log::Level::Info {
                log::info!("{line} (monetize does not know this box yet: the estate's owner opens its window as the install feeds it; asked again in {} s)", pause.as_secs());
            } else {
                log::log!(level, "{line}");
            }
        }
        let report_err = reported.as_ref().err().map(|f| f.to_string());
        let detail = serde_json::json!({
            "reported": reported.is_ok(),
            "report_error": report_err,
            "acks_report_error": acks_report_error,
            "acks": pulled.as_ref().map(|x| x.acks.iter().map(|a| serde_json::json!({"seq": a.seq, "ok": a.ok, "detail": a.detail})).collect::<Vec<_>>()).unwrap_or_default(),
            "forged": pulled.as_ref().map(|x| x.forged.len()).unwrap_or(0),
            "pull_error": pulled.as_ref().err().map(|f| f.to_string()),
        });
        match (&reported, &pulled) {
            (Err(r), Err(o)) if o.code != "no_key" => {
                Err(anyhow::anyhow!("report: {r}; orders: {o}"))
            }
            _ => Ok(detail),
        }
    }
}

/// **The level the pull channel's own story is said at** (lane R10): an estate box logs at
/// `warn` by default (holger-server-lib `DEFAULT_LOG_FILTER`), so the channel's start, every
/// refused or failed report round and the boot log's `STAGE report` are said at WARN —
/// at `info` none of them was ever seen on a box (test3-holger-rs, 2026-10-20; Holger's lane R10).
pub const SAID_AT: log::Level = log::Level::Warn;

/// Pure: **the level a refused round is said at, and the pause before the next** (A7 row 31,
/// Gunnar 59fa3800): nordisk-pairing `fleet::after_refusal` over the refusal's code and the
/// time since the channel started. `unauthenticated` inside the grace (monetize does not know
/// this box YET: the owner's pin window opens as the install feeds it) is said at INFO and
/// asked again within 10 s; anything else at [`SAID_AT`], after `pause`.
pub fn after_round(
    reported: &Result<u64, super::puller::Fault>,
    since_start: Duration,
    pause: Duration,
) -> (log::Level, Duration) {
    match reported {
        Err(f) => match nordisk_pairing::fleet::after_refusal(&f.code, since_start, pause) {
            (p, true) => (log::Level::Info, p),
            (p, false) => (SAID_AT, p),
        },
        Ok(_) => (SAID_AT, pause),
    }
}

/// Pure: **what a report round says when it did not land** — monetize's refusal in its own
/// words (`refused`, `unauthenticated`, `malformed`), or why the call failed; `None` when
/// the report was taken.
pub fn report_round_line(reported: &Result<u64, super::puller::Fault>) -> Option<String> {
    let f = reported.as_ref().err()?;
    Some(match f.code.as_str() {
        "refused" | "unauthenticated" | "malformed" => format!(
            "fleet: monetize REFUSED this round's report ({}): {}",
            f.code, f.message
        ),
        _ => format!(
            "fleet: this round's report FAILED ({}): {} — the next round tries again",
            f.code, f.message
        ),
    })
}

/// Pure: **what a report monetize TOOK says** (lane F12): the first one taken, and any
/// later one whose `serving` or `data` word differs from the last one taken; `None` for a
/// report that says what the last one said (a quiet channel is a working one). `words` is
/// (`serving`, `data`); an absent word is said as `nothing`.
pub fn accepted_line(
    last: Option<&(String, String)>,
    words: &(String, String),
    role: &str,
) -> Option<String> {
    let shown = |v: &str| {
        if v.trim().is_empty() {
            "nothing".to_string()
        } else {
            v.trim().to_string()
        }
    };
    let (serving, data) = (shown(&words.0), shown(&words.1));
    match last {
        None => Some(format!("fleet: monetize TOOK this box's first report as `{role}`: serving = {serving}, data = {data}")),
        Some(l) if l != words => Some(format!("fleet: monetize took this box's report as `{role}`: serving = {serving} (was {}), data = {data} (was {})", shown(&l.0), shown(&l.1))),
        Some(_) => None,
    }
}

/// The spec of one round, in `product`'s jera workspace.
pub fn spec(generation: i64, product: &str) -> ChainSpec {
    ChainSpec::new(
        KIND,
        KEY,
        "fleet",
        generation,
        product,
        serde_json::json!({}),
    )
    .with_target("the ONE monetize's pull channel")
    .with_priority(Priority::Normal)
}

/// **Start the pull channel** on `rt`: a ChainQueue of its own (a held long-poll never
/// waits behind other work, nor other work behind it) and the ticker that submits rounds.
pub fn start(rt: &tokio::runtime::Handle, sink: JobSink, duty: Arc<Duty>) -> Arc<ChainQueue> {
    let q = ChainQueue::new(sink);
    let d = Arc::clone(&duty);
    // A32-14, stays a task: the pull channel's jera worker.
    rt.spawn(Arc::clone(&q).run(move |_spec| d.round(LONG_POLL_MAX_MS)));
    let tick = Arc::clone(&q);
    let product = duty
        .puller
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .me
        .product
        .clone();
    // A32-14, stays a task: the pull channel's ticker (it submits the jera rounds).
    rt.spawn(async move {
        let mut generation = 0i64;
        loop {
            generation += 1;
            let _ = tick.submit(spec(generation, &product));
            let pause = duty.pause_ms.load(Ordering::Relaxed).min(5_000);
            tokio::time::sleep(Duration::from_millis(pause)).await;
        }
    });
    q
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pump::key::BoxKey;
    use crate::pump::puller::{Addressee, Asker, Ctx, PumpBox};
    use crate::pump::puller::{Fault, Signed};
    use monetize_embed::fleet::SignedOrder;

    /// A link that records the role of every call and answers nothing.
    struct Heard(Arc<Mutex<Vec<String>>>);
    impl Link for Heard {
        fn report(&mut self, s: Signed) -> Result<u64, crate::pump::puller::Fault> {
            self.0
                .lock()
                .unwrap()
                .push(format!("report {}/{}", s.call.instance, s.call.role));
            Ok(1_000)
        }
        fn orders(&mut self, s: Signed) -> Result<Vec<SignedOrder>, Fault> {
            self.0
                .lock()
                .unwrap()
                .push(format!("orders {}/{}", s.call.instance, s.call.role));
            Ok(Vec::new())
        }
        fn ask(&mut self, _: Signed) -> Result<crate::pump::puller::Asked, Fault> {
            Err(Fault::new("none", "no asks here"))
        }
    }

    struct Bare;
    impl PumpBox for Bare {
        fn run(
            &self,
            _: &Ctx,
            o: &SignedOrder,
            _: &mut dyn Asker,
        ) -> monetize_embed::fleet::OrderAck {
            monetize_embed::fleet::OrderAck {
                seq: o.seq,
                ok: false,
                detail: "a bare box runs nothing".into(),
                receipt_json: String::new(),
            }
        }
    }

    fn duty(role: Option<RoleNow>, heard: &Arc<Mutex<Vec<String>>>) -> Arc<Duty> {
        let me = Addressee {
            product: "a-product".into(),
            instance: "acme".into(),
            role: "main".into(),
        };
        let p = Puller::new(me, BoxKey::from_seed([3; 32]), "test");
        Duty::with_role(
            p,
            Box::new(Heard(Arc::clone(heard))),
            Arc::new(Bare),
            Some([9; 32]),
            role,
        )
    }

    /// ★ A7 row 1: the role is asked at every round. A box the door has not elected
    /// reports NOTHING; elected twin after its start, it reports as `twin`, never as `main`.
    #[test]
    fn a_box_elected_twin_after_its_start_reports_as_the_twin() {
        let heard = Arc::new(Mutex::new(Vec::new()));
        let elected: Arc<Mutex<Option<Option<&'static str>>>> = Arc::new(Mutex::new(Some(None)));
        let e = Arc::clone(&elected);
        let role: RoleNow =
            Arc::new(move || nordisk_pairing::elect::pull_role("", false, *e.lock().unwrap()));
        let d = duty(Some(role), &heard);
        d.round(0).unwrap();
        assert!(
            heard.lock().unwrap().is_empty(),
            "not elected yet: nothing reported under a guess, {:?}",
            heard.lock().unwrap()
        );
        *elected.lock().unwrap() = Some(Some("twin"));
        d.round(0).unwrap();
        assert_eq!(
            *heard.lock().unwrap(),
            vec!["report acme/twin", "orders acme/twin"]
        );
    }

    /// ★ P4 (lane mon-shared) RED->GREEN: the report that carries a run order's ack back is
    /// never dropped. Its failure is said, and the round's row names it; the ack stays
    /// pending and rides the next round's report.
    #[test]
    fn a_failed_acks_report_is_said_and_the_ack_rides_the_next_round() {
        struct AcksLost(u32);
        impl Link for AcksLost {
            fn report(&mut self, _: Signed) -> Result<u64, Fault> {
                self.0 += 1;
                if self.0 == 2 {
                    Err(Fault::new("unavailable", "the door went away"))
                } else {
                    Ok(1_000)
                }
            }
            fn orders(&mut self, _: Signed) -> Result<Vec<SignedOrder>, Fault> {
                Ok(vec![crate::pump::puller::tests::signed(
                    4,
                    "main",
                    "note",
                    "{}",
                    crate::pump::puller::tests::MONETIZE_SEED,
                )])
            }
            fn ask(&mut self, _: Signed) -> Result<crate::pump::puller::Asked, Fault> {
                Err(Fault::new("none", "no asks here"))
            }
        }
        let me = Addressee {
            product: "a-product".into(),
            instance: "acme".into(),
            role: "main".into(),
        };
        let p = Puller::new(me, BoxKey::from_seed([3; 32]), "test");
        let key = crate::pump::puller::tests::monetize_key().to_bytes();
        let d = Duty::with_role(p, Box::new(AcksLost(0)), Arc::new(Bare), Some(key), None);
        let row = d.round(0).unwrap();
        assert_eq!(row["acks"].as_array().map(|a| a.len()), Some(1), "{row}");
        let said = row["acks_report_error"].as_str().unwrap_or_default();
        assert!(
            said.contains("the door went away"),
            "the failed acks report is in the row: {row}"
        );
        assert_eq!(
            d.puller.lock().unwrap().pulled.pending.len(),
            1,
            "the ack stays pending for the next report"
        );
    }

    /// A key and a door fed after the start reach the next round (no restart).
    #[test]
    fn a_late_key_and_a_new_door_reach_the_next_round() {
        let heard = Arc::new(Mutex::new(Vec::new()));
        let d = duty(None, &heard);
        d.set_key(Some([1; 32]));
        assert_eq!(d.monetize_pubkey(), Some([1; 32]));
        let moved = Arc::new(Mutex::new(Vec::new()));
        d.relink(Box::new(Heard(Arc::clone(&moved))), "acme2");
        d.round(0).unwrap();
        assert!(heard.lock().unwrap().is_empty());
        assert_eq!(
            *moved.lock().unwrap(),
            vec!["report acme2/main", "orders acme2/main"]
        );
    }

    /// ★ R10 RED→GREEN: a box logs at `warn` by default, so the pull channel's story is said
    /// at WARN or it is never seen.
    #[test]
    fn the_pull_channels_story_is_said_at_warn() {
        assert!(
            SAID_AT <= log::Level::Warn,
            "{SAID_AT:?} is dropped by a box's default `warn` filter"
        );
    }

    /// ★ F12 (test3-holger-rs 2026-10-21: after its pin its main said nothing more): the first
    /// report monetize TOOK is said, and so is every change of its `serving` or `data` word; a
    /// report that says what the last one said is quiet. The duty keeps the words it last had
    /// taken, and a refused round keeps none.
    #[test]
    fn the_first_report_taken_and_every_change_of_its_words_is_said() {
        let w = |s: &str, d: &str| (s.to_string(), d.to_string());
        let first =
            accepted_line(None, &w("yes", "ok"), "main").expect("the first report taken is said");
        assert!(
            first.contains("TOOK")
                && first.contains("`main`")
                && first.contains("serving = yes")
                && first.contains("data = ok"),
            "{first}"
        );
        assert_eq!(
            accepted_line(Some(&w("yes", "ok")), &w("yes", "ok"), "main"),
            None,
            "the same words again are quiet"
        );
        let changed = accepted_line(Some(&w("yes", "ok")), &w("no", "fresh"), "main")
            .expect("a change is said");
        assert!(
            changed.contains("serving = no (was yes)") && changed.contains("data = fresh (was ok)"),
            "{changed}"
        );
        assert!(accepted_line(None, &w("", ""), "twin")
            .unwrap()
            .contains("serving = nothing, data = nothing"));

        let heard = Arc::new(Mutex::new(Vec::new()));
        let d = duty(None, &heard);
        assert!(d.accepted.lock().unwrap().is_none());
        d.round(0).unwrap();
        assert!(
            d.accepted.lock().unwrap().is_some(),
            "an accepted round keeps the words it said"
        );
    }

    /// ★ A7 row 31 (Gunnar 59fa3800): `unauthenticated` inside the grace is "not known yet":
    /// said at INFO, asked again within 10 s. Past the grace, or any other refusal: WARN and
    /// the pause as before. The duty's own round keeps the pause it chose.
    #[test]
    fn a_box_not_known_yet_is_said_quietly_and_asked_again_soon_inside_the_grace() {
        let every = Duration::from_secs(30);
        let unauth: Result<u64, Fault> = Err(Fault::new(
            "unauthenticated",
            "monetize does not know this box key",
        ));
        assert_eq!(
            after_round(&unauth, Duration::from_secs(60), every),
            (log::Level::Info, Duration::from_secs(10))
        );
        assert_eq!(
            after_round(
                &unauth,
                nordisk_pairing::fleet::NOT_YET_KNOWN_GRACE + Duration::from_secs(1),
                every
            ),
            (SAID_AT, every),
            "past the grace: WARN"
        );
        assert_eq!(
            after_round(
                &Err(Fault::new("unavailable", "down")),
                Duration::from_secs(60),
                every
            ),
            (SAID_AT, every)
        );
        assert_eq!(
            after_round(&Ok(30_000), Duration::from_secs(60), every).1,
            every,
            "a taken report keeps the pause"
        );
        // A duty refused `unauthenticated` at its first round pauses at most 10 s.
        struct Refused;
        impl Link for Refused {
            fn report(&mut self, _: Signed) -> Result<u64, Fault> {
                Err(Fault::new("unauthenticated", "not known"))
            }
            fn orders(&mut self, _: Signed) -> Result<Vec<SignedOrder>, Fault> {
                Err(Fault::new("unauthenticated", "not known"))
            }
            fn ask(&mut self, _: Signed) -> Result<crate::pump::puller::Asked, Fault> {
                Err(Fault::new("none", "no asks here"))
            }
        }
        let me = Addressee {
            product: "a-product".into(),
            instance: "acme".into(),
            role: "main".into(),
        };
        let d = Duty::new(
            Puller::new(me, BoxKey::from_seed([3; 32]), "test"),
            Box::new(Refused),
            Arc::new(Bare),
            Some([9; 32]),
        );
        let _ = d.round(0);
        assert!(
            d.pause_ms.load(Ordering::Relaxed) <= 10_000,
            "{}",
            d.pause_ms.load(Ordering::Relaxed)
        );
    }

    /// ★ R10 RED→GREEN: every refused or failed report round is said, with monetize's words.
    #[test]
    fn a_refused_or_failed_report_round_is_said_with_monetizes_words() {
        let refused = report_round_line(&Err(Fault::new(
            "refused",
            "no estate acme-7 is sold to this box key",
        )))
        .unwrap();
        assert!(
            refused.starts_with("fleet: ")
                && refused.contains("REFUSED")
                && refused.contains("no estate acme-7 is sold to this box key"),
            "{refused}"
        );
        let failed = report_round_line(&Err(Fault::new(
            "unavailable",
            "tcp connect error: Connection refused",
        )))
        .unwrap();
        assert!(
            failed.contains("FAILED")
                && failed.contains("unavailable")
                && failed.contains("tcp connect error"),
            "{failed}"
        );
        assert_eq!(
            report_round_line(&Ok(30_000)),
            None,
            "a taken report says nothing"
        );
    }

    /// ★ P4a: a round is a jera job in the PRODUCT's workspace (it was always "holger"), under
    /// the one idempotency key.
    #[test]
    fn a_round_is_one_job_in_the_products_workspace() {
        let s = spec(7, "a-product");
        assert_eq!(
            (
                s.kind.as_str(),
                s.key.as_str(),
                s.group.as_str(),
                s.generation,
                s.workspace.as_str()
            ),
            (KIND, KEY, "fleet", 7, "a-product")
        );
        assert_eq!(s.target, "the ONE monetize's pull channel");
    }
}