B2 rework: pin the resume wait's clock-step guard; correct the actuator doc

cargo-mutants-style probe C26 (the resume backoff read without its longest
wait) survived: the clock-step test called is_waiting directly, not the resume
site. An Engine-level test now sets a real half-minute backoff, which holds,
and a day-ahead one, which is attempted.

docs/host-actuator.md said continuity gives up after three attempts; it is six
restarts inside an hour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 01:53:38 +02:00
commit ef6cfb177f
2 changed files with 40 additions and 2 deletions

View file

@ -82,8 +82,8 @@ running a script, and a dead `muninn serve` is seen the tick it dies.
2. No path on yggdrasil can start, stop or observe a host workload except
through the actuator session bound to that host's anchor.
3. A host with no attached actuator is a failed observation, which
continuity counts and gives up on after three attempts, exactly like a
unit that will not start.
continuity counts and gives up on after six restarts inside one hour,
exactly like a unit that will not start.
4. The actuator cannot be told to run a program the recipe did not declare;
`allowed_programs` on the host-native runner is enforced on the host.

View file

@ -13689,6 +13689,44 @@ mod tests {
assert!(!is_waiting(4, 10, 5));
}
#[test]
fn a_resume_backoff_set_by_a_stepped_back_clock_is_due_and_a_real_one_waits() -> Result<()> {
let world = EngineFixture::with_workload(Arc::new(StillWorkload))?;
let id = transaction_at(&world, DeploymentPhase::Warming)?.transaction_id;
let not_before = |world: &EngineFixture| {
world
.engine
.resume_backoff
.lock()
.unwrap()
.get(&id)
.map(|backoff| backoff.not_before_unix_millis)
};
let set = |world: &EngineFixture, not_before_unix_millis: u64| {
world.engine.resume_backoff.lock().unwrap().insert(
id.clone(),
ResumeBackoff {
failures: 3,
not_before_unix_millis,
},
);
};
// Half a minute ahead is a wait the backoff can set: the step is left alone.
let waiting = now_millis()? + 30_000;
set(&world, waiting);
let _ = world.engine.resume_candidate(&resident(&world)?);
assert_eq!(not_before(&world), Some(waiting));
// A day ahead is a clock that stepped back: the step is attempted, and
// whatever it does, it replaces the backoff.
let ahead = now_millis()? + 86_400_000;
set(&world, ahead);
let _ = world.engine.resume_candidate(&resident(&world)?);
assert!(not_before(&world).is_none_or(|due| due < ahead));
Ok(())
}
#[test]
fn a_persistent_fault_is_reported_once_and_again_only_after_recovery() -> Result<()> {
let workload = Arc::new(ChangingErrorWorkload(