use crate::contract::recovery::{QuarantineRecord, RecoveryProbe};
use crate::contract::registry::{BoundaryRun, RunStep};
use crate::contract::report::{BoundaryReport, ObservedFact};
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct SimRun {
pub observed: Vec<ObservedFact>,
pub sealed: Option<BoundaryReport>,
pub faulted: Option<String>,
pub crashed_after: Option<usize>,
}
impl SimRun {
#[must_use]
pub fn did_seal(&self) -> bool {
self.sealed.is_some()
}
}
pub struct SimSupervisor;
impl SimSupervisor {
#[must_use]
pub fn drive(mut run: BoundaryRun, crash_after: Option<usize>) -> SimRun {
let mut observed = Vec::new();
let mut sealed = None;
let mut faulted = None;
let mut steps = 0usize;
while let Some(step) = run.drive_step() {
match step {
RunStep::Observed(fact) => observed.push(fact),
RunStep::Sealed(report) => {
sealed = Some(*report);
return SimRun {
observed,
sealed,
faulted,
crashed_after: None,
};
}
RunStep::Faulted(detail) => {
faulted = Some(detail);
return SimRun {
observed,
sealed,
faulted,
crashed_after: None,
};
}
}
steps += 1;
if crash_after == Some(steps - 1) {
return SimRun {
observed,
sealed,
faulted,
crashed_after: Some(steps - 1),
};
}
}
SimRun {
observed,
sealed,
faulted,
crashed_after: None,
}
}
}
pub struct SimProbe;
impl SimProbe {
#[must_use]
pub fn probe(sim: &SimRun, orphans: Vec<QuarantineRecord>) -> RecoveryProbe {
if sim.did_seal() {
return RecoveryProbe::default();
}
RecoveryProbe {
orphans,
torn_report: false,
artifacts: BTreeMap::new(),
}
}
}
#[cfg(test)]
mod supervisor_tests {
use super::{SimProbe, SimSupervisor};
use crate::contract::budget::{BudgetRequirements, MinGuarantee};
use crate::contract::host_control::HostControl;
use crate::contract::ids::{AttemptId, BackendId};
use crate::contract::plan::{BoundaryPlan, BoundarySpec, EvidenceRequirements, Workload};
use crate::contract::recovery::{reconcile, QuarantineRecord, RecoveryAction, RunView};
use crate::contract::registry::{BackendRegistry, BoundaryPlanner, BoundaryRunner};
use crate::sim::backend::{LieMode, OneShotLiar, SimBackend};
use std::sync::Arc;
fn sim_backend() -> SimBackend {
SimBackend::new(Box::new(OneShotLiar::new(LieMode::Honest)))
}
fn plan() -> BoundaryPlan {
let mut registry = BackendRegistry::new();
registry.register(Arc::new(sim_backend()));
let spec = BoundarySpec {
workload: Workload::Process {
exe: "true".to_string(),
args: Vec::new(),
},
capabilities: Vec::new(),
controls: vec![HostControl::LaunchWorkload],
budgets: BudgetRequirements::uniform(64, MinGuarantee::Mediated),
evidence: EvidenceRequirements::default(),
};
BoundaryPlanner::new(®istry)
.plan(&spec, &BackendId::new(SimBackend::ID))
.expect("honest sim plans a runnable budgeted launch spec")
}
fn run_for(plan: &BoundaryPlan) -> crate::contract::registry::BoundaryRun {
let mut registry = BackendRegistry::new();
registry.register(Arc::new(sim_backend()));
BoundaryRunner::new(®istry)
.begin(plan)
.expect("begin a run")
}
#[test]
fn drive_to_completion_seals() {
let plan = plan();
let sim = SimSupervisor::drive(run_for(&plan), None);
assert!(sim.did_seal(), "an uninjured run seals");
assert!(sim.faulted.is_none());
assert!(sim.crashed_after.is_none());
let mut registry = BackendRegistry::new();
registry.register(Arc::new(sim_backend()));
let direct = BoundaryRunner::new(®istry)
.run(&plan)
.expect("run seals");
assert_eq!(
sim.sealed.expect("sealed").body_hash,
direct.body_hash,
"the stepped seal equals the one-shot run() seal — shared core",
);
}
#[test]
fn crash_before_seal_reconciles_to_rollback() {
let plan = plan();
let sim = SimSupervisor::drive(run_for(&plan), Some(0));
assert!(!sim.did_seal(), "a crash before seal leaves no report");
assert_eq!(sim.crashed_after, Some(0));
let orphan = QuarantineRecord {
kind: "process".to_string(),
reference: "pid:99".to_string(),
};
let probe = SimProbe::probe(&sim, vec![orphan.clone()]);
let mut view = RunView::new(AttemptId([1u8; 32]), plan.plan_id);
view.started = true;
assert_eq!(
reconcile(&view, &probe),
RecoveryAction::RollBack {
orphans: vec![orphan]
},
"an in-flight run with live orphans rolls back",
);
}
}