estate/holger-live/tests/the_live_slot.rs
//! ★ **A Holger box's live slot** (L20): korp's own legs on a bench ESP — the running report,
//! a signed stage for this BASE into the other slot, korp-start's boot of it on trial, the
//! confirm after serving, the rollback of a slot that never confirmed, and every refusal by name.
use std::time::Duration;
use holger_live::bench::BenchBox;
use holger_live::{
after_stop, confirm_plan, AfterStop, ConfirmStep, CONFIRM_AFTER, CONFIRM_DEADLINE,
};
fn dir(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("holger-live-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
fn app(n: u8) -> Vec<u8> {
(0..64 * 1024u32).map(|i| (i % 251) as u8 ^ n).collect()
}
/// ★ Staged, booted on trial, confirmed after serving: the box runs the new app in slot B.
#[test]
fn a_signed_app_for_this_base_is_staged_booted_on_trial_and_confirmed() {
let d = dir("ok");
let b = BenchBox::lay(&d, "kernel-1", 42, &app(1)).unwrap();
let r = b.slot.running().unwrap();
assert_eq!(
(r.slot.as_str(), r.live_update, r.app.as_str()),
("a", true, blake3::hash(&app(1)).to_hex().as_str())
);
let st = b.stage_of(&app(2), 42);
let said = b.slot.stage(&st).unwrap();
assert!(said.contains("slot B"), "{said}");
assert!(holger_live::staged(), "the stop ends in a re-exec");
assert_eq!(
b.written
.lock()
.unwrap()
.iter()
.map(|(s, a)| (*s, a.len()))
.collect::<Vec<_>>(),
[(holger_live::Slot::B, app(2).len())]
);
assert_eq!(b.slot.running().unwrap().slot, "b trial");
// The re-exec: korp-start boots the staged slot and marks it booted.
b.slot.boot_as_korp_start().unwrap();
let r = b.slot.running().unwrap();
assert_eq!(
(r.slot.as_str(), r.app.as_str()),
("b booted", st.pin_hex.as_str())
);
assert!(b.slot.on_trial());
assert_eq!(
b.slot.confirm_step(Duration::from_secs(10), None).unwrap(),
ConfirmStep::Wait
);
assert_eq!(
b.slot
.confirm_step(Duration::from_secs(90), Some(Duration::from_secs(20)))
.unwrap(),
ConfirmStep::Confirm
);
assert_eq!(b.slot.running().unwrap().slot, "b", "confirmed");
let _ = std::fs::remove_dir_all(&d);
}
/// ★ A slot that never served is rolled back at the next start: the old app in slot A again.
#[test]
fn a_new_app_that_never_confirms_is_rolled_back() {
let d = dir("back");
let b = BenchBox::lay(&d, "kernel-1", 42, &app(1)).unwrap();
b.slot.stage(&b.stage_of(&app(3), 42)).unwrap();
b.slot.boot_as_korp_start().unwrap();
assert_eq!(
b.slot
.confirm_step(CONFIRM_DEADLINE + Duration::from_secs(1), None)
.unwrap(),
ConfirmStep::RollBack
);
let said = b.slot.boot_as_korp_start().unwrap();
assert!(said.contains("rolled back"), "{said}");
let r = b.slot.running().unwrap();
assert_eq!(
(r.slot.as_str(), r.app.as_str()),
("a", blake3::hash(&app(1)).to_hex().as_str())
);
let _ = std::fs::remove_dir_all(&d);
}
/// ★ Refused by name, nothing written: another root's signature, another BASE, a trial still
/// open, a box whose live switch is off.
#[test]
fn a_stage_that_fails_its_check_is_refused_and_nothing_moves() {
let d = dir("refuse");
let b = BenchBox::lay(&d, "kernel-1", 42, &app(1)).unwrap();
let e = b.slot.stage(&b.stage_of(&app(2), 7)).unwrap_err();
assert!(
e.contains("[live-verify]") && e.contains("signature"),
"{e}"
);
let mut other = b.stage_of(&app(2), 42);
other.base = "f".repeat(64);
assert!(b
.slot
.stage(&other)
.unwrap_err()
.contains("[live-base-mismatch]"));
assert!(b.written.lock().unwrap().is_empty(), "nothing written");
assert_eq!(b.slot.running().unwrap().slot, "a");
b.slot.stage(&b.stage_of(&app(2), 42)).unwrap();
assert!(b
.slot
.stage(&b.stage_of(&app(3), 42))
.unwrap_err()
.contains("[live-trial-open]"));
let d2 = dir("off");
let mut off = BenchBox::lay(&d2, "kernel-1", 42, &app(1)).unwrap();
off.slot.policy.live_update = false;
assert!(!off.slot.running().unwrap().live_update);
assert!(off
.slot
.stage(&off.stage_of(&app(2), 42))
.unwrap_err()
.contains("[live-update-off]"));
let _ = std::fs::remove_dir_all(&d);
let _ = std::fs::remove_dir_all(&d2);
}
/// ★ The pure rules: confirm only after serving, roll back at the deadline; a staged update
/// ends the stop in a re-exec.
#[test]
fn the_trial_rules_and_the_end_of_the_stop() {
let s = Duration::from_secs;
assert_eq!(confirm_plan(false, s(9999), None), ConfirmStep::Nothing);
assert_eq!(confirm_plan(true, s(30), Some(s(20))), ConfirmStep::Wait);
assert_eq!(
confirm_plan(true, s(20) + CONFIRM_AFTER, Some(s(20))),
ConfirmStep::Confirm
);
assert_eq!(
confirm_plan(true, CONFIRM_DEADLINE + s(1), None),
ConfirmStep::RollBack
);
assert_eq!(
(after_stop(true), after_stop(false)),
(AfterStop::ReExec, AfterStop::PowerOff)
);
}
/// ★ A different kernel is a different BASE: what the front reads to choose the keep medium.
#[test]
fn a_different_kernel_is_a_different_base() {
let (d1, d2) = (dir("k1"), dir("k2"));
let a = BenchBox::lay(&d1, "kernel-1", 42, &app(1)).unwrap();
let b = BenchBox::lay(&d2, "kernel-2", 42, &app(1)).unwrap();
assert_ne!(a.base(), b.base());
let _ = std::fs::remove_dir_all(&d1);
let _ = std::fs::remove_dir_all(&d2);
}
/// ★ **A live update with a changed declaration** (lane B5): the box runs `holger.ron` "(one)";
/// the stage carries the new app with "(two)", signed for it. The box stages slot B, korp-start
/// boots it, and the box's `/run/korp/release` holds "(two)". A tampered declaration is refused
/// by name and nothing moves; a stage with none is refused while the box carries one
/// (`[live-release-absent]`: it keeps its app and its release).
#[test]
fn a_live_update_changes_the_release_declaration_and_a_tampered_one_is_refused() {
use nordisk_pairing::live::Declared;
let d = dir("declared");
let one = vec![("holger.ron".to_string(), b"(one)".to_vec())];
let b = BenchBox::lay_declared(&d, "kernel-1", 42, &app(1), &one).unwrap();
b.slot.boot_as_korp_start().unwrap();
assert_eq!(
std::fs::read(b.release_dir().join("holger.ron")).unwrap(),
b"(one)",
"the box's declaration at its boot"
);
let st = b.stage_of(&app(2), 42);
let two = vec![("holger.ron".to_string(), b"(two)".to_vec())];
let (bundle, sig) = holger_live::bench::sign_release(&two, &app(2), 42);
// Tampered under the right signature: refused, nothing written.
let evil = Declared {
release: Some((
korp_installer::release::encode(&[("holger.ron".to_string(), b"(evil)".to_vec())])
.unwrap(),
sig.clone(),
)),
env: None,
};
let e = b.slot.stage_declared(&st, &evil).unwrap_err();
assert!(e.contains("[release-sig]"), "{e}");
// None at all while the box carries one: kept.
let e = b.slot.stage(&st).unwrap_err();
assert!(
e.contains("[live-release-absent]") && nordisk_pairing::live::falls_to_keep(&e),
"{e}"
);
assert!(b.written.lock().unwrap().is_empty(), "nothing written");
assert_eq!(b.slot.running().unwrap().slot, "a");
// The new declaration: staged, booted, exposed.
let said = b
.slot
.stage_declared(
&st,
&Declared {
release: Some((bundle, sig)),
env: Some(vec![]),
},
)
.unwrap();
assert!(
said.contains("slot B") && said.contains("holger.ron"),
"{said}"
);
b.slot.boot_as_korp_start().unwrap();
assert_eq!(b.slot.running().unwrap().app, st.pin_hex);
assert_eq!(
std::fs::read(b.release_dir().join("holger.ron")).unwrap(),
b"(two)",
"korp-start lays the new slot's declaration"
);
let _ = std::fs::remove_dir_all(&d);
}