use serde::{Deserialize, Serialize};
pub const STEWARD_STATUS_SCHEMA_V1: &str = "ee.mesh.steward.status.v1";
pub mod audit_events {
pub const RECONCILIATION_SKIPPED: &str = "mesh.steward_reconciliation_skipped";
pub const RECONCILIATION_TRIGGERED: &str = "mesh.steward_reconciliation_triggered";
pub const RECONCILIATION_REFUSED: &str = "mesh.steward_reconciliation_refused";
pub const RECONCILIATION_DAILY_CAP_REACHED: &str =
"mesh.steward_reconciliation_daily_cap_reached";
}
pub const STEWARD_DEFAULT_INTERVAL_SECONDS: u64 = 900;
pub const STEWARD_DEFAULT_JITTER_SECONDS: u64 = 60;
pub const STEWARD_DEFAULT_MAX_DAILY: u64 = 100;
pub const STEWARD_AUTO_ENROLL_ON_DEMAND_ENV: &str = "EE_MESH_AUTO_ENROLL_ON_DEMAND";
pub const STEWARD_RECONCILIATION_INTERVAL_ENV: &str =
"EE_MESH_STEWARD_RECONCILIATION_INTERVAL_SECONDS";
pub const STEWARD_RECONCILIATION_JITTER_ENV: &str = "EE_MESH_STEWARD_RECONCILIATION_JITTER_SECONDS";
pub const STEWARD_RECONCILIATION_MAX_DAILY_ENV: &str = "EE_MESH_STEWARD_RECONCILIATION_MAX_DAILY";
pub const STEWARD_AUTO_ENROLL_DISABLED_CODE: &str = "steward_auto_enroll_disabled";
pub const STEWARD_AUTO_ENROLL_DAILY_CAP_REACHED_CODE: &str =
"steward_auto_enroll_daily_cap_reached";
pub const STEWARD_AUTO_ENROLL_CONSECUTIVE_FAILURES_CODE: &str =
"steward_auto_enroll_consecutive_failures";
const STEWARD_CONSECUTIVE_FAILURE_WARNING_THRESHOLD: u64 = 3;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StewardConfig {
pub enabled: bool,
pub interval_seconds: u64,
pub jitter_seconds: u64,
pub max_daily: u64,
}
impl Default for StewardConfig {
fn default() -> Self {
Self {
enabled: false,
interval_seconds: STEWARD_DEFAULT_INTERVAL_SECONDS,
jitter_seconds: STEWARD_DEFAULT_JITTER_SECONDS,
max_daily: STEWARD_DEFAULT_MAX_DAILY,
}
}
}
impl StewardConfig {
#[must_use]
pub const fn enabled(self) -> bool {
self.enabled
}
pub fn from_env_values(
enabled: Option<&str>,
interval_seconds: Option<&str>,
jitter_seconds: Option<&str>,
max_daily: Option<&str>,
) -> Result<Self, StewardConfigError> {
let defaults = Self::default();
Ok(Self {
enabled: parse_opt_in(STEWARD_AUTO_ENROLL_ON_DEMAND_ENV, enabled, defaults.enabled)?,
interval_seconds: parse_positive_u64(
STEWARD_RECONCILIATION_INTERVAL_ENV,
interval_seconds,
defaults.interval_seconds,
)?,
jitter_seconds: parse_u64(
STEWARD_RECONCILIATION_JITTER_ENV,
jitter_seconds,
defaults.jitter_seconds,
)?,
max_daily: parse_u64(
STEWARD_RECONCILIATION_MAX_DAILY_ENV,
max_daily,
defaults.max_daily,
)?,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StewardConfigError {
InvalidBool {
env_var: &'static str,
value: String,
},
InvalidNumber {
env_var: &'static str,
value: String,
},
ZeroNotAllowed {
env_var: &'static str,
},
}
impl StewardConfigError {
#[must_use]
pub const fn env_var(&self) -> &'static str {
match self {
Self::InvalidBool { env_var, .. }
| Self::InvalidNumber { env_var, .. }
| Self::ZeroNotAllowed { env_var } => env_var,
}
}
}
fn parse_opt_in(
env_var: &'static str,
value: Option<&str>,
default: bool,
) -> Result<bool, StewardConfigError> {
let Some(value) = value else {
return Ok(default);
};
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
_ => Err(StewardConfigError::InvalidBool {
env_var,
value: value.to_owned(),
}),
}
}
fn parse_positive_u64(
env_var: &'static str,
value: Option<&str>,
default: u64,
) -> Result<u64, StewardConfigError> {
let parsed = parse_u64(env_var, value, default)?;
if parsed == 0 {
return Err(StewardConfigError::ZeroNotAllowed { env_var });
}
Ok(parsed)
}
fn parse_u64(
env_var: &'static str,
value: Option<&str>,
default: u64,
) -> Result<u64, StewardConfigError> {
let Some(value) = value else {
return Ok(default);
};
value
.trim()
.parse::<u64>()
.map_err(|_| StewardConfigError::InvalidNumber {
env_var,
value: value.to_owned(),
})
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StewardStateSnapshot {
pub last_reconciliation_at: Option<String>,
pub last_reconciliation_outcome: StewardStatusOutcome,
pub reconciliations_today: u64,
pub consecutive_failures_24h: u64,
}
impl Default for StewardStateSnapshot {
fn default() -> Self {
Self {
last_reconciliation_at: None,
last_reconciliation_outcome: StewardStatusOutcome::NotYetRun,
reconciliations_today: 0,
consecutive_failures_24h: 0,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StewardStatus {
pub schema: &'static str,
pub enabled: bool,
pub interval_seconds: u64,
pub last_reconciliation_at: Option<String>,
pub last_reconciliation_outcome: StewardStatusOutcome,
pub reconciliations_today: u64,
pub next_reconciliation_approx_at: Option<String>,
pub degraded: Vec<StewardDegradation>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StewardDegradation {
pub code: &'static str,
pub severity: &'static str,
pub repair: &'static str,
}
#[must_use]
pub fn build_steward_status(
config: StewardConfig,
state: &StewardStateSnapshot,
next_reconciliation_approx_at: Option<String>,
) -> StewardStatus {
StewardStatus {
schema: STEWARD_STATUS_SCHEMA_V1,
enabled: config.enabled,
interval_seconds: config.interval_seconds,
last_reconciliation_at: state.last_reconciliation_at.clone(),
last_reconciliation_outcome: state.last_reconciliation_outcome,
reconciliations_today: state.reconciliations_today,
next_reconciliation_approx_at,
degraded: steward_status_degradations(config, state),
}
}
fn steward_status_degradations(
config: StewardConfig,
state: &StewardStateSnapshot,
) -> Vec<StewardDegradation> {
let mut degraded = Vec::new();
if !config.enabled {
degraded.push(StewardDegradation {
code: STEWARD_AUTO_ENROLL_DISABLED_CODE,
severity: "info",
repair: "Set EE_MESH_AUTO_ENROLL_ON_DEMAND=1 to enable opt-in steward reconciliation.",
});
}
if config.enabled && state.reconciliations_today >= config.max_daily {
degraded.push(StewardDegradation {
code: STEWARD_AUTO_ENROLL_DAILY_CAP_REACHED_CODE,
severity: "warning",
repair: "Raise EE_MESH_STEWARD_RECONCILIATION_MAX_DAILY or investigate flapping drift.",
});
}
if state.consecutive_failures_24h >= STEWARD_CONSECUTIVE_FAILURE_WARNING_THRESHOLD {
degraded.push(StewardDegradation {
code: STEWARD_AUTO_ENROLL_CONSECUTIVE_FAILURES_CODE,
severity: "medium",
repair: "Review `ee audit timeline --event-type mesh.steward_reconciliation_failed`.",
});
}
degraded
}
#[must_use]
pub fn apply_steward_decision_to_state(
mut state: StewardStateSnapshot,
decision: StewardDecision,
observed_at: impl Into<String>,
) -> StewardStateSnapshot {
if decision.outcome == StewardOutcome::NotEnabled {
return state;
}
state.last_reconciliation_at = Some(observed_at.into());
state.last_reconciliation_outcome = decision.status_outcome();
if decision.outcome.increments_reconciliation_counter() {
state.reconciliations_today = state.reconciliations_today.saturating_add(1);
}
state.consecutive_failures_24h = 0;
state
}
#[must_use]
pub fn record_steward_failure(
mut state: StewardStateSnapshot,
observed_at: impl Into<String>,
) -> StewardStateSnapshot {
state.last_reconciliation_at = Some(observed_at.into());
state.consecutive_failures_24h = state.consecutive_failures_24h.saturating_add(1);
state
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DriftSeverity {
None,
Info,
Warning,
Medium,
}
impl DriftSeverity {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Info => "info",
Self::Warning => "warning",
Self::Medium => "medium",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DriftKind {
NewPeersAvailable,
StalePeersInConfig,
TailnetChanged,
ManualConflictPresent,
None,
}
impl DriftKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::NewPeersAvailable => "new_peers_available",
Self::StalePeersInConfig => "stale_peers_in_config",
Self::TailnetChanged => "tailnet_changed",
Self::ManualConflictPresent => "manual_conflict_present",
Self::None => "none",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StewardDecisionInput {
pub enabled: bool,
pub drift_severity: DriftSeverity,
pub drift_kind: DriftKind,
pub reconciliations_today: u64,
pub max_daily: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StewardOutcome {
NotEnabled,
NoOp,
Triggered,
Refused,
DailyCapReached,
}
impl StewardOutcome {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::NotEnabled => "not_enabled",
Self::NoOp => "no_op",
Self::Triggered => "triggered",
Self::Refused => "refused",
Self::DailyCapReached => "daily_cap_reached",
}
}
#[must_use]
pub fn audit_event_type(self) -> Option<&'static str> {
match self {
Self::NotEnabled => None,
Self::NoOp => Some(audit_events::RECONCILIATION_SKIPPED),
Self::Triggered => Some(audit_events::RECONCILIATION_TRIGGERED),
Self::Refused => Some(audit_events::RECONCILIATION_REFUSED),
Self::DailyCapReached => Some(audit_events::RECONCILIATION_DAILY_CAP_REACHED),
}
}
#[must_use]
pub fn increments_reconciliation_counter(self) -> bool {
matches!(self, Self::Triggered)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StewardStatusOutcome {
NoActionableDrift,
Triggered,
Refused,
DailyCapReached,
NotYetRun,
}
impl StewardStatusOutcome {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::NoActionableDrift => "no_actionable_drift",
Self::Triggered => "triggered",
Self::Refused => "refused",
Self::DailyCapReached => "daily_cap_reached",
Self::NotYetRun => "not_yet_run",
}
}
}
pub mod reasons {
pub const NO_ACTIONABLE_DRIFT: &str = "no_actionable_drift";
pub const NEW_PEERS: &str = "new_peers";
pub const STALE_PEERS: &str = "stale_peers";
pub const REQUIRES_USER_ACTION: &str = "requires_user_action";
pub const DAILY_CAP_REACHED: &str = "daily_cap_reached";
pub const NOT_ENABLED: &str = "not_enabled";
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StewardDecision {
pub outcome: StewardOutcome,
pub reason: &'static str,
}
impl StewardDecision {
#[must_use]
pub fn status_outcome(self) -> StewardStatusOutcome {
match self.outcome {
StewardOutcome::NotEnabled => StewardStatusOutcome::NotYetRun,
StewardOutcome::NoOp => StewardStatusOutcome::NoActionableDrift,
StewardOutcome::Triggered => StewardStatusOutcome::Triggered,
StewardOutcome::Refused => StewardStatusOutcome::Refused,
StewardOutcome::DailyCapReached => StewardStatusOutcome::DailyCapReached,
}
}
}
#[must_use]
pub fn decide_steward_outcome(input: &StewardDecisionInput) -> StewardDecision {
if !input.enabled {
return StewardDecision {
outcome: StewardOutcome::NotEnabled,
reason: reasons::NOT_ENABLED,
};
}
if input.reconciliations_today >= input.max_daily {
return StewardDecision {
outcome: StewardOutcome::DailyCapReached,
reason: reasons::DAILY_CAP_REACHED,
};
}
if input.drift_severity == DriftSeverity::Medium {
return StewardDecision {
outcome: StewardOutcome::Refused,
reason: reasons::REQUIRES_USER_ACTION,
};
}
match input.drift_severity {
DriftSeverity::None | DriftSeverity::Info => StewardDecision {
outcome: StewardOutcome::NoOp,
reason: reasons::NO_ACTIONABLE_DRIFT,
},
DriftSeverity::Warning => match input.drift_kind {
DriftKind::NewPeersAvailable => StewardDecision {
outcome: StewardOutcome::Triggered,
reason: reasons::NEW_PEERS,
},
DriftKind::StalePeersInConfig => StewardDecision {
outcome: StewardOutcome::Triggered,
reason: reasons::STALE_PEERS,
},
_ => StewardDecision {
outcome: StewardOutcome::NoOp,
reason: reasons::NO_ACTIONABLE_DRIFT,
},
},
DriftSeverity::Medium => unreachable!("medium handled above"),
}
}
#[must_use]
pub fn apply_interval_jitter(
base_interval_seconds: u64,
jitter_window_seconds: u64,
raw_jitter_signed: i64,
) -> u64 {
let clamped_jitter = clamp_jitter_to_window(raw_jitter_signed, jitter_window_seconds);
let signed_base = i128::from(base_interval_seconds);
let adjusted = signed_base.saturating_add(i128::from(clamped_jitter));
let bounded = adjusted.max(1);
u64::try_from(bounded).unwrap_or(u64::MAX)
}
fn clamp_jitter_to_window(raw: i64, window: u64) -> i64 {
let window_signed = i64::try_from(window).unwrap_or(i64::MAX);
raw.clamp(-window_signed, window_signed)
}
#[cfg(test)]
mod tests {
use super::*;
fn input(
enabled: bool,
severity: DriftSeverity,
kind: DriftKind,
reconciliations_today: u64,
max_daily: u64,
) -> StewardDecisionInput {
StewardDecisionInput {
enabled,
drift_severity: severity,
drift_kind: kind,
reconciliations_today,
max_daily,
}
}
#[test]
fn steward_config_defaults_to_disabled_with_documented_limits() {
let config = StewardConfig::default();
assert!(!config.enabled());
assert_eq!(config.interval_seconds, STEWARD_DEFAULT_INTERVAL_SECONDS);
assert_eq!(config.jitter_seconds, STEWARD_DEFAULT_JITTER_SECONDS);
assert_eq!(config.max_daily, STEWARD_DEFAULT_MAX_DAILY);
}
#[test]
fn steward_config_resolves_env_values_without_reading_process_env() {
let config = StewardConfig::from_env_values(Some("1"), Some("1200"), Some("45"), Some("7"))
.expect("valid config");
assert!(config.enabled);
assert_eq!(config.interval_seconds, 1200);
assert_eq!(config.jitter_seconds, 45);
assert_eq!(config.max_daily, 7);
}
#[test]
fn steward_config_rejects_invalid_opt_in_value() {
let error =
StewardConfig::from_env_values(Some("maybe"), None, None, None).expect_err("invalid");
assert_eq!(error.env_var(), STEWARD_AUTO_ENROLL_ON_DEMAND_ENV);
assert!(matches!(error, StewardConfigError::InvalidBool { .. }));
}
#[test]
fn steward_config_rejects_zero_interval_but_allows_zero_daily_cap() {
let interval_error =
StewardConfig::from_env_values(None, Some("0"), None, None).expect_err("zero");
assert_eq!(
interval_error.env_var(),
STEWARD_RECONCILIATION_INTERVAL_ENV
);
assert!(matches!(
interval_error,
StewardConfigError::ZeroNotAllowed { .. }
));
let config = StewardConfig::from_env_values(Some("true"), Some("1"), Some("0"), Some("0"))
.expect("zero cap is a valid never-reconcile cap");
assert_eq!(config.jitter_seconds, 0);
assert_eq!(config.max_daily, 0);
}
#[test]
fn steward_status_disabled_surfaces_info_degradation_only_when_requested() {
let status = build_steward_status(
StewardConfig::default(),
&StewardStateSnapshot::default(),
None,
);
assert_eq!(status.schema, STEWARD_STATUS_SCHEMA_V1);
assert!(!status.enabled);
assert_eq!(
status.last_reconciliation_outcome,
StewardStatusOutcome::NotYetRun
);
assert_eq!(status.degraded.len(), 1);
assert_eq!(status.degraded[0].code, STEWARD_AUTO_ENROLL_DISABLED_CODE);
assert_eq!(status.degraded[0].severity, "info");
}
#[test]
fn steward_status_daily_cap_and_consecutive_failures_are_degraded() {
let config = StewardConfig::from_env_values(Some("1"), Some("900"), Some("60"), Some("2"))
.expect("valid");
let state = StewardStateSnapshot {
reconciliations_today: 2,
consecutive_failures_24h: 3,
..StewardStateSnapshot::default()
};
let status = build_steward_status(config, &state, Some("2026-05-20T12:15:00Z".to_owned()));
let codes: Vec<&str> = status
.degraded
.iter()
.map(|degradation| degradation.code)
.collect();
assert!(codes.contains(&STEWARD_AUTO_ENROLL_DAILY_CAP_REACHED_CODE));
assert!(codes.contains(&STEWARD_AUTO_ENROLL_CONSECUTIVE_FAILURES_CODE));
assert_eq!(
status.next_reconciliation_approx_at.as_deref(),
Some("2026-05-20T12:15:00Z")
);
}
#[test]
fn applying_triggered_decision_updates_status_state_and_daily_counter() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
1,
STEWARD_DEFAULT_MAX_DAILY,
));
let state = apply_steward_decision_to_state(
StewardStateSnapshot {
reconciliations_today: 1,
consecutive_failures_24h: 2,
..StewardStateSnapshot::default()
},
decision,
"2026-05-20T12:30:00Z",
);
assert_eq!(
state.last_reconciliation_at.as_deref(),
Some("2026-05-20T12:30:00Z")
);
assert_eq!(
state.last_reconciliation_outcome,
StewardStatusOutcome::Triggered
);
assert_eq!(state.reconciliations_today, 2);
assert_eq!(state.consecutive_failures_24h, 0);
}
#[test]
fn disabled_decision_does_not_touch_state_file_projection() {
let original = StewardStateSnapshot {
last_reconciliation_at: Some("2026-05-20T11:00:00Z".to_owned()),
last_reconciliation_outcome: StewardStatusOutcome::Refused,
reconciliations_today: 4,
consecutive_failures_24h: 1,
};
let decision = decide_steward_outcome(&input(
false,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
4,
STEWARD_DEFAULT_MAX_DAILY,
));
assert_eq!(
apply_steward_decision_to_state(original.clone(), decision, "2026-05-20T12:30:00Z"),
original
);
}
#[test]
fn recording_steward_failure_increments_consecutive_failure_counter() {
let state = record_steward_failure(
StewardStateSnapshot {
consecutive_failures_24h: 2,
..StewardStateSnapshot::default()
},
"2026-05-20T12:35:00Z",
);
assert_eq!(state.consecutive_failures_24h, 3);
assert_eq!(
state.last_reconciliation_at.as_deref(),
Some("2026-05-20T12:35:00Z")
);
}
#[test]
fn disabled_steward_short_circuits_and_emits_no_audit() {
let decision = decide_steward_outcome(&input(
false,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
0,
STEWARD_DEFAULT_MAX_DAILY,
));
assert_eq!(decision.outcome, StewardOutcome::NotEnabled);
assert_eq!(decision.reason, reasons::NOT_ENABLED);
assert!(decision.outcome.audit_event_type().is_none());
}
#[test]
fn daily_cap_reached_short_circuits_even_when_drift_actionable() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
100,
100,
));
assert_eq!(decision.outcome, StewardOutcome::DailyCapReached);
assert_eq!(decision.reason, reasons::DAILY_CAP_REACHED);
assert_eq!(
decision.outcome.audit_event_type(),
Some(audit_events::RECONCILIATION_DAILY_CAP_REACHED)
);
}
#[test]
fn daily_cap_zero_means_never_reconcile() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
0,
0,
));
assert_eq!(decision.outcome, StewardOutcome::DailyCapReached);
}
#[test]
fn medium_severity_with_tailnet_change_refuses() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Medium,
DriftKind::TailnetChanged,
0,
100,
));
assert_eq!(decision.outcome, StewardOutcome::Refused);
assert_eq!(decision.reason, reasons::REQUIRES_USER_ACTION);
assert_eq!(
decision.outcome.audit_event_type(),
Some(audit_events::RECONCILIATION_REFUSED)
);
}
#[test]
fn medium_severity_with_manual_conflict_refuses() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Medium,
DriftKind::ManualConflictPresent,
0,
100,
));
assert_eq!(decision.outcome, StewardOutcome::Refused);
}
#[test]
fn medium_severity_does_not_trip_triggered_even_with_actionable_kind() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Medium,
DriftKind::NewPeersAvailable,
0,
100,
));
assert_eq!(decision.outcome, StewardOutcome::Refused);
}
#[test]
fn severity_none_yields_noop() {
let decision =
decide_steward_outcome(&input(true, DriftSeverity::None, DriftKind::None, 5, 100));
assert_eq!(decision.outcome, StewardOutcome::NoOp);
assert_eq!(decision.reason, reasons::NO_ACTIONABLE_DRIFT);
assert_eq!(
decision.outcome.audit_event_type(),
Some(audit_events::RECONCILIATION_SKIPPED)
);
}
#[test]
fn severity_info_yields_noop_even_with_new_peers_kind() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Info,
DriftKind::NewPeersAvailable,
5,
100,
));
assert_eq!(decision.outcome, StewardOutcome::NoOp);
}
#[test]
fn warning_new_peers_triggers_with_new_peers_reason() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
5,
100,
));
assert_eq!(decision.outcome, StewardOutcome::Triggered);
assert_eq!(decision.reason, reasons::NEW_PEERS);
assert_eq!(
decision.outcome.audit_event_type(),
Some(audit_events::RECONCILIATION_TRIGGERED)
);
}
#[test]
fn warning_stale_peers_triggers_with_stale_peers_reason() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Warning,
DriftKind::StalePeersInConfig,
5,
100,
));
assert_eq!(decision.outcome, StewardOutcome::Triggered);
assert_eq!(decision.reason, reasons::STALE_PEERS);
}
#[test]
fn warning_with_none_kind_declines_rather_than_guess() {
let decision = decide_steward_outcome(&input(
true,
DriftSeverity::Warning,
DriftKind::None,
5,
100,
));
assert_eq!(decision.outcome, StewardOutcome::NoOp);
}
#[test]
fn schema_constant_matches_documented_version() {
assert_eq!(STEWARD_STATUS_SCHEMA_V1, "ee.mesh.steward.status.v1");
}
#[test]
fn enum_strings_match_snake_case_serde() {
for variant in [
DriftSeverity::None,
DriftSeverity::Info,
DriftSeverity::Warning,
DriftSeverity::Medium,
] {
let serialized = serde_json::to_string(&variant).expect("serialize");
assert!(serialized.contains(variant.as_str()), "{serialized}");
}
for variant in [
DriftKind::NewPeersAvailable,
DriftKind::StalePeersInConfig,
DriftKind::TailnetChanged,
DriftKind::ManualConflictPresent,
DriftKind::None,
] {
let serialized = serde_json::to_string(&variant).expect("serialize");
assert!(serialized.contains(variant.as_str()), "{serialized}");
}
for variant in [
StewardOutcome::NotEnabled,
StewardOutcome::NoOp,
StewardOutcome::Triggered,
StewardOutcome::Refused,
StewardOutcome::DailyCapReached,
] {
let serialized = serde_json::to_string(&variant).expect("serialize");
assert!(serialized.contains(variant.as_str()), "{serialized}");
}
for variant in [
StewardStatusOutcome::NoActionableDrift,
StewardStatusOutcome::Triggered,
StewardStatusOutcome::Refused,
StewardStatusOutcome::DailyCapReached,
StewardStatusOutcome::NotYetRun,
] {
let serialized = serde_json::to_string(&variant).expect("serialize");
assert!(serialized.contains(variant.as_str()), "{serialized}");
}
}
#[test]
fn status_outcome_uses_schema_vocabulary_not_execution_vocabulary() {
let noop = decide_steward_outcome(&input(
true,
DriftSeverity::None,
DriftKind::None,
0,
STEWARD_DEFAULT_MAX_DAILY,
));
assert_eq!(noop.outcome.as_str(), "no_op");
assert_eq!(noop.status_outcome().as_str(), reasons::NO_ACTIONABLE_DRIFT);
let disabled = decide_steward_outcome(&input(
false,
DriftSeverity::Warning,
DriftKind::NewPeersAvailable,
0,
STEWARD_DEFAULT_MAX_DAILY,
));
assert_eq!(disabled.status_outcome(), StewardStatusOutcome::NotYetRun);
}
#[test]
fn daily_counter_only_increments_after_triggered_reconciliation() {
for outcome in [
StewardOutcome::NotEnabled,
StewardOutcome::NoOp,
StewardOutcome::Refused,
StewardOutcome::DailyCapReached,
] {
assert!(!outcome.increments_reconciliation_counter());
}
assert!(StewardOutcome::Triggered.increments_reconciliation_counter());
}
#[test]
fn jitter_zero_returns_base_interval_unchanged() {
assert_eq!(apply_interval_jitter(900, 60, 0), 900);
}
#[test]
fn jitter_positive_within_window_adds_to_base() {
assert_eq!(apply_interval_jitter(900, 60, 30), 930);
}
#[test]
fn jitter_negative_within_window_subtracts_from_base() {
assert_eq!(apply_interval_jitter(900, 60, -30), 870);
}
#[test]
fn jitter_at_window_edge_is_honored() {
assert_eq!(apply_interval_jitter(900, 60, 60), 960);
assert_eq!(apply_interval_jitter(900, 60, -60), 840);
}
#[test]
fn jitter_beyond_window_is_clamped() {
assert_eq!(apply_interval_jitter(900, 60, 1000), 960);
assert_eq!(apply_interval_jitter(900, 60, -1000), 840);
}
#[test]
fn jitter_clamps_to_minimum_one_second_even_at_negative_extreme() {
assert_eq!(apply_interval_jitter(5, 100, -100), 1);
}
#[test]
fn jitter_does_not_overflow_with_huge_inputs() {
let result = apply_interval_jitter(u64::MAX, 1000, 999);
assert_eq!(result, u64::MAX);
}
}