nordisk-pairing 0.5.26

The boxes of an estate find each other: a seed each box is installed with, a one-time join against the front, a tunnel (WireGuard by default) they meet inside, roles decided once, a pairing loop each box runs itself, and the signal it reports. Shared by gunnar and holger; the laptop's xtask only lays machines, writes seeds and reads the signal. Design: .nornir/pairing-design.md (2026-09-24).
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
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
//! **The front does the update its shop opened** (0.5.26, P4a; A22 W8, A28 §4 "update step"):
//! the ONE update step every product's front runs, moved here from Holger's front
//! (`holger-estate-front/src/update.rs`, the newer of the two: lane B5's [`Declared`] and its keep
//! fallback) and Gunnar's (`gunnar-front/src/update.rs`), which carried the same `decide`,
//! `on_trial`, `staged_of` and `wait_confirmed`. Feature `frontside-update`.
//!
//! The operator opens an update at the ONE box (`EstateOrder.UpdateEstate`); the front PULLS it
//! (`SoldEstate.UpdateTake`, behind [`Shop`]) and does it one box at a time, in the shop's order
//! twin → main → front, reporting each (`SoldEstate.UpdateReport`) before the next is handed out:
//!
//! ```text
//!   twin / main:  GET /live/running on the box's log door (the kept installer material)
//!     runs the release's app already         → done:nothing
//!     same BASE, live on, the release's app   → fetched (sha256, length, pin checked),
//!                                               POST /live/stage, waited for until confirmed
//!                                                                         → done:live
//!                                               the old app back, no trial  → failed (rolled back)
//!     a stage refused (korp's legs)           → failed (nothing moved)
//!     a stage refused for a keep boot only    → SoldEstate.Upgrade (keep medium) → done:keep
//!       (lane B5: the release env moved, a declaration the box keeps does not ride, or
//!        the box takes none live — `crate::live::falls_to_keep`)
//!     BASE moved, live off, no live app       → SoldEstate.Upgrade (keep medium) → done:keep
//!   front (itself, its own slot):
//!     runs it already (staged here before)    → done:live (else done:nothing)
//!     same BASE, live on                      → staged into its own slot: it re-execs, and
//!                                               reports on its next look, running the new app
//!     else                                    → needs-keep (the shop keep-boots the front)
//! ```
//!
//! [`step`] is the decision and the sequence over [`Hands`] (the boxes' live doors, the fetch,
//! the keep upgrade, a clock) — pure enough for tests; [`look`] is one look over a [`Shop`].
//! A take the shop refuses (out of turn, no update open) is its words. Nothing here names a
//! product: the front's keep medium is the product's words ([`Hands::front_keep`]).

use std::time::Duration;

use crate::live::{Declared, Running, Stage};

/// How often a staged box is read again.
pub const POLL_EVERY: Duration = Duration::from_secs(10);
/// How long a staged box has to confirm (its trial is 60 s of serving; korp-start rolls a box
/// that never serves back at 600 s).
pub const CONFIRM_BUDGET: Duration = Duration::from_secs(15 * 60);
/// A box running its OLD app with no trial this long after the stage was rolled back.
pub const ROLLED_BACK_AFTER: Duration = Duration::from_secs(60);
/// The marker the front leaves before it stages ITSELF: after its re-exec it reports `live`.
pub const STAGED_SELF_REL: &str = "update/staged-self";

/// One live app as the shop hands it (`EstateLiveApp`).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LiveApp {
    pub base: String,
    pub app: String,
    pub app_len: u64,
    /// hex
    pub sig: String,
    pub url: String,
    pub sha256: String,
    /// Lane B5: the release declaration's bundle and its signature, hex (empty: none / unsigned).
    pub release: String,
    pub release_sig: String,
    /// Lane B5: the release's `korp.env.*` tokens (empty: none declared).
    pub env: Vec<String>,
}

/// The update the shop handed out (`EstateUpdateTakeResponse`).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Take {
    pub open: bool,
    pub release: String,
    pub live: bool,
    /// `twin` | `main` | `front`.
    pub role: String,
    /// The live app main and twin run (the release's product app), while live is on.
    pub app: Option<LiveApp>,
    /// The front's own live app.
    pub front: Option<LiveApp>,
    pub words: String,
}

/// The ONE box's update door, as this front calls it.
pub trait Shop {
    /// `SoldEstate.UpdateTake`.
    fn take(&self) -> Result<Take, String>;
    /// `SoldEstate.UpdateReport`: `role`'s step, `outcome` and `words` signed together.
    fn report(&self, role: &str, outcome: &str, words: &str) -> Result<String, String>;
}

/// What this front can do to a box.
pub trait Hands {
    /// `GET /live/running` of `role` (the front: its own slot).
    fn running(&self, role: &str) -> Result<Running, String>;
    /// `POST /live/stage` to `role` with what the release declares (the front: its own slot —
    /// it then re-execs).
    fn stage(&self, role: &str, stage: &Stage, declared: &Declared) -> Result<String, String>;
    /// The archive at the presigned url.
    fn fetch(&self, url: &str) -> Result<Vec<u8>, String>;
    /// `role` (main or twin) upgraded from the release's keep medium and answering again.
    fn keep(&self, role: &str) -> Result<String, String>;
    /// Wait `d` (a test's clock moves instead).
    fn wait(&self, d: Duration);
    /// Was the front's own stage of `release` left here before its re-exec? (and forget it)
    fn staged_self(&self, release: &str) -> bool;
    /// Leave the marker before the front stages itself.
    fn mark_staged_self(&self, release: &str) -> Result<(), String>;
    /// **The product's words for the front's keep boot** (`needs-keep`): what the shop
    /// keep-boots, and from which medium (`<product>'s front from <product>-front-keep`).
    fn front_keep(&self) -> String {
        "the front from its keep medium".into()
    }
}

pub const DONE_KEEP: &str = "done:keep";
pub const DONE_LIVE: &str = "done:live";
pub const DONE_NOTHING: &str = "done:nothing";
pub const NEEDS_KEEP: &str = "needs-keep";
pub const FAILED: &str = "failed";

/// What is done to one box ([`decide`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Plan {
    /// It runs the release's app already.
    Nothing,
    /// The release's app staged live into its slot.
    Live,
    /// Its BASE moved, live is off, or the release has no live app: the keep medium.
    Keep(String),
    /// A trial is still open on it: asked again on the next look.
    Wait(String),
}

/// Whether a slot word still has a trial open (`b trial`, `b booted`).
fn on_trial(slot: &str) -> bool {
    slot.split_whitespace().nth(1).is_some()
}

/// Pure: **what one box gets** — its running app against the release's live app.
pub fn decide(r: &Running, app: Option<&LiveApp>, live: bool) -> Plan {
    let Some(app) = app else {
        return Plan::Keep(if live { "the release carries no live app for it".into() } else { "live updates are OFF for this estate".into() });
    };
    if on_trial(&r.slot) {
        return Plan::Wait(format!("[live-trial-open] slot {}: its last update is not confirmed yet", r.slot));
    }
    if r.app.trim() == app.app.trim() {
        return Plan::Nothing;
    }
    if !live {
        return Plan::Keep("live updates are OFF for this estate".into());
    }
    if !r.live_update {
        return Plan::Keep("live update is OFF on the box (korp.live_update=off, its BASE)".into());
    }
    if r.base.trim() != app.base.trim() {
        return Plan::Keep(format!("its BASE moved ({} → {}): a base update", short(&r.base), short(&app.base)));
    }
    Plan::Live
}

fn short(h: &str) -> &str {
    &h[..h.len().min(12)]
}

fn unhex(s: &str) -> Result<Vec<u8>, String> {
    let s = s.trim();
    if !s.len().is_multiple_of(2) {
        return Err("odd hex".into());
    }
    (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())).collect()
}

/// **The release's app, fetched and proven** — its sha256 as the shop named it, its length and
/// its pin as the release recorded — and the stage the box takes.
pub fn staged_of(h: &dyn Hands, app: &LiveApp) -> Result<Stage, String> {
    if app.sig.trim().is_empty() {
        return Err("[update-unsigned] the release's live app carries no signature: never staged".into());
    }
    let archive = h.fetch(&app.url)?;
    let sha: String = ring::digest::digest(&ring::digest::SHA256, &archive).as_ref().iter().map(|b| format!("{b:02x}")).collect();
    if !app.sha256.trim().is_empty() && sha != app.sha256.trim() {
        return Err(format!("[update-fetch] the archive's sha256 is {}, the release says {}", short(&sha), short(&app.sha256)));
    }
    if archive.len() as u64 != app.app_len {
        return Err(format!("[update-fetch] the archive is {} bytes, the release says {}", archive.len(), app.app_len));
    }
    let pin = blake3::hash(&archive).to_hex().to_string();
    if pin != app.app.trim() {
        return Err(format!("[update-fetch] the archive's pin is {}, the release says {}", short(&pin), short(&app.app)));
    }
    Ok(Stage { pin_hex: app.app.trim().to_string(), signature: unhex(&app.sig).map_err(|e| format!("[update-unsigned] the signature: {e}"))?, base: app.base.trim().to_string(), archive })
}

/// **What the release declares beside `app`** (lane B5): its release declaration and its env,
/// as the box is handed them. A declaration that is not hex is refused by name.
pub fn declared_of(app: &LiveApp) -> Result<Declared, String> {
    let release = match app.release.trim() {
        "" => None,
        b => Some((unhex(b).map_err(|e| format!("[update-release] the release declaration: {e}"))?, unhex(&app.release_sig).map_err(|e| format!("[update-release] its signature: {e}"))?)),
    };
    Ok(Declared { release, env: (!app.env.is_empty()).then(|| app.env.clone()) })
}

/// **Wait until `role` runs `app` confirmed**; the old app back with no trial is a rollback.
pub fn wait_confirmed(h: &dyn Hands, role: &str, app: &str, old: &str) -> Result<String, String> {
    let mut waited = Duration::ZERO;
    let mut last = String::new();
    while waited < CONFIRM_BUDGET {
        h.wait(POLL_EVERY);
        waited += POLL_EVERY;
        match h.running(role) {
            Ok(r) if r.app.trim() == app.trim() && !on_trial(&r.slot) => return Ok(format!("the {role} runs app {} confirmed (slot {})", short(app), r.slot)),
            Ok(r) if r.app.trim() == old.trim() && !on_trial(&r.slot) && waited >= ROLLED_BACK_AFTER => {
                return Err(format!("[update-rolled-back] the {role} runs its old app {} again (slot {}): the new one never confirmed", short(old), r.slot))
            }
            Ok(r) => last = format!("slot {}, app {}", r.slot, short(&r.app)),
            // The box is down while it re-execs: asked again.
            Err(e) => last = e,
        }
    }
    Err(format!("[update-not-confirmed] the {role} did not confirm within {} min ({last})", CONFIRM_BUDGET.as_secs() / 60))
}

/// What a step reports: the outcome word and the words.
pub type Report = (String, String);

/// **One box's step**: the box whose turn it is, updated, and what to report — `None` when
/// nothing is to be reported now (no update open; a trial still open; the front staged itself
/// and reports after its re-exec).
pub fn step(h: &dyn Hands, t: &Take) -> Option<Report> {
    if !t.open || t.role.is_empty() {
        return None;
    }
    let role = t.role.as_str();
    if !matches!(role, "twin" | "main" | "front") {
        return Some((FAILED.into(), format!("this front updates twin, main and front, not {role:?}")));
    }
    // The box this step works on is BUSY until it ends: the recharge watcher leaves it alone
    // (lane G5, A7 row 10; Gunnar aa14357b).
    let _busy = crate::frontside::busy::on(role);
    let done = |how: &str, words: String| Some((format!("done:{how}"), words));
    let failed = |words: String| Some((FAILED.to_string(), words));
    let front = role == "front";
    let app = if front { t.front.as_ref() } else { t.app.as_ref() };
    let running = match h.running(role) {
        Ok(r) => r,
        // A box with no live door answers nothing live: its keep medium (the fallback).
        Err(e) if !front && e.contains("[live-unsupported]") => {
            return match h.keep(role) {
                Ok(w) => done("keep", format!("its live door: {e}; {w}")),
                Err(k) => failed(format!("its live door: {e}; the keep upgrade: {k}")),
            }
        }
        Err(e) if front => return Some((NEEDS_KEEP.into(), format!("the front's own slot: {e}"))),
        Err(e) => return failed(format!("its live door did not answer: {e}")),
    };
    match decide(&running, app, t.live) {
        Plan::Wait(_) => None,
        Plan::Nothing => {
            let how = if front && h.staged_self(&t.release) { "live" } else { "nothing" };
            done(how, format!("the {role} runs app {}", short(&running.app)))
        }
        Plan::Keep(why) if front => Some((NEEDS_KEEP.into(), format!("{why}: the shop keep-boots {}", h.front_keep()))),
        Plan::Keep(why) => match h.keep(role) {
            Ok(w) => done("keep", format!("{why}: {w}")),
            Err(e) => failed(format!("{why}; the keep upgrade of the {role}: {e}")),
        },
        Plan::Live => {
            let app = app.expect("Live is decided on an app");
            let (stage, declared) = match staged_of(h, app).and_then(|s| declared_of(app).map(|d| (s, d))) {
                Ok(s) => s,
                Err(e) => return failed(e),
            };
            if front {
                if let Err(e) = h.mark_staged_self(&t.release) {
                    return failed(format!("the marker before its own stage: {e}"));
                }
                return match h.stage(role, &stage, &declared) {
                    // It re-execs now; its next look reports.
                    Ok(_) => None,
                    // Only its keep boot lays what the release moved: no live update of its own.
                    Err(e) if crate::live::falls_to_keep(&e) => {
                        h.staged_self(&t.release);
                        Some((NEEDS_KEEP.into(), format!("its own slot refused the live stage ({}): the shop keep-boots {}", e.trim(), h.front_keep())))
                    }
                    Err(e) => failed(format!("its own stage was refused: {e}")),
                };
            }
            match h.stage(role, &stage, &declared) {
                Ok(_) => {}
                // Only a keep boot lays what the release moved (lane B5).
                Err(e) if crate::live::falls_to_keep(&e) => {
                    let why = format!("the {role} refused the live stage ({})", e.trim());
                    return match h.keep(role) {
                        Ok(w) => done("keep", format!("{why}: {w}")),
                        Err(k) => failed(format!("{why}; the keep upgrade of the {role}: {k}")),
                    };
                }
                Err(e) => return failed(format!("the stage was refused: {e}")),
            }
            match wait_confirmed(h, role, &app.app, &running.app) {
                Ok(w) => done("live", w),
                Err(e) => failed(e),
            }
        }
    }
}

/// **One look**: take, do the box whose turn it is, report it. `Ok(None)`: nothing to report
/// now. Answers `(role, outcome, the shop's words)`.
pub fn look(shop: &dyn Shop, h: &dyn Hands) -> Result<Option<(String, String, String)>, String> {
    let t = shop.take()?;
    let Some((outcome, words)) = step(h, &t) else { return Ok(None) };
    let said = shop.report(&t.role, &outcome, &words)?;
    Ok(Some((t.role, outcome, said)))
}

/// **Where a box reaches a release object the bench named on its own loopback** (the mock's
/// guestfwd answers the host's ports at 10.0.2.100). Any other url is as given.
pub fn reachable(url: &str) -> String {
    match url.strip_prefix("http://127.0.0.1:") {
        Some(rest) => format!("http://10.0.2.100:{rest}"),
        None => url.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    use std::collections::BTreeMap;

    const OLD: &str = "0101010101010101010101010101010101010101010101010101010101010101";

    fn archive() -> Vec<u8> {
        (0..4096u32).map(|i| (i % 251) as u8).collect()
    }

    fn app_of(base: &str) -> LiveApp {
        let a = archive();
        LiveApp {
            base: base.into(),
            app: blake3::hash(&a).to_hex().to_string(),
            app_len: a.len() as u64,
            sig: "ab".repeat(80),
            url: "https://objsto.example/release/holger/03.live-app.znippy?X-Amz=…".into(),
            sha256: ring::digest::digest(&ring::digest::SHA256, &a).as_ref().iter().map(|b| format!("{b:02x}")).collect(),
            ..LiveApp::default()
        }
    }

    fn running(base: &str, app: &str, slot: &str) -> Running {
        Running { base: base.into(), app: app.into(), slot: slot.into(), live_update: true }
    }

    /// A fake estate: each box's running app, what a stage does to it, the keep upgrade.
    #[derive(Default)]
    struct Fake {
        boxes: RefCell<BTreeMap<String, Running>>,
        /// The app a staged box comes back with (`None`: it rolls back).
        comes_back: RefCell<Option<String>>,
        refuse_stage: Option<&'static str>,
        no_door: bool,
        staged: RefCell<Vec<String>>,
        declared: RefCell<Vec<Declared>>,
        kept: RefCell<Vec<String>>,
        marker: RefCell<Option<String>>,
        waited: RefCell<Duration>,
        down_polls: RefCell<u32>,
    }

    impl Hands for Fake {
        fn running(&self, role: &str) -> Result<Running, String> {
            if self.no_door {
                return Err("REFUSED [live-unsupported] this VM takes no live update".into());
            }
            if *self.down_polls.borrow() > 0 {
                *self.down_polls.borrow_mut() -= 1;
                return Err("connection refused (re-exec)".into());
            }
            self.boxes.borrow().get(role).cloned().ok_or_else(|| "no such box".into())
        }
        fn stage(&self, role: &str, stage: &Stage, declared: &Declared) -> Result<String, String> {
            self.declared.borrow_mut().push(declared.clone());
            if let Some(w) = self.refuse_stage {
                return Err(w.into());
            }
            self.staged.borrow_mut().push(role.into());
            // A box is down while it re-execs; the front reads its own slot, never down.
            if role != "front" {
                *self.down_polls.borrow_mut() = 2;
            }
            let mut b = self.boxes.borrow_mut();
            let r = b.get_mut(role).unwrap();
            if let Some(app) = self.comes_back.borrow().clone() {
                assert_eq!(app, stage.pin_hex);
                r.app = app;
            }
            r.slot = "a".into();
            Ok("staged".into())
        }
        fn fetch(&self, _: &str) -> Result<Vec<u8>, String> {
            Ok(archive())
        }
        fn keep(&self, role: &str) -> Result<String, String> {
            self.kept.borrow_mut().push(role.into());
            Ok(format!("the {role} was booted from the release's keep medium"))
        }
        fn wait(&self, d: Duration) {
            *self.waited.borrow_mut() += d;
        }
        fn staged_self(&self, release: &str) -> bool {
            self.marker.borrow_mut().take().is_some_and(|r| r == release)
        }
        fn mark_staged_self(&self, release: &str) -> Result<(), String> {
            *self.marker.borrow_mut() = Some(release.into());
            Ok(())
        }
    }

    fn take(role: &str, live: bool) -> Take {
        let base = "b".repeat(64);
        Take { open: true, release: "03".into(), live, role: role.into(), app: live.then(|| app_of(&base)), front: live.then(|| app_of(&base)), words: String::new() }
    }

    /// ★ Same BASE, live on: the twin gets the release's app live — fetched and proven, staged,
    /// waited for through its re-exec until it runs it confirmed.
    #[test]
    fn same_base_is_staged_live_and_waited_for_until_confirmed() {
        let f = Fake::default();
        let app = app_of(&"b".repeat(64));
        f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
        *f.comes_back.borrow_mut() = Some(app.app.clone());
        let (o, w) = step(&f, &take("twin", true)).expect("reported");
        assert_eq!(o, DONE_LIVE, "{w}");
        assert!(w.contains("confirmed"), "{w}");
        assert_eq!(*f.staged.borrow(), vec!["twin".to_string()]);
        assert!(f.kept.borrow().is_empty());
    }

    /// ★ The box comes back on its OLD app with no trial: rolled back — reported failed; a stage
    /// korp's legs refuse is failed too, and nothing is kept in its place.
    #[test]
    fn a_box_that_rolls_back_or_refuses_the_stage_is_reported_failed() {
        let f = Fake::default();
        f.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), OLD, "a"));
        let (o, w) = step(&f, &take("main", true)).unwrap();
        assert_eq!(o, FAILED);
        assert!(w.contains("[update-rolled-back]"), "{w}");
        assert!(*f.waited.borrow() >= ROLLED_BACK_AFTER);
        let r = Fake { refuse_stage: Some("[live-verify] the signature leg"), ..Fake::default() };
        r.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), OLD, "a"));
        let (o, w) = step(&r, &take("main", true)).unwrap();
        assert_eq!(o, FAILED);
        assert!(w.contains("[live-verify]") && r.kept.borrow().is_empty(), "{w}");
    }

    /// ★ BASE moved, live off, the box's own switch, or a box with no live door: the keep
    /// medium through the shop's Upgrade — never a stage.
    #[test]
    fn a_moved_base_live_off_the_boxs_switch_or_no_live_door_go_through_the_keep_medium() {
        for (t, r, no_door) in [
            (take("twin", true), running(&"c".repeat(64), OLD, "a"), false),
            (take("twin", false), running(&"b".repeat(64), OLD, "a"), false),
            (take("twin", true), Running { live_update: false, ..running(&"b".repeat(64), OLD, "a") }, false),
            (take("twin", true), running(&"b".repeat(64), OLD, "a"), true),
        ] {
            let f = Fake { no_door, ..Fake::default() };
            f.boxes.borrow_mut().insert("twin".into(), r);
            let (o, w) = step(&f, &t).unwrap();
            assert_eq!(o, DONE_KEEP, "{w}");
            assert!(f.staged.borrow().is_empty());
            assert_eq!(*f.kept.borrow(), vec!["twin".to_string()]);
        }
    }

    /// ★ The front stages ITSELF and reports after its re-exec (`live`); its BASE moved, it asks
    /// the shop to keep-boot it (`needs-keep`); a box on trial is left for the next look.
    #[test]
    fn the_front_stages_itself_and_reports_after_its_reexec_or_asks_for_its_keep_boot() {
        let f = Fake::default();
        let app = app_of(&"b".repeat(64));
        f.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
        *f.comes_back.borrow_mut() = Some(app.app.clone());
        assert_eq!(step(&f, &take("front", true)), None, "staged: it re-execs, nothing reported now");
        assert_eq!(f.marker.borrow().as_deref(), Some("03"));
        *f.down_polls.borrow_mut() = 0;
        let (o, _) = step(&f, &take("front", true)).unwrap();
        assert_eq!(o, DONE_LIVE, "the marker says it was this front's own live update");
        let moved = Fake::default();
        moved.boxes.borrow_mut().insert("front".into(), running(&"c".repeat(64), OLD, "a"));
        assert_eq!(step(&moved, &take("front", true)).unwrap().0, NEEDS_KEEP);
        assert!(moved.kept.borrow().is_empty(), "the front never upgrades itself through Upgrade");
        let trial = Fake::default();
        trial.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "b booted"));
        assert_eq!(step(&trial, &take("twin", true)), None);
    }

    /// ★ **What the release declares rides the stage** (lane B5): the release declaration
    /// (bundle and signature, hex as the shop hands them) and the release's env reach the box;
    /// an app that declares neither stages neither.
    #[test]
    fn the_release_declaration_and_env_ride_the_stage() {
        let f = Fake::default();
        let mut app = app_of(&"b".repeat(64));
        app.release = "6b6f7270".into();
        app.release_sig = "77".repeat(64);
        app.env = vec!["korp.env.HOLGER_DATA=/data".into()];
        f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
        *f.comes_back.borrow_mut() = Some(app.app.clone());
        let t = Take { app: Some(app), ..take("twin", true) };
        assert_eq!(step(&f, &t).unwrap().0, DONE_LIVE);
        assert_eq!(f.declared.borrow()[0], Declared { release: Some((b"korp".to_vec(), vec![0x77; 64])), env: Some(vec!["korp.env.HOLGER_DATA=/data".into()]) });
        let plain = Fake::default();
        plain.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
        step(&plain, &take("twin", true));
        assert_eq!(plain.declared.borrow()[0], Declared::default(), "nothing declared");
    }

    /// ★ **A box that refuses a stage only a keep boot answers** (its release env moved, its
    /// declaration cannot ride, it takes none live) is keep-booted: `done:keep` for main and
    /// twin, `needs-keep` for the front (its marker forgotten); a declaration that does not hold
    /// stays a failure.
    #[test]
    fn a_keep_refusal_of_the_stage_is_answered_with_a_keep_boot() {
        for w in ["REFUSED [live-env-moved] the release changes korp.env.*", "REFUSED [live-release-absent] none rides", "REFUSED [live-declared-unsupported] x"] {
            let f = Fake { refuse_stage: Some(w), ..Fake::default() };
            f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
            let (o, said) = step(&f, &take("twin", true)).unwrap();
            assert_eq!(o, DONE_KEEP, "{said}");
            assert_eq!(*f.kept.borrow(), vec!["twin".to_string()]);
            let front = Fake { refuse_stage: Some(w), ..Fake::default() };
            front.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
            let (o, said) = step(&front, &take("front", true)).unwrap();
            assert_eq!(o, NEEDS_KEEP, "{said}");
            assert!(front.marker.borrow().is_none(), "the front's marker is forgotten: its keep boot is no live update");
        }
        let f = Fake { refuse_stage: Some("REFUSED [release-sig] does not hold"), ..Fake::default() };
        f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
        assert_eq!(step(&f, &take("twin", true)).unwrap().0, FAILED);
        assert!(f.kept.borrow().is_empty());
    }

    /// ★ The fetched archive must be the release's — sha256, length, pin — and signed.
    #[test]
    fn an_archive_that_is_not_the_releases_is_never_staged() {
        let f = Fake::default();
        let mut bad = app_of(&"b".repeat(64));
        bad.app = "9".repeat(64);
        assert!(staged_of(&f, &bad).unwrap_err().contains("pin"));
        let mut unsigned = app_of(&"b".repeat(64));
        unsigned.sig.clear();
        assert!(staged_of(&f, &unsigned).unwrap_err().contains("[update-unsigned]"));
        let mut sha = app_of(&"b".repeat(64));
        sha.sha256 = "0".repeat(64);
        assert!(staged_of(&f, &sha).unwrap_err().contains("sha256"));
        assert!(step(&Fake::default(), &Take { open: false, ..take("twin", true) }).is_none());
    }

    /// The ONE box's update, in memory: its turn order twin → main → front, refusing a report
    /// out of turn by name, `open = false` once the front reported.
    struct FakeShop {
        turn: RefCell<Vec<&'static str>>,
        reported: RefCell<Vec<(String, String)>>,
        refuse_take: Option<&'static str>,
        live: bool,
    }

    impl FakeShop {
        fn open(live: bool) -> FakeShop {
            FakeShop { turn: RefCell::new(vec!["twin", "main", "front"]), reported: RefCell::default(), refuse_take: None, live }
        }
    }

    impl Shop for FakeShop {
        fn take(&self) -> Result<Take, String> {
            if let Some(w) = self.refuse_take {
                return Err(format!("refused: {w}"));
            }
            Ok(match self.turn.borrow().first() {
                Some(r) => take(r, self.live),
                None => Take::default(),
            })
        }
        fn report(&self, role: &str, outcome: &str, _: &str) -> Result<String, String> {
            let mut turn = self.turn.borrow_mut();
            if turn.first() != Some(&role) {
                return Err(format!("refused: [update-out-of-turn] {role} reported, the turn is {:?}", turn.first()));
            }
            turn.remove(0);
            self.reported.borrow_mut().push((role.into(), outcome.into()));
            Ok(format!("{role}: {outcome} recorded"))
        }
    }

    /// ★ Live off: twin then main are kept and the front needs its keep boot, in the shop's order.
    #[test]
    fn live_off_twin_then_main_are_kept_and_the_front_needs_keep_in_the_shops_order() {
        let shop = FakeShop::open(false);
        let h = Fake::default();
        for r in ["twin", "main", "front"] {
            h.boxes.borrow_mut().insert(r.into(), running(&"b".repeat(64), OLD, "a"));
        }
        for _ in 0..4 {
            look(&shop, &h).expect("every look is in turn");
        }
        assert_eq!(shop.reported.borrow().iter().map(|(r, o)| format!("{r} {o}")).collect::<Vec<_>>(), ["twin done:keep", "main done:keep", "front needs-keep"]);
        assert_eq!(h.kept.borrow().as_slice(), ["twin", "main"], "the front never upgrades itself");
        assert_eq!(look(&shop, &h).unwrap(), None, "nothing open: nothing reported");
    }

    /// ★ Live on: twin, main and front all take the release live, in the shop's order.
    #[test]
    fn live_on_twin_main_and_front_take_the_release_live() {
        let shop = FakeShop::open(true);
        let h = Fake::default();
        for r in ["twin", "main", "front"] {
            h.boxes.borrow_mut().insert(r.into(), running(&"b".repeat(64), OLD, "a"));
        }
        *h.comes_back.borrow_mut() = Some(app_of(&"b".repeat(64)).app);
        for _ in 0..4 {
            look(&shop, &h).expect("every look is in turn");
        }
        assert_eq!(shop.reported.borrow().iter().map(|(r, o)| format!("{r} {o}")).collect::<Vec<_>>(), ["twin done:live", "main done:live", "front done:live"]);
        assert!(h.kept.borrow().is_empty());
    }

    #[test]
    fn a_take_refused_or_an_unknown_role_is_said_by_name() {
        let shop = FakeShop { refuse_take: Some("[update-none-open] no update is open for acme"), ..FakeShop::open(false) };
        assert!(look(&shop, &Fake::default()).unwrap_err().contains("update-none-open"));
        assert_eq!(step(&Fake::default(), &Take { open: true, role: "nas".into(), ..Default::default() }).map(|r| r.0).as_deref(), Some(FAILED));
    }

    /// ★ The bench's loopback bucket is reached at the mock's guestfwd; a real url is as given.
    #[test]
    fn a_bench_loopback_release_object_is_reached_at_the_mocks_guestfwd() {
        assert_eq!(reachable("http://127.0.0.1:17463/objsto/x?X=1"), "http://10.0.2.100:17463/objsto/x?X=1");
        assert_eq!(reachable("https://objsto.example/holger-release/x?X=1"), "https://objsto.example/holger-release/x?X=1");
    }

    /// ★ **While a step runs its box is BUSY** — the recharge watcher leaves it alone — and only
    /// then (lane G5, A7 row 10; Gunnar aa14357b `a_box_is_busy_while_its_step_runs_and_only_then`).
    #[test]
    fn a_box_is_busy_while_its_step_runs_and_only_then() {
        struct Peek(Fake);
        impl Hands for Peek {
            fn running(&self, role: &str) -> Result<Running, String> {
                assert!(crate::frontside::busy::busy(Some(role)) && crate::frontside::busy::busy(None), "the step's own box is busy");
                self.0.running(role)
            }
            fn stage(&self, role: &str, s: &Stage, d: &Declared) -> Result<String, String> {
                self.0.stage(role, s, d)
            }
            fn fetch(&self, u: &str) -> Result<Vec<u8>, String> {
                self.0.fetch(u)
            }
            fn keep(&self, role: &str) -> Result<String, String> {
                assert!(crate::frontside::busy::busy(Some(role)), "busy through its keep boot");
                self.0.keep(role)
            }
            fn wait(&self, d: Duration) {
                self.0.wait(d)
            }
            fn staged_self(&self, r: &str) -> bool {
                self.0.staged_self(r)
            }
            fn mark_staged_self(&self, r: &str) -> Result<(), String> {
                self.0.mark_staged_self(r)
            }
        }
        let p = Peek(Fake { no_door: true, ..Fake::default() });
        assert_eq!(step(&p, &take("twin", true)).unwrap().0, DONE_KEEP);
    }

    /// ★ P4a: **the front's keep boot is said in the product's words** ([`Hands::front_keep`]);
    /// a product that names none gets the neutral words. Nothing else in the step names a product.
    #[test]
    fn the_fronts_keep_boot_is_said_in_the_products_words() {
        struct Named(Fake);
        impl Hands for Named {
            fn running(&self, role: &str) -> Result<Running, String> {
                self.0.running(role)
            }
            fn stage(&self, role: &str, s: &Stage, d: &Declared) -> Result<String, String> {
                self.0.stage(role, s, d)
            }
            fn fetch(&self, u: &str) -> Result<Vec<u8>, String> {
                self.0.fetch(u)
            }
            fn keep(&self, role: &str) -> Result<String, String> {
                self.0.keep(role)
            }
            fn wait(&self, d: Duration) {
                self.0.wait(d)
            }
            fn staged_self(&self, r: &str) -> bool {
                self.0.staged_self(r)
            }
            fn mark_staged_self(&self, r: &str) -> Result<(), String> {
                self.0.mark_staged_self(r)
            }
            fn front_keep(&self) -> String {
                "Acme's front from acme-front-keep".into()
            }
        }
        let named = Named(Fake::default());
        named.0.boxes.borrow_mut().insert("front".into(), running(&"c".repeat(64), OLD, "a"));
        let (o, w) = step(&named, &take("front", true)).unwrap();
        assert_eq!(o, NEEDS_KEEP);
        assert!(w.ends_with("the shop keep-boots Acme's front from acme-front-keep"), "{w}");
        let refused = Named(Fake { refuse_stage: Some("REFUSED [live-env-moved] x"), ..Fake::default() });
        refused.0.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
        let (o, w) = step(&refused, &take("front", true)).unwrap();
        assert_eq!(o, NEEDS_KEEP);
        assert!(w.contains("[live-env-moved]") && w.ends_with("the shop keep-boots Acme's front from acme-front-keep"), "{w}");
        let plain = Fake::default();
        plain.boxes.borrow_mut().insert("front".into(), running(&"c".repeat(64), OLD, "a"));
        assert!(step(&plain, &take("front", true)).unwrap().1.ends_with("the shop keep-boots the front from its keep medium"));
    }
}