crates/installer/nordisk-pairing/src/frontside/update.rs
//! **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
//! its own stage rolled back (the marker, → failed (rolled back), never staged again
//! the old app, no trial)
//! 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>,
/// feed-big (2026-11-04): the release declaration BY REFERENCE (`release` empty) — its
/// object's presigned url, sha256 and length (Holger 3.2's carries the signed CVE bundle).
pub release_url: String,
pub release_sha256: String,
pub release_len: u64,
}
/// 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()
}
/// `roles` (main, twin or both) RE-ADMITTED from the kept bundle where they stand, in ONE
/// run (`crate::kept::readmit_together`): the second half of a keep upgrade, for boxes that
/// booted their upgraded system and were never re-admitted (Gunnar's front, gunnar.rs
/// 2026-10-25; every product's since T16). A front that keeps no bundle refuses by name.
fn readmit(&self, roles: &[&str]) -> Result<String, String> {
Err(format!("REFUSED [readmit-unsupported] this front re-admits no box ({})", roles.join(" and ")))
}
}
/// The refusal of a box's OPEN install listener (`crate::bootstrap::respond`: no `/live/*`
/// there): the box booted a fresh system and waits for its installer's feed.
pub const OPEN_INSTALL_REFUSAL: &str = "[install-path]";
/// Pure: **does `role`'s live door answer as a box waiting for its re-admission?** A main or
/// twin whose keep medium ran and whose upgraded system booted (its pairing state cleared, its
/// data kept) answers `GET /live/running` with the open listener's refusal until the front feeds
/// it (gunnar.rs main, 2026-10-25). Moved from Gunnar's front (T16).
pub fn awaits_readmission(role: &str, door_said: &str) -> bool {
role != "front" && door_said.contains(OPEN_INSTALL_REFUSAL)
}
/// What the re-admission done first left: the box read again, the re-admission's words, and
/// whether this step's own box was among the re-admitted.
type Readmitted = (Result<Running, String>, Option<String>, bool);
/// **Boxes left waiting for their re-admission are re-admitted FIRST, and TOGETHER** (Gunnar's
/// front, gunnar.rs 2026-10-25; T16 for every product): this box and the other box of the
/// estate alike, in one [`Hands::readmit`]. A re-admission re-feeds every other box of the kept
/// plan, and a box at its open install listener never takes a re-feed (the twin's keep upgrade
/// waited 3606 s on such a main). Then this box is read again; the step goes on from what it
/// runs. A door that answers anything else is left to the step as it answered.
fn readmit_first(h: &dyn Hands, role: &str, first: Result<Running, String>) -> Result<Readmitted, Report> {
let peer = if role == "main" { "twin" } else { "main" };
let mut waiting: Vec<&str> = Vec::new();
let mut said = String::new();
match &first {
Err(e) if awaits_readmission(role, e) => {
waiting.push(role);
said = e.clone();
}
Err(_) => return Ok((first, None, false)),
Ok(_) => {}
}
let peer_busy = crate::frontside::busy::on(peer);
if let Err(e) = h.running(peer) {
if awaits_readmission(peer, &e) {
waiting.push(peer);
if said.is_empty() {
said = e;
}
}
}
if waiting.is_empty() {
return Ok((first, None, false));
}
let back = h.readmit(&waiting).map_err(|why| (FAILED.to_string(), format!("{} answer(s) as a box never re-admitted after its keep boot ({said}); the re-admission: {why}", waiting.join(" and "))))?;
drop(peer_busy);
let me = waiting.contains(&role);
let again = if me { h.running(role) } else { first };
match again {
Ok(r) => Ok((Ok(r), Some(back), me)),
Err(again) => Err((FAILED.to_string(), format!("its keep boot was never re-admitted: {back}; its live door did not answer after: {again}"))),
}
}
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()) })
}
/// **What the release declares, its declaration fetched when it rides by reference**
/// (feed-big): the object at `release_url` proven by `release_sha256` and `release_len`, never
/// staged otherwise; inline or none: [`declared_of`].
pub fn declared_fetched(h: &dyn Hands, app: &LiveApp) -> Result<Declared, String> {
let want = app.release_sha256.trim();
if want.is_empty() {
return declared_of(app);
}
if app.release_url.trim().is_empty() {
return Err(format!("[update-release-fetch] the release declaration rides by reference ({}) and the shop named no object", short(want)));
}
let bytes = h.fetch(&app.release_url).map_err(|e| format!("[update-release-fetch] {e}"))?;
let sha: String = ring::digest::digest(&ring::digest::SHA256, &bytes).as_ref().iter().map(|b| format!("{b:02x}")).collect();
if sha != want || bytes.len() as u64 != app.release_len {
return Err(format!("[update-release-fetch] the release declaration fetched is {} ({} bytes), the release says {} ({} bytes)", short(&sha), bytes.len(), short(want), app.release_len));
}
let sig = unhex(&app.release_sig).map_err(|e| format!("[update-release] its signature: {e}"))?;
Ok(Declared { release: Some((bytes, sig)), 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 mut first = h.running(role);
let (mut readmitted, mut self_readmitted) = (None, false);
if !front {
match readmit_first(h, role, first) {
Ok((again, back, me)) => (first, readmitted, self_readmitted) = (again, back, me),
Err(report) => return Some(report),
}
}
// The words of a re-admission done first, before what this step did.
let first_words = readmitted.as_ref().map(|b| format!("re-admitted first: {b}; ")).unwrap_or_default();
let running = match first {
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 => {
if let Some(back) = readmitted.filter(|_| self_readmitted) {
return done("keep", format!("its keep boot was never re-admitted: {back}; the {role} runs app {}", short(&running.app)));
}
let how = if front && h.staged_self(&t.release) { "live" } else { "nothing" };
done(how, format!("{first_words}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!("{first_words}{why}: {w}")),
Err(e) => failed(format!("{first_words}{why}; the keep upgrade of the {role}: {e}")),
},
Plan::Live => {
let app = app.expect("Live is decided on an app");
// A32-8: the front staged THIS release into its own slot before (the marker), and runs
// its old app again with no trial: korp rolled the new one back. Said once, as a box's
// rollback is; never staged again on every look.
if front && h.staged_self(&t.release) {
return failed(format!(
"[update-rolled-back] the front runs its old app {} again (slot {}): its own stage of release {} never confirmed",
short(&running.app),
running.slot,
t.release
));
}
let (stage, declared) = match staged_of(h, app).and_then(|s| declared_fetched(h, 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>,
/// Boxes whose live door is the OPEN install listener (a keep boot never re-admitted).
open_door: RefCell<Vec<String>>,
readmitted: RefCell<Vec<String>>,
/// The re-admission refused with these words.
readmit_refused: RefCell<Option<String>>,
}
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());
}
if self.open_door.borrow().iter().any(|r| r == role) {
return Err(OPEN_LISTENER_404.into());
}
self.boxes.borrow().get(role).cloned().ok_or_else(|| "no such box".into())
}
fn readmit(&self, roles: &[&str]) -> Result<String, String> {
if let Some(e) = self.readmit_refused.borrow().clone() {
return Err(e);
}
// The re-admission re-feeds every OTHER box of the kept plan, and one at its open
// install listener never takes a re-feed: the run ends at its budget.
if let Some(other) = self.open_door.borrow().iter().find(|r| !roles.contains(&r.as_str())) {
return Err(waited_on(other));
}
self.readmitted.borrow_mut().push(roles.join("+"));
self.open_door.borrow_mut().retain(|r| !roles.contains(&r.as_str()));
Ok(format!("{} re-admitted by the front after its upgrade (its data kept); 3 box(es) closed", roles.join(" and ")))
}
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, url: &str) -> Result<Vec<u8>, String> {
if url.contains("-live-release/") {
return Ok(b"korp".to_vec());
}
Ok(archive())
}
fn keep(&self, role: &str) -> Result<String, String> {
// The keep upgrade's second half is the same re-admission (Gunnar's `upgrade_one`).
if let Some(other) = self.open_door.borrow().iter().find(|r| *r != role) {
return Err(waited_on(other));
}
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(())
}
}
/// What gunnar.rs's main answered on `GET /live/running` (2026-10-25 03:54Z): its keep medium
/// had run, the cloud then put the server in error state, the box booted its upgraded system
/// and waited for its re-admission — its door the OPEN install listener (`bootstrap::respond`).
const OPEN_LISTENER_404: &str = "REFUSED [install-path] this listener has /install/hello, /install/log, /install/feed, /install/status, /install/offer, /install/secret-offer and /install/close";
/// What the twin's keep upgrade answered on gunnar.rs (2026-10-25 11:08Z): its re-admission
/// re-fed the main, which sat at its open install listener.
fn waited_on(other: &str) -> String {
format!("REFUSED [installer-incomplete] after 3606 s: front: no signal; {other}: never answered (last: {OPEN_LISTENER_404}); twin: pair twin: tunnel wireguard up · joined · role twin · PAIRED")
}
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);
}
/// ★ **The front's own stage that rolls back is reported failed, not staged again** (A32-8):
/// the front staged itself (the marker left), re-exec'd, its trial failed and korp rolled it
/// back: it runs its OLD app with no trial. The next look must say so once, and must not
/// stage the same release into its own slot on every look for ever.
#[test]
fn the_fronts_own_stage_that_rolls_back_is_reported_failed_not_restaged() {
let f = Fake::default();
f.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
// `comes_back` None: the new app never confirms; korp rolls the slot back to OLD.
assert_eq!(step(&f, &take("front", true)), None, "staged: it re-execs, nothing reported now");
assert_eq!(f.staged.borrow().len(), 1);
let (o, w) = step(&f, &take("front", true)).expect("the rollback is reported on the next look");
assert_eq!(o, FAILED, "{w}");
assert!(w.contains("[update-rolled-back]") && w.contains("03"), "{w}");
assert_eq!(f.staged.borrow().len(), 1, "the release was not staged into its own slot again");
assert!(f.marker.borrow().is_none(), "the marker is spent");
}
/// ★ **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");
}
/// ★ feed-big (2026-11-04): **a release declaration by reference is fetched, proven by its
/// sha256 and length, and rides the stage** — Holger 3.2's declaration carries the signed CVE
/// bundle (~70 MB), so the shop hands its object's url, never the bytes; bytes that are not
/// the release's are never staged.
#[test]
fn a_release_declaration_by_reference_is_fetched_proven_and_staged() {
let f = Fake::default();
let mut app = app_of(&"b".repeat(64));
app.release_url = "https://objsto.example/release/holger-live-release/k.korp-release?X-Amz=…".into();
app.release_sha256 = ring::digest::digest(&ring::digest::SHA256, b"korp").as_ref().iter().map(|b| format!("{b:02x}")).collect();
app.release_len = 4;
app.release_sig = "77".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 t = Take { app: Some(app.clone()), ..take("twin", true) };
assert_eq!(step(&f, &t).unwrap().0, DONE_LIVE);
assert_eq!(f.declared.borrow()[0].release, Some((b"korp".to_vec(), vec![0x77; 64])));
let wrong = Fake::default();
wrong.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
let mut bad = app;
bad.release_sha256 = "0".repeat(64);
let (o, said) = step(&wrong, &Take { app: Some(bad), ..take("twin", true) }).unwrap();
assert_eq!(o, FAILED, "{said}");
assert!(said.contains("[update-release-fetch]") && wrong.staged.borrow().is_empty(), "{said}");
}
/// ★ **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"));
}
// ── T16: Gunnar's re-admission first (moved from gunnar-front/src/update.rs) ──
/// Pure: only a main or twin at its OPEN install listener awaits its re-admission.
#[test]
fn only_a_box_at_its_open_install_listener_awaits_its_readmission() {
assert!(awaits_readmission("main", OPEN_LISTENER_404));
assert!(awaits_readmission("twin", OPEN_LISTENER_404));
assert!(!awaits_readmission("front", OPEN_LISTENER_404), "the front is never re-admitted by itself");
assert!(!awaits_readmission("main", "connection refused"));
}
/// ★ **A box whose keep boot was never re-admitted** (gunnar.rs main, 2026-10-25): the front
/// re-admits it from the kept bundle, reads it again and goes on: it runs the release's app
/// already → done:keep, nothing staged, no second keep.
#[test]
fn a_box_left_waiting_for_its_readmission_is_readmitted_then_read_again() {
let f = Fake::default();
let app = app_of(&"b".repeat(64));
f.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), &app.app, "a"));
f.open_door.borrow_mut().push("main".into());
let (o, w) = step(&f, &take("main", true)).expect("reported");
assert_eq!(o, DONE_KEEP, "{w}");
assert!(w.contains("re-admitted") && w.contains("never re-admitted"), "{w}");
assert_eq!(*f.readmitted.borrow(), vec!["main".to_string()]);
assert!(f.kept.borrow().is_empty(), "no second keep boot: {w}");
assert!(f.staged.borrow().is_empty());
}
/// ★ Re-admitted, but not on the release (its BASE moved): on to the keep medium as any box.
#[test]
fn a_readmitted_box_that_is_not_on_the_release_goes_on_to_its_keep_upgrade() {
let f = Fake::default();
f.boxes.borrow_mut().insert("main".into(), running(&"c".repeat(64), OLD, "a"));
f.open_door.borrow_mut().push("main".into());
let (o, w) = step(&f, &take("main", true)).expect("reported");
assert_eq!(o, DONE_KEEP, "{w}");
assert_eq!(*f.readmitted.borrow(), vec!["main".to_string()]);
assert_eq!(*f.kept.borrow(), vec!["main".to_string()]);
}
/// ★ A re-admission that fails is reported failed by name (the open listener and why); a door
/// that does not answer at all is NOT taken for one waiting to be re-admitted.
#[test]
fn a_failed_readmission_and_a_silent_door_are_reported_failed() {
let f = Fake::default();
f.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), OLD, "a"));
f.open_door.borrow_mut().push("main".into());
*f.readmit_refused.borrow_mut() = Some("the box did not close".into());
let (o, w) = step(&f, &take("main", true)).unwrap();
assert_eq!(o, FAILED);
assert!(w.contains("[install-path]") && w.contains("the box did not close"), "{w}");
let silent = Fake::default();
*silent.down_polls.borrow_mut() = 1;
silent.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
let (o, w) = step(&silent, &take("twin", true)).unwrap();
assert_eq!(o, FAILED);
assert!(w.contains("its live door did not answer"), "{w}");
assert!(silent.readmitted.borrow().is_empty());
}
/// ★ **The twin's keep upgrade with a main left at its open install listener** (gunnar.rs
/// 2026-10-25, the twin's re-admission waited 3606 s on the main): the main is re-admitted
/// FIRST, then the twin goes through its keep medium.
#[test]
fn a_twin_whose_main_waits_for_its_readmission_readmits_the_main_first_then_keeps() {
let f = Fake::default();
f.boxes.borrow_mut().insert("twin".into(), running(&"c".repeat(64), OLD, "a"));
f.boxes.borrow_mut().insert("main".into(), running(&"c".repeat(64), OLD, "a"));
f.open_door.borrow_mut().push("main".into());
let (o, w) = step(&f, &take("twin", true)).expect("reported");
assert_eq!(o, DONE_KEEP, "{w}");
assert_eq!(*f.readmitted.borrow(), vec!["main".to_string()], "the main first, on its own");
assert_eq!(*f.kept.borrow(), vec!["twin".to_string()], "then the twin's keep upgrade");
assert!(w.contains("main re-admitted"), "the words name the main's re-admission: {w}");
}
/// ★ **Both boxes at their open install listener**: ONE re-admission of both, then the twin
/// is read again — it runs the release's app → done:keep, no second keep boot.
#[test]
fn both_boxes_left_waiting_are_readmitted_together_then_read_again() {
let f = Fake::default();
let app = app_of(&"b".repeat(64));
f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), &app.app, "a"));
f.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), &app.app, "a"));
f.open_door.borrow_mut().extend(["main".to_string(), "twin".to_string()]);
let (o, w) = step(&f, &take("twin", true)).expect("reported");
assert_eq!(o, DONE_KEEP, "{w}");
assert_eq!(*f.readmitted.borrow(), vec!["twin+main".to_string()], "one re-admission of both");
assert!(f.open_door.borrow().is_empty());
assert!(f.kept.borrow().is_empty() && f.staged.borrow().is_empty(), "{w}");
}
/// ★ **A front that keeps no bundle refuses the re-admission by name** (the default
/// [`Hands::readmit`]): the box at its open install listener is reported failed with the
/// listener's words and the refusal, never taken for a silent door.
#[test]
fn a_front_that_keeps_no_bundle_reports_the_open_listener_and_its_refusal() {
struct NoBundle(Fake);
impl Hands for NoBundle {
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)
}
}
let f = NoBundle(Fake::default());
f.0.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), OLD, "a"));
f.0.open_door.borrow_mut().push("main".into());
let (o, w) = step(&f, &take("main", true)).unwrap();
assert_eq!(o, FAILED);
assert!(w.contains("[install-path]") && w.contains("[readmit-unsupported]"), "{w}");
}
}