use std::time::Duration;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
gen_platform::TypedDispatcher,
gen_platform::Discriminant,
gen_platform::IsVariant,
gen_platform::FromStrKind,
)]
pub enum RestartStrategy {
OneForOne,
OneForAll,
RestForOne,
SimpleOneForOne,
}
impl Default for RestartStrategy {
fn default() -> Self {
Self::OneForOne
}
}
impl RestartStrategy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::OneForOne => crate::render::SUPERVISOR_ESTRATEGIA_ONE_FOR_ONE,
Self::OneForAll => crate::render::SUPERVISOR_ESTRATEGIA_ONE_FOR_ALL,
Self::RestForOne => crate::render::SUPERVISOR_ESTRATEGIA_REST_FOR_ONE,
Self::SimpleOneForOne => crate::render::SUPERVISOR_ESTRATEGIA_SIMPLE_ONE_FOR_ONE,
}
}
}
impl std::fmt::Display for RestartStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
gen_platform::TypedDispatcher,
gen_platform::Discriminant,
gen_platform::IsVariant,
gen_platform::FromStrKind,
)]
pub enum RestartPolicy {
Permanent,
Temporary,
Transient,
}
impl Default for RestartPolicy {
fn default() -> Self {
Self::Permanent
}
}
impl RestartPolicy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Permanent => crate::render::SUPERVISOR_CHILD_RESTART_PERMANENT,
Self::Temporary => crate::render::SUPERVISOR_CHILD_RESTART_TEMPORARY,
Self::Transient => crate::render::SUPERVISOR_CHILD_RESTART_TRANSIENT,
}
}
}
impl std::fmt::Display for RestartPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
gen_platform::register_dispatcher!("caixa.restart-strategy", RestartStrategy);
gen_platform::register_dispatcher!("caixa.restart-policy", RestartPolicy);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ChildSpec {
pub caixa: String,
pub versao: String,
#[serde(default)]
pub restart: RestartPolicy,
}
impl ChildSpec {
#[must_use]
pub fn nome(&self) -> &str {
self.caixa.as_str()
}
#[must_use]
pub fn versao_requirement(&self) -> &str {
self.versao.as_str()
}
#[must_use]
pub fn restart(&self) -> RestartPolicy {
self.restart
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SupervisorSpec {
#[serde(default)]
pub estrategia: RestartStrategy,
#[serde(default = "default_max_restarts")]
pub max_restarts: u32,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "duration_codec"
)]
pub restart_window: Option<Duration>,
#[serde(default)]
pub children: Vec<ChildSpec>,
}
const fn default_max_restarts() -> u32 {
5
}
pub const SUPERVISOR_MAX_RESTARTS_MAX: u32 = 1000;
pub const SUPERVISOR_RESTART_WINDOW_MAX: Duration = Duration::from_secs(3600);
impl Default for SupervisorSpec {
fn default() -> Self {
Self {
estrategia: RestartStrategy::default(),
max_restarts: default_max_restarts(),
restart_window: Some(Duration::from_secs(60)),
children: Vec::new(),
}
}
}
impl SupervisorSpec {
#[must_use]
pub fn estrategia(&self) -> RestartStrategy {
self.estrategia
}
#[must_use]
pub const fn max_restarts(&self) -> u32 {
self.max_restarts
}
#[must_use]
pub const fn restart_window(&self) -> Option<Duration> {
self.restart_window
}
#[must_use]
pub fn children(&self) -> &[ChildSpec] {
self.children.as_slice()
}
pub fn validate(&self) -> Result<(), SupervisorError> {
match self.estrategia() {
RestartStrategy::SimpleOneForOne => {
if !self.children().is_empty() {
return Err(SupervisorError::SimpleOneForOneWithStaticChildren);
}
}
_ => {
if self.children().is_empty() {
return Err(SupervisorError::NoChildren {
estrategia: self.estrategia(),
});
}
}
}
crate::render::require_positive_bounded_u32(
self.max_restarts(),
SUPERVISOR_MAX_RESTARTS_MAX,
|| SupervisorError::ZeroMaxRestarts,
|max_restarts| SupervisorError::MaxRestartsExceedsCap { max_restarts },
)?;
if let Some(w) = self.restart_window() {
crate::render::require_positive_canonical_bounded_duration(
w,
SUPERVISOR_RESTART_WINDOW_MAX,
|| SupervisorError::RestartWindowZero,
|window| SupervisorError::RestartWindowNotCanonical { window },
|window| SupervisorError::RestartWindowExceedsCap { window },
)?;
}
let mut seen = std::collections::HashSet::new();
for child in self.children() {
crate::render::require_valid_dns_1123_label(
child.nome(),
|| SupervisorError::EmptyChildName,
|reason| SupervisorError::ChildCaixaInvalid {
caixa: child.nome().to_string(),
reason,
},
)?;
crate::render::require_valid_versao_requirement(
child.versao_requirement(),
|| SupervisorError::EmptyChildVersion {
caixa: child.nome().to_string(),
},
|reason| SupervisorError::ChildVersaoInvalid {
caixa: child.nome().to_string(),
versao: child.versao_requirement().to_string(),
reason,
},
)?;
crate::render::insert_first_seen(&mut seen, child.nome(), || {
SupervisorError::DuplicateChildCaixa {
caixa: child.nome().to_string(),
}
})?;
}
Ok(())
}
}
pub fn validate_no_self_supervision(
children: &[ChildSpec],
parent_nome: &str,
) -> Result<(), SupervisorError> {
for child in children {
if child.nome() == parent_nome {
return Err(SupervisorError::ChildSupervisesSelf {
caixa: parent_nome.to_string(),
});
}
}
Ok(())
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SupervisorError {
#[error("supervisor :estrategia {estrategia:?} requires at least one :children entry")]
NoChildren { estrategia: RestartStrategy },
#[error(
"SimpleOneForOne supervisors must declare zero static children (children spawn dynamically)"
)]
SimpleOneForOneWithStaticChildren,
#[error(":max-restarts must be > 0")]
ZeroMaxRestarts,
#[error(
":supervisor :max-restarts ({max_restarts}) exceeds the supervisor-policy ceiling \
(SUPERVISOR_MAX_RESTARTS_MAX = 1000) — a value above this cap turns the typed \
restart-intensity policy into a no-op supervisor: the escalation threshold is \
structurally so high that no realistic restarts-per-:restart-window traffic shape \
can reach it, so the supervisor never escalates to its parent and a bad child can \
loop inside the window indefinitely. Every typed-slot consumer (Erlang/OTP's \
MaxIntensity/Period ratio, the future wasm-operator's per-supervisor \
restart-intensity counter, the M4 mesh.pleme.io/v1alpha1/Supervisor CR \
materializer's admission webhook) emits a `:max-restarts` declaration that is \
structurally never reached. Pin a value in 1..=1000 (Erlang/OTP / Elixir / Riak \
Core / RabbitMQ production playbooks recommend 3..=100; the OTP `supervisor` \
callback module's `MaxR = 1` minimal-restart default sits at the bottom of the \
band) or restructure the supervision tree (split the flaky child into its own \
sub-supervisor with a tighter budget) if you need a higher restart tolerance."
)]
MaxRestartsExceedsCap { max_restarts: u32 },
#[error(
":restart-window must be > 0 when set — Erlang/OTP's MaxIntensity/Period \
requires Period > 0; a zero window either trips on the first failure or \
never trips depending on operator interpretation. Omit :restart-window to \
express `never reset`; carry a positive duration to express the window."
)]
RestartWindowZero,
#[error(
":supervisor :restart-window ({window:?}) carries a sub-millisecond residue the shared `duration_codec` cannot round-trip — \
the codec truncates to `as_millis()` before picking the canonical unit, so a value with `subsec_nanos() % 1_000_000 != 0` either \
truncates on first serialize (e.g. `Duration::from_micros(1500)` → \"1ms\" → `Duration::from_millis(1)` ≠ original) or renders \
as \"0s\" the `RestartWindowZero` arm then rejects on re-validate. Pin an integer-millisecond magnitude in the canonical authoring form \
(`<integer><unit>` for unit ∈ {{ms, s, m, h}}, e.g. `\"500ms\"`, `\"30s\"`, `\"2m\"`, `\"1h\"`) or omit the field for `never reset`"
)]
RestartWindowNotCanonical { window: Duration },
#[error(
":supervisor :restart-window ({window:?}) exceeds the supervisor-policy ceiling \
(SUPERVISOR_RESTART_WINDOW_MAX = 1h = 3600s) — a value above this cap turns the typed \
per-supervisor rolling-window restart-intensity counter into a lifetime counter: the \
failure-counting window is structurally so long that transient restarts are never \
forgotten, the MaxIntensity/Period ratio degenerates from `trip the parent supervisor \
when the child has exceeded its restart budget within the recent window` to `trip the \
parent when the child has exceeded its restart budget over its lifetime`, and the \
supervisor's reset semantic never reaches the child — every typed-slot consumer \
(Erlang/OTP's MaxIntensity/Period reconciler, the future wasm-operator's \
per-supervisor restart-intensity counter, the M4 mesh.pleme.io/v1alpha1/Supervisor CR \
materializer's admission webhook, the caixa-operator's hierarchical reconciliation \
scheduler) emits a `:restart-window` declaration that is structurally a no-op rolling \
window. Pin a value in 1ms..=1h (Learn You Some Erlang's `{{intensity, 5, 60}}` \
worker-supervisor `Period = 60s` default, Elixir's `Supervisor` `max_seconds: 5` \
default, OTP's `supervisor` callback module `MaxT = 5..=60` typical, Riak Core's \
`MaxT ∈ 10s..=300s`, RabbitMQ broker-supervisor `MaxT = 5s` default — every Erlang/OTP \
/ Elixir production playbook sits in the 5s..=300s band; the longest documented \
per-supervisor restart-window any pleme-io substrate playbook recommends maxes at \
~30m) or omit :restart-window to express `never reset` (the supervisor's restart \
budget then becomes a strict lifetime counter by design, not a degenerate one — the \
author surfaces the lifetime-counter semantic explicitly at the slot, rather than \
hiding it behind a rolling-window declaration the cap arm rejects)"
)]
RestartWindowExceedsCap { window: Duration },
#[error("child entry has empty :caixa name")]
EmptyChildName,
#[error(
"child :caixa {caixa:?} is not a valid DNS-1123 label: {reason} \
(the K8s apiserver enforces this rule on every `metadata.name` / Service \
name / label value the child name lands in — the per-child \
`wasm.pleme.io/v1alpha1/ComputeUnit.metadata.name`, the `LABEL_PROGRAM` \
label value, and the future wasm-operator per-child Service `metadata.name` \
— each apiserver-side schema rejects names that don't match; use a \
lowercase alphanumeric + hyphen identifier like `\"worker\"` or `\"cache-v2\"`)"
)]
ChildCaixaInvalid { caixa: String, reason: String },
#[error("child {caixa:?} has empty :versao constraint")]
EmptyChildVersion { caixa: String },
#[error(
"child {caixa:?} :versao {versao:?} is not a valid semver requirement: \
{reason} (use Cargo-shaped forms like `\"^0.1\"`, `\"~0.1.2\"`, \
`\"0.1.0\"`, or `\"*\"` — the same shape `:deps :versao` and \
`:membros :versao` carry; the lacre pipeline resolves all three \
through the same parser)"
)]
ChildVersaoInvalid {
caixa: String,
versao: String,
reason: String,
},
#[error(
"child {caixa:?} appears more than once (Erlang/OTP requires unique \
child_spec.id per supervisor; duplicate children materialize as duplicate \
ComputeUnits in the rendered chart, one silently overwriting the other)"
)]
DuplicateChildCaixa { caixa: String },
#[error(
"supervisor {caixa:?} lists itself as a :children entry — a supervisor is \
never its own child (the supervision tree is a DAG rooted at the supervisor; \
OTP child specs reference distinct child processes). Since every :nome is a \
globally-unique substrate identity, a child naming the supervisor's own :nome \
is a one-node reconciliation cycle, not a coincidentally-named peer; drop the \
self-referential :children entry or rename it to the actual child caixa."
)]
ChildSupervisesSelf { caixa: String },
}
pub mod duration_codec {
use super::Duration;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> {
match v {
Some(d) => s.serialize_str(&render(*d)),
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
let opt: Option<String> = Option::deserialize(d)?;
match opt {
None => Ok(None),
Some(s) => parse(&s).map(Some).map_err(serde::de::Error::custom),
}
}
pub(crate) fn parse(s: &str) -> Result<Duration, String> {
if let Some(b) = crate::render::find_ascii_whitespace_byte(s) {
return Err(format!(
"duration: value {s:?} contains whitespace byte 0x{b:02x} — the canonical \
authoring form for the typed duration slots routed through this shared codec \
(`:supervisor :restart-window`, `:politicas :timeout`, \
`:politicas :circuit-breaker :window`) is `<integer><unit>` (e.g. \
`\"30s\"`, `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no whitespace bytes \
anywhere. A whitespace-carrying shape (`\" 30s\"`, `\"30s \"`, `\"30 s\"`, \
`\"\\t30s\"`, `\"30s\\n\"`) round-trips through `render` to a *different* \
canonical form (`\"30s\"`) on first serialize — breaking the THEORY.md \
Part V render-determinism contract every typed slot carries. Strip every \
whitespace byte (write `\"30s\"` verbatim)"
));
}
if let Some(ch) = crate::render::find_non_ascii_whitespace_char(s) {
return Err(format!(
"duration: value {s:?} contains non-ASCII Unicode whitespace character \
{ch:?} (U+{cp:04X}) — the canonical authoring form for the typed \
duration slots routed through this shared codec (`:supervisor \
:restart-window`, `:politicas :timeout`, `:politicas :circuit-breaker \
:window`) is `<integer><unit>` (e.g. `\"30s\"`, `\"500ms\"`, `\"2m\"`, \
`\"1h\"`) with no whitespace characters anywhere (ASCII or Unicode). A \
non-ASCII-whitespace-carrying shape (`\"\\u{{00A0}}30s\"`, \
`\"30s\\u{{2028}}\"`, `\"30\\u{{2003}}s\"`) survives the ASCII byte-scan \
but `str::trim` (which uses `char::is_whitespace` — the Unicode \
`White_Space` property, strictly wider than the ASCII byte set) silently \
strips it at parse entry, and the value round-trips through `render` to \
a *different* canonical form (`\"30s\"`) on first serialize — breaking \
the THEORY.md Part V render-determinism contract every typed slot \
carries. Strip every non-ASCII whitespace character (write `\"30s\"` \
verbatim with only ASCII bytes)",
cp = ch as u32
));
}
let s = s.trim();
let split = s.find(|c: char| c.is_ascii_alphabetic()).unwrap_or(s.len());
let (num_part, unit) = s.split_at(split);
let num_trim = num_part.trim();
let digit_only = crate::render::is_digit_only_magnitude(num_trim);
if !digit_only {
let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
if numeric {
return Err(format!(
"duration: magnitude {num_trim:?} is not a non-negative integer — the \
canonical authoring form for the typed duration slots routed through \
this shared codec (`:supervisor :restart-window`, `:politicas :timeout`, \
`:politicas :circuit-breaker :window`) is `<integer><unit>` (e.g. \
`\"30s\"`, `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no decimal point and \
no leading `+` / `-` sign. A fractional / decimal-shaped magnitude \
(`\"1.5s\"`, `\"1.0s\"`, `\"0.5m\"`, `\"+30s\"`, `\"-30s\"`) round-trips \
through `render` to a *different* canonical form (`\"1500ms\"`, `\"1s\"`, \
`\"30s\"`, `\"30s\"`, `\"30s\"`) on first serialize — breaking the \
THEORY.md Part V render-determinism contract every typed slot carries. \
Pick an integer magnitude in the unit that divides cleanly (write \
`\"1500ms\"` instead of `\"1.5s\"`; `\"30s\"` instead of `\"0.5m\"`)"
));
}
return Err(format!("bad duration magnitude in {s:?}"));
}
if crate::render::is_leading_zero_padded_magnitude(num_trim) {
return Err(format!(
"duration: magnitude {num_trim:?} has a non-canonical leading zero — the \
canonical authoring form for the typed duration slots routed through \
this shared codec (`:supervisor :restart-window`, `:politicas :timeout`, \
`:politicas :circuit-breaker :window`) is `<integer><unit>` (e.g. \
`\"30s\"`, `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no leading-zero padding \
on the magnitude. A leading-zero magnitude (`\"030s\"`, `\"00s\"`, \
`\"01h\"`, `\"0500ms\"`) round-trips through `render` to a *different* \
canonical form (`\"30s\"`, `\"0s\"`, `\"1h\"`, `\"500ms\"`) on first \
serialize — breaking the THEORY.md Part V render-determinism contract \
every typed slot carries. Strip the leading zeros (write \
`\"30s\"` instead of `\"030s\"`)"
));
}
let num: u64 = num_trim.parse::<u64>().map_err(|_| {
format!("bad duration magnitude in {s:?} (digit-only magnitude overflows u64)")
})?;
let unit_trim = unit.trim();
let dur = match unit_trim {
"ms" => Duration::from_millis(num),
"s" | "" => Duration::from_secs(num),
"m" => Duration::from_secs(num.checked_mul(60).ok_or_else(|| {
format!("duration {num}{unit_trim} overflows u64 (magnitude × 60 > 2^64-1)")
})?),
"h" => Duration::from_secs(num.checked_mul(3600).ok_or_else(|| {
format!("duration {num}{unit_trim} overflows u64 (magnitude × 3600 > 2^64-1)")
})?),
other => return Err(format!("unknown duration unit {other:?}")),
};
Ok(dur)
}
pub fn render(d: Duration) -> String {
let total_ms = d.as_millis();
if total_ms == 0 {
return "0s".into();
}
if total_ms % (3600 * 1000) == 0 {
return format!("{}h", total_ms / (3600 * 1000));
}
if total_ms % (60 * 1000) == 0 {
return format!("{}m", total_ms / (60 * 1000));
}
if total_ms % 1000 == 0 {
return format!("{}s", total_ms / 1000);
}
format!("{total_ms}ms")
}
#[must_use]
pub fn is_integer_millisecond_duration(d: Duration) -> bool {
d.subsec_nanos().is_multiple_of(1_000_000)
}
}
pub mod duration_codec_required {
use super::Duration;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&super::duration_codec::render(*v))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
let s = String::deserialize(d)?;
super::duration_codec::parse(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn child(name: &str, ver: &str, restart: RestartPolicy) -> ChildSpec {
ChildSpec {
caixa: name.into(),
versao: ver.into(),
restart,
}
}
#[test]
fn default_has_one_for_one_and_5_restarts_in_60s() {
let s = SupervisorSpec::default();
assert_eq!(s.estrategia, RestartStrategy::OneForOne);
assert_eq!(s.max_restarts, 5);
assert_eq!(s.restart_window, Some(Duration::from_secs(60)));
assert!(s.children.is_empty());
}
#[test]
fn validate_one_for_one_requires_children() {
let mut s = SupervisorSpec::default();
s.children = vec![];
assert!(matches!(
s.validate().unwrap_err(),
SupervisorError::NoChildren { .. }
));
s.children = vec![child("worker", "^0.1", RestartPolicy::Permanent)];
s.validate().unwrap();
}
#[test]
fn validate_simple_one_for_one_forbids_static_children() {
let mut s = SupervisorSpec {
estrategia: RestartStrategy::SimpleOneForOne,
..SupervisorSpec::default()
};
s.children
.push(child("w", "^0.1", RestartPolicy::Permanent));
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::SimpleOneForOneWithStaticChildren
);
s.children.clear();
s.validate().unwrap();
}
#[test]
fn validate_rejects_zero_max_restarts() {
let s = SupervisorSpec {
max_restarts: 0,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(s.validate().unwrap_err(), SupervisorError::ZeroMaxRestarts);
}
#[test]
fn validate_rejects_max_restarts_above_cap() {
let s = SupervisorSpec {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX + 1,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::MaxRestartsExceedsCap {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX + 1,
}
);
}
#[test]
fn validate_rejects_max_restarts_far_above_cap() {
let s = SupervisorSpec {
max_restarts: u32::MAX,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::MaxRestartsExceedsCap {
max_restarts: u32::MAX,
}
);
}
#[test]
fn validate_accepts_max_restarts_at_cap() {
let s = SupervisorSpec {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.expect("max_restarts == SUPERVISOR_MAX_RESTARTS_MAX must validate");
}
#[test]
fn validate_accepts_max_restarts_typical_values() {
for n in [1u32, 3, 5, 10, 20, 50, 100, 200, 500, 1000] {
let s = SupervisorSpec {
max_restarts: n,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.unwrap_or_else(|e| panic!("max_restarts={n} must validate; got {e:?}"));
}
}
#[test]
fn zero_max_restarts_takes_precedence_over_cap() {
let s = SupervisorSpec {
max_restarts: 0,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::ZeroMaxRestarts,
"max_restarts == 0 must surface the zero-floor diagnostic, not the cap diagnostic"
);
}
#[test]
fn max_restarts_cap_takes_precedence_over_restart_window_gates() {
let s = SupervisorSpec {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX + 1,
restart_window: Some(Duration::ZERO),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::MaxRestartsExceedsCap {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX + 1,
},
"over-cap max_restarts must surface the cap diagnostic before any window-axis diagnostic"
);
}
#[test]
fn max_restarts_cap_diagnostic_carries_offending_value() {
let s = SupervisorSpec {
max_restarts: 50_000,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::MaxRestartsExceedsCap {
max_restarts: 50_000
}
),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("50000"),
":supervisor :max-restarts cap diagnostic must carry the offending value verbatim (got: {msg})"
);
}
#[test]
fn supervisor_max_restarts_cap_pins_canonical_value() {
assert_eq!(SUPERVISOR_MAX_RESTARTS_MAX, 1000);
}
#[test]
fn validate_rejects_empty_child_name() {
let s = SupervisorSpec {
children: vec![child("", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(s.validate().unwrap_err(), SupervisorError::EmptyChildName);
}
#[test]
fn validate_rejects_empty_child_version() {
let s = SupervisorSpec {
children: vec![child("w", "", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert!(matches!(
s.validate().unwrap_err(),
SupervisorError::EmptyChildVersion { .. }
));
}
#[test]
fn validate_rejects_invalid_child_versao_requirement() {
let s = SupervisorSpec {
children: vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("cache", "^bad-version", RestartPolicy::Transient),
],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildVersaoInvalid { ref caixa, ref versao, .. }
if caixa == "cache" && versao == "^bad-version"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_versao_with_double_caret_typo() {
let s = SupervisorSpec {
children: vec![child("worker", "^^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildVersaoInvalid { ref caixa, ref versao, .. }
if caixa == "worker" && versao == "^^0.1"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_versao_with_v_prefixed_tag() {
let s = SupervisorSpec {
children: vec![child("worker", "v0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildVersaoInvalid { ref caixa, ref versao, .. }
if caixa == "worker" && versao == "v0.1"
),
"got {err:?}"
);
}
#[test]
fn validate_accepts_canonical_child_versao_forms() {
for form in [
"^0.1", "~0.1.2", "0.1.0", "*", ">=0.1, <2", ] {
let s = SupervisorSpec {
children: vec![child("worker", form, RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.unwrap_or_else(|e| panic!("canonical form {form:?} must validate, got {e:?}"));
}
}
#[test]
fn child_versao_empty_takes_precedence_over_invalid() {
let s = SupervisorSpec {
children: vec![child("worker", "", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(err, SupervisorError::EmptyChildVersion { ref caixa } if caixa == "worker"),
"got {err:?}"
);
}
#[test]
fn child_versao_invalid_fires_before_duplicate_check() {
let s = SupervisorSpec {
children: vec![
child("worker", "^bad", RestartPolicy::Permanent),
child("cache", "^0.1", RestartPolicy::Transient),
child("worker", "^0.2", RestartPolicy::Permanent), ],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildVersaoInvalid { ref caixa, .. } if caixa == "worker"
),
"got {err:?}"
);
}
#[test]
fn child_versao_invalid_diagnostic_carries_offending_versao() {
let s = SupervisorSpec {
children: vec![child("worker", "not-a-req", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
let SupervisorError::ChildVersaoInvalid {
caixa,
versao,
reason,
} = err
else {
panic!("expected ChildVersaoInvalid, got other variant");
};
assert_eq!(caixa, "worker");
assert_eq!(versao, "not-a-req");
assert!(
!reason.is_empty(),
"ChildVersaoInvalid `reason` must carry the parser's wording verbatim"
);
}
#[test]
fn validate_rejects_child_caixa_with_uppercase() {
let s = SupervisorSpec {
children: vec![child("Worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
let SupervisorError::ChildCaixaInvalid { caixa, reason } = err else {
panic!("expected ChildCaixaInvalid, got other variant");
};
assert_eq!(caixa, "Worker");
assert!(
reason.contains("uppercase"),
"diagnostic must name the violation as `uppercase` (got: {reason:?})"
);
assert!(
reason.contains("\"worker\""),
"diagnostic must suggest the lower-cased fix verbatim (got: {reason:?})"
);
}
#[test]
fn validate_rejects_child_caixa_with_underscore() {
let s = SupervisorSpec {
children: vec![child("my_worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, ref reason }
if caixa == "my_worker" && reason.contains('_')
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_caixa_with_dot() {
let s = SupervisorSpec {
children: vec![child("team.worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, ref reason }
if caixa == "team.worker" && reason.contains('.')
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_caixa_with_leading_hyphen() {
let s = SupervisorSpec {
children: vec![child("-worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, ref reason }
if caixa == "-worker" && reason.contains("start and end")
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_caixa_with_trailing_hyphen() {
let s = SupervisorSpec {
children: vec![child("worker-", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, .. }
if caixa == "worker-"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_caixa_with_unicode() {
let s = SupervisorSpec {
children: vec![child("café", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, .. }
if caixa == "café"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_caixa_with_whitespace() {
let s = SupervisorSpec {
children: vec![child("my worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, .. }
if caixa == "my worker"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_child_caixa_too_long() {
let too_long = "a".repeat(64);
let s = SupervisorSpec {
children: vec![child(&too_long, "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
let SupervisorError::ChildCaixaInvalid { caixa, reason } = err else {
panic!("expected ChildCaixaInvalid, got other variant");
};
assert_eq!(caixa, too_long);
assert!(
reason.contains("63"),
"diagnostic must name the 63-byte cap (got: {reason:?})"
);
assert!(
reason.contains("64"),
"diagnostic must name the actual length (got: {reason:?})"
);
}
#[test]
fn child_caixa_max_length_validates() {
let max_label = "a".repeat(63);
let s = SupervisorSpec {
children: vec![child(&max_label, "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate().unwrap();
}
#[test]
fn validate_accepts_canonical_child_caixa_forms() {
for form in [
"worker",
"cache-v2",
"a",
"db",
"2-pool",
"payment-retry",
"0",
] {
let s = SupervisorSpec {
children: vec![child(form, "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.unwrap_or_else(|e| panic!("canonical form {form:?} must validate, got {e:?}"));
}
}
#[test]
fn child_caixa_empty_takes_precedence_over_invalid() {
let s = SupervisorSpec {
children: vec![child("", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert_eq!(err, SupervisorError::EmptyChildName);
}
#[test]
fn child_caixa_invalid_fires_before_versao_check() {
let s = SupervisorSpec {
children: vec![child("My_Worker", "", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, .. } if caixa == "My_Worker"
),
"got {err:?}"
);
}
#[test]
fn child_caixa_invalid_fires_before_duplicate_check() {
let s = SupervisorSpec {
children: vec![
child("Worker", "^0.1", RestartPolicy::Permanent),
child("cache", "^0.1", RestartPolicy::Transient),
child("worker", "^0.2", RestartPolicy::Permanent), ],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::ChildCaixaInvalid { ref caixa, .. } if caixa == "Worker"
),
"got {err:?}"
);
}
#[test]
fn child_caixa_invalid_diagnostic_carries_offending_caixa() {
let s = SupervisorSpec {
children: vec![child("My_Worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
let SupervisorError::ChildCaixaInvalid { caixa, reason } = err else {
panic!("expected ChildCaixaInvalid, got other variant");
};
assert_eq!(caixa, "My_Worker");
assert!(
!reason.is_empty(),
"ChildCaixaInvalid `reason` must carry the parser's wording verbatim"
);
}
#[test]
fn validate_accepts_none_restart_window() {
let s = SupervisorSpec {
restart_window: None,
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate().unwrap();
}
#[test]
fn validate_rejects_zero_restart_window() {
let s = SupervisorSpec {
restart_window: Some(Duration::ZERO),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowZero
);
}
#[test]
fn validate_rejects_sub_millisecond_restart_window() {
let s = SupervisorSpec {
restart_window: Some(Duration::from_micros(1500)),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::RestartWindowNotCanonical { window } => {
assert_eq!(window, Duration::from_micros(1500));
}
other => panic!("expected RestartWindowNotCanonical, got {other:?}"),
}
}
#[test]
fn validate_rejects_one_nanosecond_restart_window() {
let s = SupervisorSpec {
restart_window: Some(Duration::from_nanos(1)),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::RestartWindowNotCanonical { window } => {
assert_eq!(window, Duration::from_nanos(1));
}
other => panic!("expected RestartWindowNotCanonical, got {other:?}"),
}
}
#[test]
fn validate_rejects_nanosecond_past_canonical_boundary_restart_window() {
let w = Duration::from_nanos(1_000_001);
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowNotCanonical { window: w }
);
}
#[test]
fn validate_accepts_integer_millisecond_restart_window_values() {
for w in [
Duration::from_millis(1),
Duration::from_millis(500),
Duration::from_millis(1500),
Duration::from_secs(1),
Duration::from_secs(30),
Duration::from_secs(60),
Duration::from_secs(120),
Duration::from_secs(3600),
] {
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.unwrap_or_else(|e| panic!("integer-ms {w:?} must validate, got {e:?}"));
}
}
#[test]
fn validate_restart_window_zero_takes_precedence_over_canonical_gate() {
let s = SupervisorSpec {
restart_window: Some(Duration::ZERO),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowZero
);
}
#[test]
fn restart_window_canonical_diagnostic_carries_offending_duration() {
let w = Duration::from_micros(500);
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("500"),
"diagnostic must carry the offending magnitude verbatim (got {msg:?})"
);
assert!(
msg.contains("sub-millisecond"),
"diagnostic must name the sub-millisecond residue class (got {msg:?})"
);
}
#[test]
fn restart_window_validated_value_round_trips_through_codec() {
for w in [
Duration::from_millis(1),
Duration::from_millis(1500),
Duration::from_secs(30),
Duration::from_secs(3600),
] {
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate().unwrap();
let json = serde_json::to_string(&s).unwrap();
let back: SupervisorSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.restart_window, Some(w));
}
}
#[test]
fn validate_rejects_restart_window_above_cap() {
let w = SUPERVISOR_RESTART_WINDOW_MAX + Duration::from_secs(1);
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowExceedsCap { window: w }
);
}
#[test]
fn validate_rejects_restart_window_one_millisecond_above_cap() {
let w = SUPERVISOR_RESTART_WINDOW_MAX + Duration::from_millis(1);
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowExceedsCap { window: w }
);
}
#[test]
fn validate_rejects_restart_window_far_above_cap() {
for w in [
Duration::from_secs(86_400), Duration::from_secs(604_800), Duration::from_secs(1_000_000), ] {
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowExceedsCap { window: w }
);
}
}
#[test]
fn validate_accepts_restart_window_at_cap() {
let s = SupervisorSpec {
restart_window: Some(SUPERVISOR_RESTART_WINDOW_MAX),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.expect("restart_window == SUPERVISOR_RESTART_WINDOW_MAX must validate");
}
#[test]
fn validate_accepts_restart_window_typical_values() {
for w in [
Duration::from_millis(1),
Duration::from_millis(500),
Duration::from_secs(1),
Duration::from_secs(5), Duration::from_secs(10), Duration::from_secs(30),
Duration::from_secs(60), Duration::from_secs(120), Duration::from_secs(300), Duration::from_secs(900), Duration::from_secs(1800),
Duration::from_secs(3600), ] {
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate()
.unwrap_or_else(|e| panic!("restart_window={w:?} must validate; got {e:?}"));
}
}
#[test]
fn restart_window_zero_takes_precedence_over_cap() {
let s = SupervisorSpec {
restart_window: Some(Duration::ZERO),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowZero,
"Duration::ZERO must surface the zero-floor diagnostic, not the cap diagnostic"
);
}
#[test]
fn restart_window_canonical_takes_precedence_over_cap() {
let w = SUPERVISOR_RESTART_WINDOW_MAX + Duration::from_nanos(1);
let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowNotCanonical { window: w },
"sub-ms above-cap value must surface the canonical-form diagnostic, not the cap diagnostic"
);
}
#[test]
fn max_restarts_cap_takes_precedence_over_restart_window_cap() {
let w = SUPERVISOR_RESTART_WINDOW_MAX + Duration::from_secs(1);
let s = SupervisorSpec {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX + 1,
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::MaxRestartsExceedsCap {
max_restarts: SUPERVISOR_MAX_RESTARTS_MAX + 1,
},
"over-cap max_restarts must surface the cap diagnostic before any window-axis diagnostic"
);
}
#[test]
fn restart_window_cap_diagnostic_carries_offending_value() {
let w = Duration::from_secs(7200); let s = SupervisorSpec {
restart_window: Some(w),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(err, SupervisorError::RestartWindowExceedsCap { window } if window == w),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("7200"),
":supervisor :restart-window cap diagnostic must carry the offending value verbatim (got: {msg})"
);
}
#[test]
fn supervisor_restart_window_cap_pins_canonical_value() {
assert_eq!(SUPERVISOR_RESTART_WINDOW_MAX, Duration::from_secs(3600));
assert_eq!(SUPERVISOR_RESTART_WINDOW_MAX.as_millis(), 3_600_000);
assert_eq!(SUPERVISOR_RESTART_WINDOW_MAX, crate::LIMITS_WALL_CLOCK_MAX);
assert_eq!(SUPERVISOR_RESTART_WINDOW_MAX, crate::POLICY_TIMEOUT_MAX);
assert_eq!(
SUPERVISOR_RESTART_WINDOW_MAX,
crate::POLICY_BREAKER_WINDOW_MAX
);
}
#[test]
fn restart_window_cap_value_round_trips_through_codec() {
let s = SupervisorSpec {
restart_window: Some(SUPERVISOR_RESTART_WINDOW_MAX),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
s.validate().unwrap();
let json = serde_json::to_string(&s).unwrap();
assert!(
json.contains("\"1h\""),
"SUPERVISOR_RESTART_WINDOW_MAX must serialize to the canonical `\"1h\"` form (got {json})"
);
let back: SupervisorSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.restart_window, Some(SUPERVISOR_RESTART_WINDOW_MAX));
}
#[test]
fn validate_rejects_duplicate_child_caixa() {
let s = SupervisorSpec {
children: vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("cache", "^0.1", RestartPolicy::Transient),
child("worker", "^0.2", RestartPolicy::Permanent),
],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(err, SupervisorError::DuplicateChildCaixa { ref caixa } if caixa == "worker"),
"got {err:?}"
);
}
#[test]
fn validate_duplicate_child_diagnostic_names_first_collision() {
let s = SupervisorSpec {
children: vec![
child("a", "^0.1", RestartPolicy::Permanent),
child("b", "^0.1", RestartPolicy::Permanent),
child("a", "^0.1", RestartPolicy::Permanent),
child("b", "^0.1", RestartPolicy::Permanent),
],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(err, SupervisorError::DuplicateChildCaixa { ref caixa } if caixa == "a"),
"got {err:?}"
);
}
#[test]
fn validate_no_self_supervision_rejects_self_referential_child() {
let children = vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("orquestra", "^0.1", RestartPolicy::Permanent),
];
let err = validate_no_self_supervision(&children, "orquestra").unwrap_err();
assert!(
matches!(err, SupervisorError::ChildSupervisesSelf { ref caixa } if caixa == "orquestra"),
"got {err:?}"
);
}
#[test]
fn validate_no_self_supervision_accepts_distinct_children() {
let children = vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("sub-tree", "^0.1", RestartPolicy::Permanent),
];
validate_no_self_supervision(&children, "orquestra").unwrap();
}
#[test]
fn validate_no_self_supervision_empty_children_is_ok() {
validate_no_self_supervision(&[], "orquestra").unwrap();
}
#[test]
fn validate_simple_one_for_one_skips_uniqueness_check() {
let s = SupervisorSpec {
estrategia: RestartStrategy::SimpleOneForOne,
restart_window: None,
children: vec![],
..SupervisorSpec::default()
};
s.validate().unwrap();
let s_zero = SupervisorSpec {
estrategia: RestartStrategy::SimpleOneForOne,
restart_window: Some(Duration::ZERO),
children: vec![],
..SupervisorSpec::default()
};
assert_eq!(
s_zero.validate().unwrap_err(),
SupervisorError::RestartWindowZero
);
}
#[test]
fn validate_zero_window_runs_after_max_restarts_check() {
let s = SupervisorSpec {
max_restarts: 0,
restart_window: Some(Duration::ZERO),
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(s.validate().unwrap_err(), SupervisorError::ZeroMaxRestarts);
}
#[test]
fn round_trip_all_strategies() {
for strat in [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
RestartStrategy::SimpleOneForOne,
] {
let s = SupervisorSpec {
estrategia: strat,
children: if strat.is_simple_one_for_one() {
vec![]
} else {
vec![child("w", "^0.1", RestartPolicy::Permanent)]
},
..SupervisorSpec::default()
};
let json = serde_json::to_string(&s).unwrap();
let back: SupervisorSpec = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
}
#[test]
fn round_trip_all_restart_policies() {
for policy in [
RestartPolicy::Permanent,
RestartPolicy::Temporary,
RestartPolicy::Transient,
] {
let c = child("w", "^0.1", policy);
let json = serde_json::to_string(&c).unwrap();
let back: ChildSpec = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
}
#[test]
fn restart_strategy_is_simple_one_for_one_predicate_partitions_the_arm_set() {
let cases: &[(RestartStrategy, bool)] = &[
(RestartStrategy::OneForOne, false),
(RestartStrategy::OneForAll, false),
(RestartStrategy::RestForOne, false),
(RestartStrategy::SimpleOneForOne, true),
];
for (variant, expected) in cases {
assert_eq!(
variant.is_simple_one_for_one(),
*expected,
"RestartStrategy::{variant:?}.is_simple_one_for_one() must \
return {expected} (partition invariant on the \
IsVariant-derived arm-discriminator predicate — every \
test-fixture site that partitions the `:children` slot \
shape on `SimpleOneForOne ↔ non-SimpleOneForOne` keys \
off this typed dispatch, so a derive regression must \
surface here rather than at the fixture-refusal site)"
);
}
}
#[test]
fn restart_strategy_fixture_partition_routes_through_is_simple_one_for_one_predicate() {
let cases = [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
RestartStrategy::SimpleOneForOne,
];
for strat in cases {
let via_predicate = strat.is_simple_one_for_one();
let via_matches = matches!(strat, RestartStrategy::SimpleOneForOne);
assert_eq!(
via_predicate, via_matches,
"RestartStrategy::{strat:?}: is_simple_one_for_one() must \
byte-equal matches!(_, RestartStrategy::SimpleOneForOne) — \
the pre-lift open-coded pattern and the \
IsVariant-derived predicate are the same axis, \
one typed dispatch"
);
}
}
#[test]
fn duration_codec_round_trip_canonical_units() {
let cases = [
("30s", Duration::from_secs(30)),
("5m", Duration::from_secs(300)),
("1h", Duration::from_secs(3600)),
("500ms", Duration::from_millis(500)),
];
for (lit, dur) in cases {
let s = SupervisorSpec {
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
restart_window: Some(dur),
..SupervisorSpec::default()
};
let json = serde_json::to_string(&s).unwrap();
assert!(
json.contains(&format!("\"{lit}\"")),
"expected \"{lit}\" in {json}"
);
let back: SupervisorSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.restart_window, Some(dur));
}
}
#[test]
fn duration_canonicalizes_to_largest_unit() {
let s = SupervisorSpec {
children: vec![child("w", "^0.1", RestartPolicy::Permanent)],
restart_window: Some(Duration::from_secs(60)),
..SupervisorSpec::default()
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("\"1m\""), "{json}");
let back: SupervisorSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.restart_window, Some(Duration::from_secs(60)));
}
#[test]
fn three_child_one_for_one_validates() {
let s = SupervisorSpec {
estrategia: RestartStrategy::OneForOne,
max_restarts: 5,
restart_window: Some(Duration::from_secs(60)),
children: vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("cache", "^0.1", RestartPolicy::Transient),
child("scratch", "^0.1", RestartPolicy::Temporary),
],
};
s.validate().unwrap();
}
#[test]
fn json_uses_pascal_case_for_strategy_and_policy() {
let c = child("w", "^0.1", RestartPolicy::Permanent);
let json = serde_json::to_string(&c).unwrap();
assert!(json.contains("\"Permanent\""));
assert!(!json.contains("\"permanent\""));
let s = SupervisorSpec {
estrategia: RestartStrategy::OneForOne,
children: vec![c],
..SupervisorSpec::default()
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("\"estrategia\":\"OneForOne\""));
}
#[test]
fn parse_accepts_integer_canonical_units() {
for (lit, dur) in [
("30s", Duration::from_secs(30)),
("500ms", Duration::from_millis(500)),
("2m", Duration::from_secs(120)),
("1h", Duration::from_secs(3600)),
("0s", Duration::ZERO),
] {
assert_eq!(
duration_codec::parse(lit).unwrap(),
dur,
"parse({lit:?}) should be {dur:?}"
);
}
}
#[test]
fn parse_accepts_bare_integer_as_seconds() {
assert_eq!(
duration_codec::parse("30").unwrap(),
Duration::from_secs(30)
);
}
#[test]
fn parse_rejects_fractional_seconds_with_canonical_form_diagnostic() {
let err = duration_codec::parse("1.5s").unwrap_err();
assert!(err.contains("\"1.5\""), "missing magnitude in {err:?}");
assert!(
err.contains("not a non-negative integer"),
"missing canonical-form reason in {err:?}"
);
assert!(
err.contains("\"1500ms\""),
"missing canonical-form remediation in {err:?}"
);
}
#[test]
fn parse_rejects_decimal_shaped_integer_seconds() {
let err = duration_codec::parse("1.0s").unwrap_err();
assert!(err.contains("\"1.0\""), "missing magnitude in {err:?}");
assert!(
err.contains("not a non-negative integer"),
"missing canonical-form reason in {err:?}"
);
}
#[test]
fn parse_rejects_half_unit_minute() {
let err = duration_codec::parse("0.5m").unwrap_err();
assert!(err.contains("\"0.5\""), "missing magnitude in {err:?}");
assert!(
err.contains("\"30s\""),
"missing canonical-form remediation in {err:?}"
);
}
#[test]
fn parse_rejects_leading_plus_sign() {
let err = duration_codec::parse("+30s").unwrap_err();
assert!(err.contains("\"+30\""), "missing magnitude in {err:?}");
assert!(
err.contains("not a non-negative integer"),
"missing canonical-form reason in {err:?}"
);
}
#[test]
fn parse_rejects_leading_minus_sign() {
let err = duration_codec::parse("-30s").unwrap_err();
assert!(err.contains("\"-30\""), "missing magnitude in {err:?}");
assert!(
err.contains("not a non-negative integer"),
"missing canonical-form reason in {err:?}"
);
}
#[test]
fn parse_garbage_still_falls_through_to_bad_magnitude() {
let err = duration_codec::parse("--1s").unwrap_err();
assert!(
err.contains("bad duration magnitude"),
"expected bad-magnitude wording in {err:?}"
);
}
#[test]
fn parse_digit_only_magnitude_carries_zero_f64_drift() {
assert_eq!(
duration_codec::parse("3600s").unwrap(),
Duration::from_secs(3600)
);
assert_eq!(
duration_codec::parse("60m").unwrap(),
Duration::from_secs(3600)
);
assert_eq!(
duration_codec::parse("1h").unwrap(),
Duration::from_secs(3600)
);
assert_eq!(
duration_codec::parse("999ms").unwrap(),
Duration::from_millis(999)
);
}
#[test]
fn restart_window_serde_rejects_fractional_seconds() {
let payload = r#"{"estrategia":"OneForOne","maxRestarts":5,
"restartWindow":"1.5s",
"children":[{"caixa":"w","versao":"^0.1","restart":"Permanent"}]}"#;
let err = serde_json::from_str::<SupervisorSpec>(payload).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not a non-negative integer"),
"expected integer-magnitude diagnostic in {msg:?}"
);
assert!(msg.contains("\"1.5\""), "missing magnitude in {msg:?}");
}
#[test]
fn restart_window_serde_rejects_leading_plus() {
let payload = r#"{"estrategia":"OneForOne","maxRestarts":5,
"restartWindow":"+30s",
"children":[{"caixa":"w","versao":"^0.1","restart":"Permanent"}]}"#;
let err = serde_json::from_str::<SupervisorSpec>(payload).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("\"+30\""), "missing magnitude in {msg:?}");
assert!(
msg.contains("not a non-negative integer"),
"missing canonical-form reason in {msg:?}"
);
}
#[test]
fn parse_rejects_leading_zero_magnitude() {
let err = duration_codec::parse("030s").unwrap_err();
assert!(
err.contains("non-canonical leading zero"),
"expected leading-zero diagnostic in {err:?}"
);
assert!(err.contains("\"030\""), "missing magnitude in {err:?}");
assert!(
err.contains("\"30s\""),
"missing canonical-form remediation in {err:?}"
);
assert!(
err.contains("THEORY.md"),
"missing render-determinism citation in {err:?}"
);
}
#[test]
fn parse_rejects_multi_digit_zero_magnitude() {
let err = duration_codec::parse("00s").unwrap_err();
assert!(
err.contains("non-canonical leading zero"),
"expected leading-zero diagnostic in {err:?}"
);
assert!(err.contains("\"00\""), "missing magnitude in {err:?}");
}
#[test]
fn parse_rejects_leading_zero_per_hour_window() {
let err = duration_codec::parse("01h").unwrap_err();
assert!(
err.contains("non-canonical leading zero"),
"expected leading-zero diagnostic in {err:?}"
);
assert!(err.contains("\"01\""), "missing magnitude in {err:?}");
}
#[test]
fn parse_rejects_leading_zero_bare_integer_as_seconds() {
let err = duration_codec::parse("030").unwrap_err();
assert!(
err.contains("non-canonical leading zero"),
"expected leading-zero diagnostic in {err:?}"
);
assert!(err.contains("\"030\""), "missing magnitude in {err:?}");
}
#[test]
fn parse_accepts_single_zero_magnitude_at_codec_layer() {
assert_eq!(duration_codec::parse("0s").unwrap(), Duration::ZERO);
assert_eq!(duration_codec::parse("0ms").unwrap(), Duration::ZERO);
assert_eq!(duration_codec::parse("0").unwrap(), Duration::ZERO);
}
#[test]
fn parse_accepts_canonical_magnitude_with_leading_one() {
assert_eq!(
duration_codec::parse("100ms").unwrap(),
Duration::from_millis(100)
);
assert_eq!(
duration_codec::parse("100s").unwrap(),
Duration::from_secs(100)
);
assert_eq!(
duration_codec::parse("10m").unwrap(),
Duration::from_secs(600)
);
assert_eq!(
duration_codec::parse("10h").unwrap(),
Duration::from_secs(36_000)
);
}
#[test]
fn restart_window_serde_rejects_leading_zero() {
let payload = r#"{"estrategia":"OneForOne","maxRestarts":5,
"restartWindow":"030s",
"children":[{"caixa":"w","versao":"^0.1","restart":"Permanent"}]}"#;
let err = serde_json::from_str::<SupervisorSpec>(payload).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("non-canonical leading zero"),
"expected leading-zero diagnostic in {msg:?}"
);
assert!(msg.contains("\"030\""), "missing magnitude in {msg:?}");
}
#[test]
fn parse_rejects_leading_whitespace() {
let err = duration_codec::parse(" 30s").unwrap_err();
assert!(
err.contains("contains whitespace byte"),
"expected whitespace diagnostic in {err:?}"
);
assert!(err.contains("0x20"), "missing offending byte in {err:?}");
assert!(
err.contains("THEORY.md"),
"missing render-determinism contract citation in {err:?}"
);
}
#[test]
fn parse_rejects_trailing_whitespace() {
let err = duration_codec::parse("30s ").unwrap_err();
assert!(
err.contains("contains whitespace byte"),
"expected whitespace diagnostic in {err:?}"
);
assert!(err.contains("0x20"), "missing offending byte in {err:?}");
}
#[test]
fn parse_rejects_internal_whitespace_between_magnitude_and_unit() {
let err = duration_codec::parse("30 s").unwrap_err();
assert!(
err.contains("contains whitespace byte"),
"expected whitespace diagnostic in {err:?}"
);
assert!(err.contains("0x20"), "missing offending byte in {err:?}");
}
#[test]
fn parse_rejects_tab_byte() {
let err = duration_codec::parse("\t30s").unwrap_err();
assert!(
err.contains("contains whitespace byte"),
"expected whitespace diagnostic in {err:?}"
);
assert!(
err.contains("0x09"),
"missing offending tab byte in {err:?}"
);
}
#[test]
fn restart_window_serde_rejects_whitespace() {
let payload = r#"{"estrategia":"OneForOne","maxRestarts":5,
"restartWindow":" 30s",
"children":[{"caixa":"w","versao":"^0.1","restart":"Permanent"}]}"#;
let err = serde_json::from_str::<SupervisorSpec>(payload).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("contains whitespace byte"),
"expected whitespace diagnostic in {msg:?}"
);
assert!(msg.contains("0x20"), "missing offending byte in {msg:?}");
}
#[test]
fn duration_codec_parse_rejects_leading_nbsp() {
let err = duration_codec::parse("\u{00A0}30s").unwrap_err();
assert!(
err.contains("non-ASCII Unicode whitespace character"),
"expected non-ASCII whitespace diagnostic in {err:?}"
);
assert!(err.contains("U+00A0"), "missing codepoint in {err:?}");
}
#[test]
fn duration_codec_parse_rejects_trailing_line_separator() {
let err = duration_codec::parse("30s\u{2028}").unwrap_err();
assert!(
err.contains("non-ASCII Unicode whitespace character"),
"expected non-ASCII whitespace diagnostic in {err:?}"
);
assert!(err.contains("U+2028"), "missing codepoint in {err:?}");
}
#[test]
fn duration_codec_parse_accepts_ascii_only_forms_after_unicode_arm() {
assert_eq!(
duration_codec::parse("30s").unwrap(),
Duration::from_secs(30)
);
assert_eq!(
duration_codec::parse("500ms").unwrap(),
Duration::from_millis(500)
);
assert_eq!(
duration_codec::parse("1h").unwrap(),
Duration::from_secs(3600)
);
}
#[test]
fn restart_window_serde_rejects_non_ascii_whitespace() {
let payload = "{\"estrategia\":\"OneForOne\",\"maxRestarts\":5,\
\"restartWindow\":\"\u{00A0}30s\",\
\"children\":[{\"caixa\":\"w\",\"versao\":\"^0.1\",\"restart\":\"Permanent\"}]}";
let err = serde_json::from_str::<SupervisorSpec>(payload).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("non-ASCII Unicode whitespace character"),
"expected non-ASCII whitespace diagnostic in {msg:?}"
);
assert!(msg.contains("U+00A0"), "missing codepoint in {msg:?}");
}
#[test]
fn supervisor_spec_serde_keys_match_lifted_supervisor_key_consts() {
let spec = SupervisorSpec {
estrategia: RestartStrategy::OneForOne,
max_restarts: 5,
restart_window: Some(Duration::from_secs(60)),
children: vec![ChildSpec {
caixa: "w".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
}],
};
let json = serde_json::to_string(&spec).unwrap();
for key in [
crate::render::SUPERVISOR_KEY_ESTRATEGIA,
crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
crate::render::SUPERVISOR_KEY_CHILDREN,
] {
let quoted = format!("\"{key}\"");
assert!(
json.contains("ed),
"serialized SupervisorSpec must carry the lifted \
SUPERVISOR_KEY_* byte-sequence {quoted} verbatim in \
the JSON emission (got: {json})",
);
}
}
#[test]
fn supervisor_key_consts_are_pairwise_distinct() {
let all = [
crate::render::SUPERVISOR_KEY_ESTRATEGIA,
crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
crate::render::SUPERVISOR_KEY_CHILDREN,
];
for (i, a) in all.iter().enumerate() {
for b in all.iter().skip(i + 1) {
assert_ne!(
a, b,
"SUPERVISOR_KEY_* consts must be pairwise-distinct \
canonical byte-sequences — got `{a}` == `{b}`",
);
}
}
}
#[test]
fn supervisor_key_consts_are_lower_camel_case_shape() {
for key in [
crate::render::SUPERVISOR_KEY_ESTRATEGIA,
crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
crate::render::SUPERVISOR_KEY_CHILDREN,
] {
assert!(
!key.is_empty(),
"SUPERVISOR_KEY_* must be non-empty (got {key:?})"
);
let first = key.chars().next().unwrap();
assert!(
first.is_ascii_lowercase(),
"SUPERVISOR_KEY_* must lead with an ASCII-lowercase byte \
(got {key:?}, leads with {first:?})",
);
assert!(
key.chars().all(|c| c.is_ascii_alphanumeric()),
"SUPERVISOR_KEY_* must be ASCII-alphanumeric only \
— no `_` / `-` / `:` / `.` / whitespace (got {key:?})",
);
}
}
#[test]
fn supervisor_key_consts_are_byte_distinct_from_supervisor_author_key_peers() {
let pairs = [
(
crate::render::SUPERVISOR_KEY_ESTRATEGIA,
crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
),
(
crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
),
(
crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
),
(
crate::render::SUPERVISOR_KEY_CHILDREN,
crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN,
),
];
for (json_key, author_key) in pairs {
assert_ne!(
json_key, author_key,
"SUPERVISOR_KEY_* (JSON side) must differ byte-for-byte \
from the peer SUPERVISOR_AUTHOR_KEY_* (author side); \
got JSON `{json_key}` == author `{author_key}`",
);
}
}
#[test]
fn child_spec_serde_keys_match_lifted_supervisor_child_key_consts() {
let c = ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
let json = serde_json::to_string(&c).unwrap();
for key in [
crate::render::SUPERVISOR_CHILD_KEY_CAIXA,
crate::render::SUPERVISOR_CHILD_KEY_VERSAO,
crate::render::SUPERVISOR_CHILD_KEY_RESTART,
] {
let quoted = format!("\"{key}\"");
assert!(
json.contains("ed),
"serialized ChildSpec must carry the lifted \
SUPERVISOR_CHILD_KEY_* byte-sequence {quoted} verbatim \
in the JSON emission (got: {json})",
);
}
}
#[test]
fn supervisor_child_key_consts_are_pairwise_distinct() {
let all = [
crate::render::SUPERVISOR_CHILD_KEY_CAIXA,
crate::render::SUPERVISOR_CHILD_KEY_VERSAO,
crate::render::SUPERVISOR_CHILD_KEY_RESTART,
];
for (i, a) in all.iter().enumerate() {
for b in all.iter().skip(i + 1) {
assert_ne!(
a, b,
"SUPERVISOR_CHILD_KEY_* consts must be pairwise-\
distinct canonical byte-sequences — got `{a}` == `{b}`",
);
}
}
}
#[test]
fn supervisor_child_key_consts_are_lower_camel_case_shape() {
for key in [
crate::render::SUPERVISOR_CHILD_KEY_CAIXA,
crate::render::SUPERVISOR_CHILD_KEY_VERSAO,
crate::render::SUPERVISOR_CHILD_KEY_RESTART,
] {
assert!(
!key.is_empty(),
"SUPERVISOR_CHILD_KEY_* must be non-empty (got {key:?})"
);
let first = key.chars().next().unwrap();
assert!(
first.is_ascii_lowercase(),
"SUPERVISOR_CHILD_KEY_* must lead with an ASCII-lowercase \
byte (got {key:?}, leads with {first:?})",
);
assert!(
key.chars().all(|c| c.is_ascii_alphanumeric()),
"SUPERVISOR_CHILD_KEY_* must be ASCII-alphanumeric only \
— no `_` / `-` / `:` / `.` / whitespace (got {key:?})",
);
}
}
#[test]
fn restart_strategy_variants_serialize_to_lifted_scalar_values() {
for (variant, expected) in [
(
RestartStrategy::OneForOne,
crate::render::SUPERVISOR_ESTRATEGIA_ONE_FOR_ONE,
),
(
RestartStrategy::OneForAll,
crate::render::SUPERVISOR_ESTRATEGIA_ONE_FOR_ALL,
),
(
RestartStrategy::RestForOne,
crate::render::SUPERVISOR_ESTRATEGIA_REST_FOR_ONE,
),
(
RestartStrategy::SimpleOneForOne,
crate::render::SUPERVISOR_ESTRATEGIA_SIMPLE_ONE_FOR_ONE,
),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(
json,
format!("\"{expected}\""),
"RestartStrategy::{variant:?} must serialize to {expected:?}"
);
assert_eq!(
variant.as_str(),
expected,
"RestartStrategy::{variant:?}.as_str() must return the lifted \
SUPERVISOR_ESTRATEGIA_* constant"
);
}
}
#[test]
fn supervisor_estrategia_consts_are_pairwise_distinct() {
let all = [
crate::render::SUPERVISOR_ESTRATEGIA_ONE_FOR_ONE,
crate::render::SUPERVISOR_ESTRATEGIA_ONE_FOR_ALL,
crate::render::SUPERVISOR_ESTRATEGIA_REST_FOR_ONE,
crate::render::SUPERVISOR_ESTRATEGIA_SIMPLE_ONE_FOR_ONE,
];
for (i, a) in all.iter().enumerate() {
for (j, b) in all.iter().enumerate() {
if i != j {
assert_ne!(
a, b,
"SUPERVISOR_ESTRATEGIA_* consts must be pairwise distinct \
— got duplicate {a:?} at indices {i} and {j}",
);
}
}
}
}
#[test]
fn restart_strategy_display_routes_through_as_str_helper() {
for variant in [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
RestartStrategy::SimpleOneForOne,
] {
assert_eq!(
variant.to_string(),
variant.as_str(),
"RestartStrategy::{variant:?} Display must route through \
RestartStrategy::as_str (single source of truth: the lifted \
SUPERVISOR_ESTRATEGIA_* const the wire format also emits)"
);
}
}
#[test]
fn restart_strategy_display_matches_serialized_wire_byte_string() {
for variant in [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
RestartStrategy::SimpleOneForOne,
] {
let wire = serde_json::to_string(&variant).unwrap();
let unquoted = wire
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.expect("serialized RestartStrategy is a JSON string");
assert_eq!(
variant.to_string(),
unquoted,
"RestartStrategy::{variant:?} Display byte-string must match the \
Serialize derive's wire byte-string (three-path convergence: \
Display + as_str + Serialize all resolve to the same \
SUPERVISOR_ESTRATEGIA_* const)"
);
}
}
#[test]
fn restart_policy_variants_serialize_to_lifted_scalar_values() {
for (variant, expected) in [
(
RestartPolicy::Permanent,
crate::render::SUPERVISOR_CHILD_RESTART_PERMANENT,
),
(
RestartPolicy::Temporary,
crate::render::SUPERVISOR_CHILD_RESTART_TEMPORARY,
),
(
RestartPolicy::Transient,
crate::render::SUPERVISOR_CHILD_RESTART_TRANSIENT,
),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(
json,
format!("\"{expected}\""),
"RestartPolicy::{variant:?} must serialize to {expected:?}"
);
assert_eq!(
variant.as_str(),
expected,
"RestartPolicy::{variant:?}.as_str() must return the lifted \
SUPERVISOR_CHILD_RESTART_* constant"
);
}
}
#[test]
fn supervisor_child_restart_consts_are_pairwise_distinct() {
let all = [
crate::render::SUPERVISOR_CHILD_RESTART_PERMANENT,
crate::render::SUPERVISOR_CHILD_RESTART_TEMPORARY,
crate::render::SUPERVISOR_CHILD_RESTART_TRANSIENT,
];
for (i, a) in all.iter().enumerate() {
for (j, b) in all.iter().enumerate() {
if i != j {
assert_ne!(
a, b,
"SUPERVISOR_CHILD_RESTART_* consts must be pairwise distinct \
— got duplicate {a:?} at indices {i} and {j}",
);
}
}
}
}
#[test]
fn restart_policy_display_routes_through_as_str_helper() {
for variant in [
RestartPolicy::Permanent,
RestartPolicy::Temporary,
RestartPolicy::Transient,
] {
assert_eq!(
variant.to_string(),
variant.as_str(),
"RestartPolicy::{variant:?} Display must route through \
RestartPolicy::as_str (single source of truth: the lifted \
SUPERVISOR_CHILD_RESTART_* const the wire format also emits)"
);
}
}
#[test]
fn restart_policy_display_matches_serialized_wire_byte_string() {
for variant in [
RestartPolicy::Permanent,
RestartPolicy::Temporary,
RestartPolicy::Transient,
] {
let wire = serde_json::to_string(&variant).unwrap();
let unquoted = wire
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.expect("serialized RestartPolicy is a JSON string");
assert_eq!(
variant.to_string(),
unquoted,
"RestartPolicy::{variant:?} Display byte-string must match the \
Serialize derive's wire byte-string (three-path convergence: \
Display + as_str + Serialize all resolve to the same \
SUPERVISOR_CHILD_RESTART_* const)"
);
}
}
#[test]
fn child_spec_nome_returns_caixa_byte_equal_across_permutations() {
for name in [
"worker",
"cache-server",
"scratch-job",
"orders-v2",
"session-8080",
] {
let c = ChildSpec {
caixa: name.into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
assert_eq!(
c.nome(),
name,
"ChildSpec::nome must return :children :caixa verbatim \
(got {:?}, expected {name:?})",
c.nome(),
);
assert_eq!(
c.nome(),
c.caixa.as_str(),
"ChildSpec::nome must byte-equal the .caixa field access",
);
}
}
#[test]
fn child_spec_nome_borrows_from_caixa_storage() {
let c = ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
let name = c.nome();
let caixa_slice = c.caixa.as_str();
assert_eq!(
name.as_ptr(),
caixa_slice.as_ptr(),
"ChildSpec::nome must borrow from the .caixa String's backing \
storage — a fresh allocation here means the accessor no \
longer names the substrate-primitive typed dispatch and \
every downstream consumer would silently carry a detached \
copy",
);
assert_eq!(
name.len(),
caixa_slice.len(),
"ChildSpec::nome and .caixa.as_str() must byte-equal in length \
as well as in address",
);
}
#[test]
fn validate_gates_child_nome_through_lifted_accessor() {
for ok_name in ["a", "worker", "cache-server", "orders-v2", "svc-8080"] {
let s = SupervisorSpec {
children: vec![ChildSpec {
caixa: ok_name.into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
}],
..SupervisorSpec::default()
};
s.validate().unwrap_or_else(|e| {
panic!(
"SupervisorSpec::validate must accept :children :caixa {ok_name:?} \
(upstream DNS-1123 gate accepts it): got {e:?}",
);
});
let c = ChildSpec {
caixa: ok_name.into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
crate::render::require_valid_dns_1123_label(c.nome(), || (), |_reason| ())
.unwrap_or_else(|()| {
panic!(
"require_valid_dns_1123_label must accept the accessor-projected \
:children :caixa {ok_name:?}",
);
});
}
for bad_name in ["", "Worker", "my_worker", "team.worker", "-worker"] {
let s = SupervisorSpec {
children: vec![ChildSpec {
caixa: bad_name.into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
}],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::EmptyChildName | SupervisorError::ChildCaixaInvalid { .. }
),
"SupervisorSpec::validate must reject :children :caixa {bad_name:?} \
via the DNS-1123 gate: got {err:?}",
);
let c = ChildSpec {
caixa: bad_name.into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
assert!(
crate::render::require_valid_dns_1123_label(c.nome(), || (), |_reason| (),)
.is_err(),
"require_valid_dns_1123_label must reject the accessor-projected \
:children :caixa {bad_name:?}",
);
}
}
#[test]
fn child_spec_versao_requirement_returns_versao_byte_equal_across_permutations() {
for req in ["^0.1", "~0.1.2", "0.1.0", "*", "^1"] {
let c = ChildSpec {
caixa: "worker".into(),
versao: req.into(),
restart: RestartPolicy::Permanent,
};
assert_eq!(
c.versao_requirement(),
req,
"ChildSpec::versao_requirement must return :children :versao \
verbatim (got {:?}, expected {req:?})",
c.versao_requirement(),
);
assert_eq!(
c.versao_requirement(),
c.versao.as_str(),
"ChildSpec::versao_requirement must byte-equal the .versao \
field access",
);
}
}
#[test]
fn child_spec_versao_requirement_borrows_from_versao_storage() {
let c = ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
let req = c.versao_requirement();
let versao_slice = c.versao.as_str();
assert_eq!(
req.as_ptr(),
versao_slice.as_ptr(),
"ChildSpec::versao_requirement must borrow from the .versao \
String's backing storage — a fresh allocation here means the \
accessor no longer names the substrate-primitive typed \
dispatch and every downstream consumer would silently carry \
a detached copy",
);
assert_eq!(
req.len(),
versao_slice.len(),
"ChildSpec::versao_requirement and .versao.as_str() must \
byte-equal in length as well as in address",
);
}
#[test]
fn validate_gates_child_versao_through_lifted_accessor() {
for ok_req in ["^0.1", "~0.1.2", "0.1.0", "*", "^1"] {
let s = SupervisorSpec {
children: vec![ChildSpec {
caixa: "worker".into(),
versao: ok_req.into(),
restart: RestartPolicy::Permanent,
}],
..SupervisorSpec::default()
};
s.validate().unwrap_or_else(|e| {
panic!(
"SupervisorSpec::validate must accept :children :versao {ok_req:?} \
(upstream versao-requirement gate accepts it): got {e:?}",
);
});
let c = ChildSpec {
caixa: "worker".into(),
versao: ok_req.into(),
restart: RestartPolicy::Permanent,
};
crate::render::require_valid_versao_requirement(
c.versao_requirement(),
|| (),
|_reason| (),
)
.unwrap_or_else(|()| {
panic!(
"require_valid_versao_requirement must accept the accessor-projected \
:children :versao {ok_req:?}",
);
});
}
for bad_req in ["", "v0.1.0", "^bad-version", "^^0.1", "v0.1"] {
let s = SupervisorSpec {
children: vec![ChildSpec {
caixa: "worker".into(),
versao: bad_req.into(),
restart: RestartPolicy::Permanent,
}],
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
assert!(
matches!(
err,
SupervisorError::EmptyChildVersion { .. }
| SupervisorError::ChildVersaoInvalid { .. }
),
"SupervisorSpec::validate must reject :children :versao {bad_req:?} \
via the versao-requirement gate: got {err:?}",
);
let c = ChildSpec {
caixa: "worker".into(),
versao: bad_req.into(),
restart: RestartPolicy::Permanent,
};
assert!(
crate::render::require_valid_versao_requirement(
c.versao_requirement(),
|| (),
|_reason| (),
)
.is_err(),
"require_valid_versao_requirement must reject the accessor-projected \
:children :versao {bad_req:?}",
);
}
}
#[test]
fn child_spec_restart_returns_restart_verbatim_across_permutations() {
for restart in [
RestartPolicy::Permanent,
RestartPolicy::Transient,
RestartPolicy::Temporary,
] {
let c = ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart,
};
assert_eq!(
c.restart(),
restart,
"ChildSpec::restart must return :children :restart \
verbatim (got {:?}, expected {restart:?})",
c.restart(),
);
assert_eq!(
c.restart(),
c.restart,
"ChildSpec::restart accessor and .restart field access \
must byte-equal — the accessor is the substrate-primitive \
typed dispatch every downstream per-child restart-\
decision consumer must route through",
);
}
}
#[test]
fn supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations() {
for estrategia in [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
RestartStrategy::SimpleOneForOne,
] {
let children = if estrategia.is_simple_one_for_one() {
Vec::new()
} else {
vec![ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
}]
};
let s = SupervisorSpec {
estrategia,
children,
..SupervisorSpec::default()
};
assert_eq!(
s.estrategia(),
estrategia,
"SupervisorSpec::estrategia must return :supervisor :estrategia \
verbatim (got {:?}, expected {estrategia:?})",
s.estrategia(),
);
assert_eq!(
s.estrategia(),
s.estrategia,
"SupervisorSpec::estrategia accessor and .estrategia field \
access must byte-equal — the accessor is the substrate-\
primitive typed dispatch every downstream sibling-restart-\
strategy consumer must route through",
);
}
}
#[test]
fn validate_reads_through_lifted_estrategia_accessor() {
for estrategia in [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
] {
let s = SupervisorSpec {
estrategia,
children: Vec::new(),
..SupervisorSpec::default()
};
let err = s.validate().unwrap_err();
match err {
SupervisorError::NoChildren { estrategia: e } => {
assert_eq!(
e,
s.estrategia(),
"NoChildren.estrategia must byte-equal \
SupervisorSpec::estrategia() — the empty-`:children` \
refusal reads through the lifted accessor",
);
assert_eq!(
e, estrategia,
"NoChildren.estrategia must carry the author-declared \
:supervisor :estrategia variant verbatim (got {e:?}, \
expected {estrategia:?})",
);
}
other => panic!("expected NoChildren, got {other:?} for estrategia={estrategia:?}"),
}
}
}
#[test]
fn supervisor_spec_max_restarts_returns_max_restarts_u32_byte_equal_across_permutations() {
for max_restarts in [1u32, SUPERVISOR_MAX_RESTARTS_MAX, 0, u32::MAX] {
let s = SupervisorSpec {
max_restarts,
..SupervisorSpec::default()
};
assert_eq!(
s.max_restarts(),
max_restarts,
"SupervisorSpec::max_restarts must return :supervisor \
:max-restarts verbatim (got {}, expected {max_restarts})",
s.max_restarts(),
);
assert_eq!(
s.max_restarts(),
s.max_restarts,
"SupervisorSpec::max_restarts accessor and .max_restarts \
field access must byte-equal — the accessor is the \
substrate-primitive typed dispatch every downstream \
restart-budget-count consumer must route through",
);
}
}
#[test]
fn validate_max_restarts_zero_floor_and_cap_arms_route_through_accessor() {
let child = ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
let s = SupervisorSpec {
max_restarts: 0,
children: vec![child.clone()],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::ZeroMaxRestarts,
"validate must reject max_restarts == 0 with ZeroMaxRestarts \
— the accessor and the validate gate must route through the \
same substrate-primitive typed dispatch on the zero-floor arm",
);
let over_cap = SUPERVISOR_MAX_RESTARTS_MAX + 1;
let s = SupervisorSpec {
max_restarts: over_cap,
children: vec![child.clone()],
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::MaxRestartsExceedsCap { max_restarts } => {
assert_eq!(
max_restarts,
s.max_restarts(),
"MaxRestartsExceedsCap.max_restarts must byte-equal \
SupervisorSpec::max_restarts() — the cap-arm refusal \
reads through the lifted accessor",
);
assert_eq!(
max_restarts, over_cap,
"MaxRestartsExceedsCap.max_restarts must carry the \
author-declared :supervisor :max-restarts value \
verbatim (got {max_restarts}, expected {over_cap})",
);
}
other => panic!("expected MaxRestartsExceedsCap, got {other:?}"),
}
for max_restarts in [1u32, SUPERVISOR_MAX_RESTARTS_MAX] {
let s = SupervisorSpec {
max_restarts,
children: vec![child.clone()],
..SupervisorSpec::default()
};
assert!(
s.validate().is_ok(),
"validate must accept max_restarts == {max_restarts} \
(an accept-set boundary of \
1..=SUPERVISOR_MAX_RESTARTS_MAX)",
);
}
}
#[test]
fn supervisor_spec_restart_window_returns_option_duration_byte_equal_across_permutations() {
for restart_window in [
None,
Some(Duration::from_millis(1)),
Some(SUPERVISOR_RESTART_WINDOW_MAX),
Some(Duration::ZERO),
Some(Duration::MAX),
] {
let s = SupervisorSpec {
restart_window,
..SupervisorSpec::default()
};
assert_eq!(
s.restart_window(),
restart_window,
"SupervisorSpec::restart_window must return :supervisor \
:restart-window verbatim (got {:?}, expected {restart_window:?})",
s.restart_window(),
);
assert_eq!(
s.restart_window(),
s.restart_window,
"SupervisorSpec::restart_window accessor and \
.restart_window field access must byte-equal — the \
accessor is the substrate-primitive typed dispatch every \
downstream restart-intensity-denominator consumer must \
route through",
);
}
}
#[test]
fn validate_restart_window_bracket_arm_routes_through_accessor() {
let child = ChildSpec {
caixa: "worker".into(),
versao: "^0.1".into(),
restart: RestartPolicy::Permanent,
};
let s = SupervisorSpec {
restart_window: None,
children: vec![child.clone()],
..SupervisorSpec::default()
};
assert!(
s.validate().is_ok(),
"validate must accept restart_window: None (the never-reset \
sentinel) — the `if let Some(_)` bracket returns early on \
the None arm and the accessor must agree",
);
let s = SupervisorSpec {
restart_window: Some(Duration::ZERO),
children: vec![child.clone()],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::RestartWindowZero,
"validate must reject restart_window == Some(Duration::ZERO) \
with RestartWindowZero — the accessor and the validate gate \
must route through the same substrate-primitive typed \
dispatch on the zero-floor arm",
);
let sub_ms = Duration::from_micros(1500);
let s = SupervisorSpec {
restart_window: Some(sub_ms),
children: vec![child.clone()],
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::RestartWindowNotCanonical { window } => {
assert_eq!(
Some(window),
s.restart_window(),
"RestartWindowNotCanonical.window must byte-equal \
SupervisorSpec::restart_window().unwrap() — the \
non-canonical-arm refusal reads through the lifted \
accessor",
);
assert_eq!(
window, sub_ms,
"RestartWindowNotCanonical.window must carry the \
author-declared :supervisor :restart-window value \
verbatim (got {window:?}, expected {sub_ms:?})",
);
}
other => panic!("expected RestartWindowNotCanonical, got {other:?}"),
}
let over_cap = SUPERVISOR_RESTART_WINDOW_MAX + Duration::from_millis(1);
let s = SupervisorSpec {
restart_window: Some(over_cap),
children: vec![child.clone()],
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::RestartWindowExceedsCap { window } => {
assert_eq!(
Some(window),
s.restart_window(),
"RestartWindowExceedsCap.window must byte-equal \
SupervisorSpec::restart_window().unwrap() — the \
cap-arm refusal reads through the lifted accessor",
);
assert_eq!(
window, over_cap,
"RestartWindowExceedsCap.window must carry the \
author-declared :supervisor :restart-window value \
verbatim (got {window:?}, expected {over_cap:?})",
);
}
other => panic!("expected RestartWindowExceedsCap, got {other:?}"),
}
for restart_window in [Duration::from_millis(1), SUPERVISOR_RESTART_WINDOW_MAX] {
let s = SupervisorSpec {
restart_window: Some(restart_window),
children: vec![child.clone()],
..SupervisorSpec::default()
};
assert!(
s.validate().is_ok(),
"validate must accept restart_window == Some({restart_window:?}) \
(an accept-set boundary of \
1ms..=SUPERVISOR_RESTART_WINDOW_MAX)",
);
}
}
#[test]
fn supervisor_spec_restart_window_projects_option_duration_by_copy() {
for restart_window in [
None,
Some(Duration::from_millis(1)),
Some(Duration::from_secs(60)),
Some(SUPERVISOR_RESTART_WINDOW_MAX),
] {
let s = SupervisorSpec {
restart_window,
..SupervisorSpec::default()
};
let first = s.restart_window();
let second = s.restart_window();
assert_eq!(
first, second,
"SupervisorSpec::restart_window must be idempotent — two \
successive calls on the same &self must return the \
same Option<Duration>",
);
assert_eq!(
first, restart_window,
"SupervisorSpec::restart_window must return :supervisor \
:restart-window verbatim by copy — got {first:?}, \
expected {restart_window:?}",
);
}
}
#[test]
fn supervisor_spec_children_returns_children_slice_byte_equal_across_permutations() {
let fixtures: Vec<Vec<ChildSpec>> = vec![
Vec::new(),
vec![child("worker", "^0.1", RestartPolicy::Permanent)],
vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("cache-server", "^0.1", RestartPolicy::Transient),
],
vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("cache-server", "^0.1", RestartPolicy::Transient),
child("scratch-job", "^0.1", RestartPolicy::Temporary),
],
];
for children in fixtures {
let s = SupervisorSpec {
children: children.clone(),
..SupervisorSpec::default()
};
assert_eq!(
s.children(),
children.as_slice(),
"SupervisorSpec::children must return :supervisor \
:children verbatim (got {:?}, expected {:?})",
s.children(),
children.as_slice(),
);
assert_eq!(
s.children(),
s.children.as_slice(),
"SupervisorSpec::children accessor and \
.children.as_slice() field access must byte-equal — \
the accessor is the substrate-primitive typed \
dispatch every downstream static-child-list consumer \
must route through",
);
assert_eq!(
s.children().len(),
s.children.len(),
"SupervisorSpec::children().len() must byte-equal \
self.children.len() — a length-drift would silently \
split the paired partition-dispatch `.is_empty()` \
probe input from the per-child validate loop's \
traversal input",
);
}
}
#[test]
fn validate_reads_through_lifted_children_accessor() {
let s = SupervisorSpec {
estrategia: RestartStrategy::SimpleOneForOne,
children: vec![child("worker", "^0.1", RestartPolicy::Permanent)],
..SupervisorSpec::default()
};
assert_eq!(
s.validate().unwrap_err(),
SupervisorError::SimpleOneForOneWithStaticChildren,
"SimpleOneForOne + non-empty children must trip \
SimpleOneForOneWithStaticChildren — the accessor projects \
a non-empty slice, and the SimpleOneForOne-arm refusal \
probe reads through the lifted accessor",
);
assert!(
!s.children().is_empty(),
"the SimpleOneForOne-arm refusal input must be a non-empty \
slice per the accessor's projection",
);
for estrategia in [
RestartStrategy::OneForOne,
RestartStrategy::OneForAll,
RestartStrategy::RestForOne,
] {
let s = SupervisorSpec {
estrategia,
children: Vec::new(),
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::NoChildren { estrategia: e } => {
assert_eq!(
e, estrategia,
"NoChildren.estrategia must carry the author-\
declared :supervisor :estrategia variant \
verbatim (got {e:?}, expected {estrategia:?})",
);
}
other => panic!(
"expected NoChildren, got {other:?} for \
estrategia={estrategia:?}"
),
}
assert!(
s.children().is_empty(),
"the non-SimpleOneForOne-arm refusal input must be the \
empty slice per the accessor's projection",
);
}
let s = SupervisorSpec {
estrategia: RestartStrategy::OneForOne,
children: vec![
child("worker", "^0.1", RestartPolicy::Permanent),
child("worker", "^0.2", RestartPolicy::Transient),
],
..SupervisorSpec::default()
};
match s.validate().unwrap_err() {
SupervisorError::DuplicateChildCaixa { caixa } => {
assert_eq!(
caixa, "worker",
"DuplicateChildCaixa.caixa must carry the shared \
child `:caixa` name verbatim",
);
}
other => panic!("expected DuplicateChildCaixa, got {other:?}"),
}
assert_eq!(
s.children().len(),
2,
"the per-child validate loop's traversal input must be a \
two-element slice per the accessor's projection",
);
}
}