use rust_supervisor::id::types::ChildId;
use rust_supervisor::spec::child::{ChildSpec, TaskKind};
use rust_supervisor::spec::supervisor::{
ChildStrategyOverride, EscalationPolicy, GroupStrategy, RestartBudget, SupervisionStrategy,
SupervisorSpec,
};
use rust_supervisor::task::factory::{TaskResult, service_fn};
use rust_supervisor::tree::builder::SupervisorTree;
use rust_supervisor::tree::order::{
restart_execution_plan, restart_scope, shutdown_order, startup_order,
};
use std::sync::Arc;
use std::time::Duration;
#[test]
fn tree_preserves_declaration_and_reverse_shutdown_order() {
let first = child("first");
let second = child("second");
let spec = SupervisorSpec::root(vec![first.clone(), second.clone()]);
let tree = SupervisorTree::build(&spec).unwrap();
assert_eq!(startup_order(&tree)[0].child.id, first.id);
assert_eq!(shutdown_order(&tree)[0].child.id, second.id);
}
#[test]
fn rest_for_one_selects_failed_child_and_following_children() {
let first = child("first");
let second = child("second");
let spec = SupervisorSpec::root(vec![first, second.clone()]);
let tree = SupervisorTree::build(&spec).unwrap();
let scope = restart_scope(&tree, SupervisionStrategy::RestForOne, &second.id);
assert_eq!(scope, vec![second.id]);
}
#[test]
fn group_strategy_limits_restart_plan_to_group_members() {
let first = child("first");
let mut second = child("second");
let mut third = child("third");
let mut fourth = child("fourth");
second.tags.push("pipeline".to_owned());
third.tags.push("pipeline".to_owned());
fourth.tags.push("other".to_owned());
let mut spec = SupervisorSpec::root(vec![first, second.clone(), third.clone(), fourth]);
spec.group_strategies = vec![GroupStrategy::new(
"pipeline",
SupervisionStrategy::RestForOne,
)];
let tree = SupervisorTree::build(&spec).unwrap();
let plan = restart_execution_plan(&tree, &spec, &second.id);
assert_eq!(plan.group, Some("pipeline".to_owned()));
assert_eq!(plan.strategy, SupervisionStrategy::RestForOne);
assert_eq!(plan.scope, vec![second.id, third.id]);
}
#[test]
fn child_override_wins_over_group_strategy_and_selects_budget() {
let mut first = child("first");
let second = child("second");
first.tags.push("pipeline".to_owned());
let budget = RestartBudget::new(3, Duration::from_secs(10));
let mut override_strategy =
ChildStrategyOverride::new(first.id.clone(), SupervisionStrategy::OneForAll);
override_strategy.restart_budget = Some(budget);
override_strategy.escalation_policy = Some(EscalationPolicy::ShutdownTree);
let mut spec = SupervisorSpec::root(vec![first.clone(), second.clone()]);
spec.group_strategies = vec![GroupStrategy::new(
"pipeline",
SupervisionStrategy::OneForOne,
)];
spec.child_strategy_overrides = vec![override_strategy];
let tree = SupervisorTree::build(&spec).unwrap();
let plan = restart_execution_plan(&tree, &spec, &first.id);
assert_eq!(plan.group, None);
assert_eq!(plan.strategy, SupervisionStrategy::OneForAll);
assert_eq!(plan.scope, vec![first.id, second.id]);
assert_eq!(plan.restart_budget, Some(budget));
assert_eq!(plan.escalation_policy, Some(EscalationPolicy::ShutdownTree));
}
#[test]
fn validation_rejects_child_with_ambiguous_strategy_groups() {
let mut child = child("worker");
child.tags.push("alpha".to_owned());
child.tags.push("beta".to_owned());
let mut spec = SupervisorSpec::root(vec![child]);
spec.group_strategies = vec![
GroupStrategy::new("alpha", SupervisionStrategy::OneForOne),
GroupStrategy::new("beta", SupervisionStrategy::OneForAll),
];
let error = spec.validate().unwrap_err();
assert!(error.to_string().contains("ambiguous"));
}
#[test]
fn validation_rejects_unused_strategy_group() {
let child = child("worker");
let mut spec = SupervisorSpec::root(vec![child]);
spec.group_strategies = vec![GroupStrategy::new(
"missing",
SupervisionStrategy::OneForOne,
)];
let error = spec.validate().unwrap_err();
assert!(error.to_string().contains("unused group"));
}
#[test]
fn validation_rejects_unknown_child_override() {
let child = child("worker");
let mut spec = SupervisorSpec::root(vec![child]);
spec.child_strategy_overrides = vec![ChildStrategyOverride::new(
ChildId::new("missing"),
SupervisionStrategy::OneForOne,
)];
let error = spec.validate().unwrap_err();
assert!(error.to_string().contains("unknown child"));
}
#[test]
fn validation_rejects_invalid_restart_budget() {
let mut spec = SupervisorSpec::root(vec![child("worker")]);
spec.restart_budget = Some(RestartBudget::new(0, Duration::from_secs(1)));
let error = spec.validate().unwrap_err();
assert!(error.to_string().contains("max_restarts"));
}
fn child(id: &str) -> ChildSpec {
let factory = service_fn(|_ctx| async { TaskResult::Succeeded });
ChildSpec::worker(
ChildId::new(id),
id,
TaskKind::AsyncWorker,
Arc::new(factory),
)
}