Skip to main content

behavior_actors/lifecycle/
termination_propagation.rs

1//! Exact terminal-outcome propagation from a statically selected actor.
2
3use crate::{
4    ChildStopped, ObserveChild, ObservePeer, PeerStopped, ReportTerminalOutcome, TerminalOutcome,
5};
6use behavior::{
7    Actions, Address, Behavior, BehaviorActed, BirthMode, CreationId, EventLayer, Here,
8    InterpreterRequest, InterpreterRequests, Protocol, ReturnsToEmitter, SendEffects, SendLayer,
9};
10
11/// A statically selected source of one authoritative terminal report.
12///
13/// Implementations identify both the interpreter request and the exact report
14/// returned to this composition.  This keeps owned-child and known-peer
15/// selection concrete without a runtime target enum or topology lookup.
16pub trait TerminationTarget<A: Address>: Copy {
17    type Report;
18    type Request: InterpreterRequest<ReturnToEmitter = ReturnsToEmitter<Self::Report, Here>>;
19
20    fn request(self) -> Self::Request;
21    fn matches(self, report: &Self::Report) -> bool;
22    fn outcome(report: Self::Report) -> TerminalOutcome<A>;
23}
24
25/// Select one exact generation from the emitting actor's child namespace.
26pub struct ChildTermination<P: Protocol, Occurrence> {
27    pub child: CreationId,
28    protocol: core::marker::PhantomData<fn() -> P>,
29    occurrence: core::marker::PhantomData<fn() -> Occurrence>,
30}
31
32impl<P: Protocol, Occurrence> Copy for ChildTermination<P, Occurrence> {}
33
34impl<P: Protocol, Occurrence> Clone for ChildTermination<P, Occurrence> {
35    fn clone(&self) -> Self {
36        *self
37    }
38}
39
40impl<P: Protocol, Occurrence> PartialEq for ChildTermination<P, Occurrence> {
41    fn eq(&self, other: &Self) -> bool {
42        self.child == other.child
43    }
44}
45
46impl<P: Protocol, Occurrence> Eq for ChildTermination<P, Occurrence> {}
47
48impl<P: Protocol, Occurrence> core::fmt::Debug for ChildTermination<P, Occurrence> {
49    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50        formatter
51            .debug_struct("ChildTermination")
52            .field("child", &self.child)
53            .finish()
54    }
55}
56
57impl<P: Protocol, Occurrence> ChildTermination<P, Occurrence> {
58    #[must_use]
59    pub const fn new(child: CreationId) -> Self {
60        Self {
61            child,
62            protocol: core::marker::PhantomData,
63            occurrence: core::marker::PhantomData,
64        }
65    }
66}
67
68impl<P, Occurrence> TerminationTarget<P::Addr> for ChildTermination<P, Occurrence>
69where
70    P: Protocol,
71{
72    type Report = ChildStopped<P::Addr>;
73    type Request = ObserveChild<P, Occurrence>;
74
75    fn request(self) -> Self::Request {
76        ObserveChild::new(self.child)
77    }
78
79    fn matches(self, report: &Self::Report) -> bool {
80        report.child == self.child
81    }
82
83    fn outcome(report: Self::Report) -> TerminalOutcome<P::Addr> {
84        report.outcome
85    }
86}
87
88/// Select one exact incarnation at an established peer address.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct PeerTermination<A: Address> {
91    pub peer: A,
92}
93
94impl<A: Address> PeerTermination<A> {
95    #[must_use]
96    pub const fn new(peer: A) -> Self {
97        Self { peer }
98    }
99}
100
101impl<A> TerminationTarget<A> for PeerTermination<A>
102where
103    A: Address + Copy + Eq,
104{
105    type Report = PeerStopped<A>;
106    type Request = ObservePeer<A>;
107
108    fn request(self) -> Self::Request {
109        ObservePeer::new(self.peer)
110    }
111
112    fn matches(self, report: &Self::Report) -> bool {
113        report.peer == self.peer
114    }
115
116    fn outcome(report: Self::Report) -> TerminalOutcome<A> {
117        report.outcome
118    }
119}
120
121/// Explicit disposition of an accepted authoritative terminal report.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum TerminalDisposition {
124    /// The composition's documented policy consumes the report locally.
125    Discharge,
126    /// Publish the report unchanged as this actor's terminal outcome and stop.
127    Propagate,
128}
129
130/// Pure policy deciding whether one exact terminal outcome crosses this
131/// composition's terminal boundary.
132pub type TerminalPropagationPolicy<A> = fn(&TerminalOutcome<A>) -> TerminalDisposition;
133
134/// Propagate every terminal outcome.
135#[must_use]
136pub const fn propagate_all<A: Address>(_: &TerminalOutcome<A>) -> TerminalDisposition {
137    TerminalDisposition::Propagate
138}
139
140/// Propagate crashes and abnormal exits while deliberately discharging normal
141/// and collected termination.
142#[must_use]
143pub const fn propagate_abnormal<A: Address>(outcome: &TerminalOutcome<A>) -> TerminalDisposition {
144    match outcome {
145        Ok(crate::Exit::Normal | crate::Exit::Collected) => TerminalDisposition::Discharge,
146        Ok(crate::Exit::LinkDied(_) | crate::Exit::SupervisionFailed(_)) | Err(_) => {
147            TerminalDisposition::Propagate
148        }
149    }
150}
151
152/// Complete phase of one terminal-propagation definition.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum TerminalPropagationState {
155    Observing,
156    Discharged,
157    Propagated,
158}
159
160/// Exact rejection from terminal propagation.
161#[derive(thiserror::Error)]
162pub enum TerminationPropagationError<E, Report> {
163    /// The wrapped behavior rejected its own event.
164    #[error("wrapped behavior rejected its event")]
165    Inner(#[source] E),
166    /// A returned terminal report does not match the configured source or the
167    /// still-observing phase.
168    #[error("terminal report does not match the active propagation source")]
169    UnexpectedReport {
170        state: TerminalPropagationState,
171        report: Report,
172    },
173}
174
175impl<E: core::fmt::Debug, Report> core::fmt::Debug for TerminationPropagationError<E, Report> {
176    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
177        match self {
178            Self::Inner(error) => formatter.debug_tuple("Inner").field(error).finish(),
179            Self::UnexpectedReport { state, .. } => formatter
180                .debug_struct("UnexpectedReport")
181                .field("state", state)
182                .field("report", &"<retained>")
183                .finish(),
184        }
185    }
186}
187
188/// Named effects owned by [`PropagateTermination`].
189#[derive(behavior_macros::SendProduct)]
190pub struct TerminalPropagationSends<Observations, Reports> {
191    pub observations: Observations,
192    pub reports: Reports,
193}
194
195type PropagationSends<A, Request> = TerminalPropagationSends<
196    InterpreterRequests<Request>,
197    InterpreterRequests<ReportTerminalOutcome<A>>,
198>;
199
200/// Propagate a selected actor's exact terminal report through this actor's
201/// terminal boundary.
202///
203/// The target determines which authoritative observation is installed.  A
204/// matching report is accepted once.  The configured policy either discharges
205/// it locally or emits [`ReportTerminalOutcome`] unchanged and stops in the
206/// same [`Actions`] value.  Publication-before-stop is interpreter policy;
207/// the actor-model nucleus does not define lifecycle observation or failure
208/// escalation.
209///
210/// A child selection requires a concrete protocol and an opaque creation ID:
211///
212/// ```compile_fail
213/// struct Worker;
214/// impl behavior::Protocol for Worker {
215///     type Addr = behavior::MailAddr;
216///     type Msg = behavior::Never;
217/// }
218///
219/// let _ = behavior_actors::ChildTermination::<Worker, behavior::ChildHead>::new("not a creation ID");
220/// ```
221pub struct PropagateTermination<B: Behavior, Target> {
222    inner: B,
223    target: Target,
224    policy: TerminalPropagationPolicy<behavior::BehaviorAddr<B>>,
225    state: TerminalPropagationState,
226}
227
228type PropagationActions<B, Target> = Actions<
229    behavior::BehaviorAddr<B>,
230    <B as Behavior>::Ph,
231    SendLayer<
232        PropagationSends<
233            behavior::BehaviorAddr<B>,
234            <Target as TerminationTarget<behavior::BehaviorAddr<B>>>::Request,
235        >,
236        <B as Behavior>::Sends,
237    >,
238    <B as Behavior>::Birth,
239>;
240
241impl<B, Target> PropagateTermination<B, Target>
242where
243    B: Behavior,
244    Target: TerminationTarget<behavior::BehaviorAddr<B>>,
245{
246    #[must_use]
247    pub const fn new(
248        inner: B,
249        target: Target,
250        policy: TerminalPropagationPolicy<behavior::BehaviorAddr<B>>,
251    ) -> Self {
252        Self {
253            inner,
254            target,
255            policy,
256            state: TerminalPropagationState::Observing,
257        }
258    }
259
260    #[must_use]
261    pub const fn state(&self) -> TerminalPropagationState {
262        self.state
263    }
264
265    fn wrap(
266        actions: Actions<behavior::BehaviorAddr<B>, B::Ph, B::Sends, B::Birth>,
267        owned: PropagationSends<behavior::BehaviorAddr<B>, Target::Request>,
268    ) -> PropagationActions<B, Target> {
269        actions.map_sends(|inner| SendLayer::new(owned, inner))
270    }
271}
272
273impl<B, Target> behavior::BehaviorBase for PropagateTermination<B, Target>
274where
275    B: Behavior + behavior::BehaviorBase,
276    Target: TerminationTarget<behavior::BehaviorAddr<B>>,
277{
278    type Base = B::Base;
279
280    fn base(&self) -> &Self::Base {
281        self.inner.base()
282    }
283}
284
285impl<B, Target> crate::StashStatus for PropagateTermination<B, Target>
286where
287    B: Behavior + crate::StashStatus,
288    Target: TerminationTarget<behavior::BehaviorAddr<B>>,
289{
290    fn stashed_messages(&self) -> usize {
291        self.inner.stashed_messages()
292    }
293}
294
295impl<B, Target, A, Ph, Sends, Br> Behavior for PropagateTermination<B, Target>
296where
297    A: Address,
298    Sends: SendEffects + behavior::SendsFor<B::Event>,
299    Br: BirthMode,
300    B: Behavior<Ph = Ph, Sends = Sends, Birth = Br>,
301    B::Protocol: behavior::Protocol<Addr = A>,
302    Target: TerminationTarget<A>,
303    Target::Report: Send,
304    Target::Request: Send,
305{
306    type Protocol = B::Protocol;
307    type Event = EventLayer<Target::Report, B::Event>;
308    type Sends = SendLayer<PropagationSends<A, Target::Request>, Sends>;
309    type Ph = Ph;
310    type Error = TerminationPropagationError<B::Error, Target::Report>;
311    type Birth = Br;
312
313    fn init(&mut self, _: behavior::InitializationTurn) -> BehaviorActed<Self> {
314        let actions =
315            behavior::initialize(&mut self.inner).map_err(TerminationPropagationError::Inner)?;
316        let mut owned: PropagationSends<A, Target::Request> = TerminalPropagationSends::empty();
317        owned.observations.send(self.target.request());
318        Ok(Self::wrap(actions, owned))
319    }
320
321    fn transition(&mut self, _: behavior::ActiveTurn, event: Self::Event) -> BehaviorActed<Self> {
322        match event {
323            EventLayer::Owned(report)
324                if self.state == TerminalPropagationState::Observing
325                    && self.target.matches(&report) =>
326            {
327                let outcome = Target::outcome(report);
328                match (self.policy)(&outcome) {
329                    TerminalDisposition::Discharge => {
330                        self.state = TerminalPropagationState::Discharged;
331                        Ok(Actions::cont())
332                    }
333                    TerminalDisposition::Propagate => {
334                        self.state = TerminalPropagationState::Propagated;
335                        let mut owned: PropagationSends<A, Target::Request> =
336                            TerminalPropagationSends::empty();
337                        owned.reports.send(ReportTerminalOutcome::new(outcome));
338                        Ok(Actions::new(
339                            SendLayer::new(owned, Sends::empty()),
340                            behavior::Creations::empty(),
341                            behavior::Step::Stop(behavior::Stopped),
342                        ))
343                    }
344                }
345            }
346            EventLayer::Owned(report) => Err(TerminationPropagationError::UnexpectedReport {
347                state: self.state,
348                report,
349            }),
350            EventLayer::Inner(event) => behavior::delegate_transition(&mut self.inner, event)
351                .map(|actions| Self::wrap(actions, TerminalPropagationSends::empty()))
352                .map_err(TerminationPropagationError::Inner),
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use std::time::Instant;
360
361    use super::*;
362    use crate::{
363        Activate as _, Crash, Exit, RestartDenial, RestartReleaseFailure, SupervisionFailureReason,
364    };
365    use behavior::{AllocationRejection, CreationRejection};
366    use behavior::{
367        Births, CreateChild, CreationSequence, Creations, MailAddr, Never, NoBirths, NoSends, Step,
368        User,
369    };
370    use proptest::prelude::*;
371
372    struct Probe {
373        worker: CreationId,
374    }
375
376    #[derive(Debug, Eq, PartialEq)]
377    struct Worker;
378
379    impl behavior::Protocol for Worker {
380        type Addr = MailAddr;
381        type Msg = Never;
382    }
383
384    impl behavior::BehaviorBase for Worker {
385        type Base = Self;
386
387        fn base(&self) -> &Self::Base {
388            self
389        }
390    }
391
392    impl Behavior for Worker {
393        type Protocol = Self;
394        type Event = User<MailAddr, Never>;
395        type Sends = NoSends;
396        type Ph = Never;
397        type Error = Never;
398        type Birth = NoBirths;
399
400        fn transition(
401            &mut self,
402            _: behavior::ActiveTurn,
403            event: Self::Event,
404        ) -> BehaviorActed<Self> {
405            match event.message {}
406        }
407    }
408
409    impl behavior::Protocol for Probe {
410        type Addr = MailAddr;
411        type Msg = u8;
412    }
413
414    impl behavior::BehaviorBase for Probe {
415        type Base = Self;
416
417        fn base(&self) -> &Self::Base {
418            self
419        }
420    }
421
422    impl Behavior for Probe {
423        type Protocol = Self;
424        type Event = User<MailAddr, u8>;
425        type Sends = Vec<u8>;
426        type Ph = Never;
427        type Error = Never;
428        type Birth = Births<Worker>;
429
430        fn init(&mut self, _: behavior::InitializationTurn) -> BehaviorActed<Self> {
431            Ok(Actions::new(
432                vec![1],
433                Creations::one(CreateChild::birth(self.worker, Worker)),
434                Step::Continue,
435            ))
436        }
437
438        fn transition(
439            &mut self,
440            _: behavior::ActiveTurn,
441            event: Self::Event,
442        ) -> BehaviorActed<Self> {
443            Ok(Actions::send(vec![event.message]))
444        }
445    }
446
447    fn worker_creation() -> CreationId {
448        CreationSequence::new()
449            .issue()
450            .expect("the first worker creation ID exists")
451    }
452
453    fn child(
454        worker: CreationId,
455        policy: TerminalPropagationPolicy<MailAddr>,
456    ) -> PropagateTermination<Probe, ChildTermination<Worker, behavior::ChildHead>> {
457        PropagateTermination::new(Probe { worker }, ChildTermination::new(worker), policy)
458    }
459
460    #[test]
461    fn initialization_preserves_inner_effects_and_observes_the_exact_child() {
462        let worker = worker_creation();
463        let initialized = child(worker, propagate_all).initialize().unwrap();
464
465        assert_eq!(initialized.actions.sends.inner, [1]);
466        assert_eq!(
467            initialized.actions.creates,
468            Creations::one(CreateChild::birth(worker, Worker))
469        );
470        assert!(matches!(initialized.actions.become_, Step::Continue));
471        assert_eq!(
472            initialized.actions.sends.owned.observations.as_slice(),
473            [ObserveChild::new(worker)]
474        );
475        assert!(initialized.actions.sends.owned.reports.is_empty());
476    }
477
478    #[test]
479    fn every_terminal_variant_is_propagated_without_reclassification() {
480        let outcomes = [
481            Ok(Exit::Normal),
482            Ok(Exit::Collected),
483            Ok(Exit::LinkDied(MailAddr(4))),
484            Ok(Exit::SupervisionFailed(
485                SupervisionFailureReason::StableChildStopped,
486            )),
487            Ok(Exit::SupervisionFailed(
488                SupervisionFailureReason::RestartDenied(RestartDenial::BudgetExceeded {
489                    restarts_in_window: 2,
490                    replacements_requested: 3,
491                    maximum_restarts: 4,
492                }),
493            )),
494            Ok(Exit::SupervisionFailed(
495                SupervisionFailureReason::RestartDenied(RestartDenial::ReleaseRejected(
496                    RestartReleaseFailure::DurationOverflow,
497                )),
498            )),
499            Ok(Exit::SupervisionFailed(
500                SupervisionFailureReason::RestartDenied(RestartDenial::AttemptSequenceExhausted),
501            )),
502            Ok(Exit::SupervisionFailed(
503                SupervisionFailureReason::RestartDenied(RestartDenial::TimerGenerationExhausted),
504            )),
505            Ok(Exit::SupervisionFailed(
506                SupervisionFailureReason::RestartDenied(RestartDenial::TimerIdentityExhausted),
507            )),
508            Ok(Exit::SupervisionFailed(
509                SupervisionFailureReason::StableChildCreationRejected(
510                    CreationRejection::Allocation(AllocationRejection::Exhausted),
511                ),
512            )),
513            Ok(Exit::SupervisionFailed(
514                SupervisionFailureReason::StableChildCreationRejected(
515                    CreationRejection::Allocation(AllocationRejection::AddressAlreadyClaimed),
516                ),
517            )),
518            Ok(Exit::SupervisionFailed(
519                SupervisionFailureReason::StableChildCreationRejected(
520                    CreationRejection::InitializationFailed,
521                ),
522            )),
523            Ok(Exit::SupervisionFailed(
524                SupervisionFailureReason::StableChildCreationRejected(
525                    CreationRejection::EnvironmentFailed,
526                ),
527            )),
528            Ok(Exit::SupervisionFailed(
529                SupervisionFailureReason::WorkerCreationRejected(CreationRejection::Allocation(
530                    AllocationRejection::Exhausted,
531                )),
532            )),
533            Ok(Exit::SupervisionFailed(
534                SupervisionFailureReason::WorkerCreationRejected(CreationRejection::Allocation(
535                    AllocationRejection::AddressAlreadyClaimed,
536                )),
537            )),
538            Ok(Exit::SupervisionFailed(
539                SupervisionFailureReason::WorkerCreationRejected(
540                    CreationRejection::InitializationFailed,
541                ),
542            )),
543            Ok(Exit::SupervisionFailed(
544                SupervisionFailureReason::WorkerCreationRejected(
545                    CreationRejection::EnvironmentFailed,
546                ),
547            )),
548            Ok(Exit::SupervisionFailed(
549                SupervisionFailureReason::WorkerFactoryRejected,
550            )),
551            Err(Crash::Failed),
552            Err(Crash::EnvironmentFailed),
553            Err(Crash::CapabilityFailed),
554            Err(Crash::Panicked),
555            Err(Crash::Cancelled),
556        ];
557
558        for outcome in outcomes {
559            let worker = worker_creation();
560            let mut active = child(worker, propagate_all).initialize().unwrap().behavior;
561            let actions = active
562                .transition(EventLayer::Owned(ChildStopped::new(
563                    worker,
564                    outcome,
565                    Instant::now(),
566                )))
567                .unwrap();
568
569            assert_eq!(
570                actions.sends.owned.reports.as_slice(),
571                [ReportTerminalOutcome::new(outcome)]
572            );
573            assert!(actions.sends.owned.observations.is_empty());
574            assert!(actions.sends.inner.is_empty());
575            assert!(actions.creates.is_empty());
576            assert!(matches!(actions.become_, Step::Stop(_)));
577            assert_eq!(active.state(), TerminalPropagationState::Propagated);
578
579            let duplicate = ChildStopped::new(worker, outcome, Instant::now());
580            let rejected = active.transition(EventLayer::Owned(duplicate));
581            assert!(matches!(
582                rejected,
583                Err(TerminationPropagationError::UnexpectedReport {
584                    state: TerminalPropagationState::Propagated,
585                    report,
586                }) if report == duplicate
587            ));
588        }
589    }
590
591    #[test]
592    fn abnormal_policy_explicitly_discharges_normal_outcomes() {
593        for outcome in [Ok(Exit::Normal), Ok(Exit::Collected)] {
594            let worker = worker_creation();
595            let mut active = child(worker, propagate_abnormal)
596                .initialize()
597                .unwrap()
598                .behavior;
599            let actions = active
600                .transition(EventLayer::Owned(ChildStopped::new(
601                    worker,
602                    outcome,
603                    Instant::now(),
604                )))
605                .unwrap();
606
607            assert!(actions.sends.owned.reports.is_empty());
608            assert!(matches!(actions.become_, Step::Continue));
609            assert_eq!(active.state(), TerminalPropagationState::Discharged);
610        }
611    }
612
613    #[test]
614    fn unmatched_reports_are_returned_and_inner_events_preserve_inner_actions() {
615        let mut creations = CreationSequence::new();
616        let worker = creations.issue().expect("the worker creation ID exists");
617        let unrelated_worker = creations
618            .issue()
619            .expect("the unrelated worker creation ID exists");
620        let mut active = child(worker, propagate_all).initialize().unwrap().behavior;
621        let unrelated = ChildStopped::new(unrelated_worker, Err(Crash::Failed), Instant::now());
622        let rejected = active.transition(EventLayer::Owned(unrelated));
623        assert!(matches!(
624            rejected,
625            Err(TerminationPropagationError::UnexpectedReport {
626                state: TerminalPropagationState::Observing,
627                report,
628            }) if report == unrelated
629        ));
630        assert_eq!(active.state(), TerminalPropagationState::Observing);
631
632        let delegated = active
633            .transition(EventLayer::Inner(User::new(MailAddr(3), 6)))
634            .unwrap();
635        assert_eq!(delegated.sends.inner, [6]);
636        assert!(delegated.sends.owned.observations.is_empty());
637        assert!(delegated.sends.owned.reports.is_empty());
638    }
639
640    #[test]
641    fn peer_target_returns_foreign_report_before_propagating_selected_peer() {
642        let mut initialized = PropagateTermination::new(
643            Probe {
644                worker: worker_creation(),
645            },
646            PeerTermination::new(MailAddr(4)),
647            propagate_all,
648        )
649        .initialize()
650        .unwrap();
651        assert_eq!(
652            initialized.actions.sends.owned.observations.as_slice(),
653            [ObservePeer::new(MailAddr(4))]
654        );
655
656        let foreign = PeerStopped::new(MailAddr(5), Err(Crash::Failed));
657        let rejected = initialized
658            .behavior
659            .transition(EventLayer::Owned(foreign.clone()));
660        assert!(matches!(
661            rejected,
662            Err(TerminationPropagationError::UnexpectedReport {
663                state: TerminalPropagationState::Observing,
664                report,
665            }) if report == foreign
666        ));
667        assert_eq!(
668            initialized.behavior.state(),
669            TerminalPropagationState::Observing
670        );
671
672        let outcome = Err(Crash::Panicked);
673        let actions = initialized
674            .behavior
675            .transition(EventLayer::Owned(PeerStopped::new(MailAddr(4), outcome)))
676            .unwrap();
677        assert_eq!(
678            actions.sends.owned.reports.as_slice(),
679            [ReportTerminalOutcome::new(outcome)]
680        );
681        assert!(actions.sends.owned.observations.is_empty());
682        assert!(actions.sends.inner.is_empty());
683        assert!(actions.creates.is_empty());
684        assert!(matches!(actions.become_, behavior::Step::Stop(_)));
685        assert_eq!(
686            initialized.behavior.state(),
687            TerminalPropagationState::Propagated
688        );
689    }
690
691    proptest! {
692        #[test]
693        fn arbitrary_terminal_payload_is_conserved_once(
694            tag in 0_u8..23,
695            peer in any::<u64>(),
696            admitted in any::<usize>(),
697            requested in any::<usize>(),
698            maximum in any::<u32>(),
699        ) {
700            let outcome = match tag {
701                0 => Ok(Exit::Normal),
702                1 => Ok(Exit::Collected),
703                2 => Ok(Exit::LinkDied(MailAddr(peer))),
704                3 => Ok(Exit::SupervisionFailed(
705                    SupervisionFailureReason::StableChildStopped,
706                )),
707                4 => Ok(Exit::SupervisionFailed(
708                    SupervisionFailureReason::RestartDenied(RestartDenial::BudgetExceeded {
709                        restarts_in_window: admitted,
710                        replacements_requested: requested,
711                        maximum_restarts: maximum,
712                    }),
713                )),
714                5 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::RestartDenied(RestartDenial::ReleaseRejected(RestartReleaseFailure::DurationOverflow)))),
715                6 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::RestartDenied(RestartDenial::AttemptSequenceExhausted))),
716                7 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::RestartDenied(RestartDenial::TimerGenerationExhausted))),
717                8 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::RestartDenied(RestartDenial::TimerIdentityExhausted))),
718                9 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::StableChildCreationRejected(CreationRejection::Allocation(AllocationRejection::Exhausted)))),
719                10 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::StableChildCreationRejected(CreationRejection::Allocation(AllocationRejection::AddressAlreadyClaimed)))),
720                11 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::StableChildCreationRejected(CreationRejection::InitializationFailed))),
721                12 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::StableChildCreationRejected(CreationRejection::EnvironmentFailed))),
722                13 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::WorkerCreationRejected(CreationRejection::Allocation(AllocationRejection::Exhausted)))),
723                14 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::WorkerCreationRejected(CreationRejection::Allocation(AllocationRejection::AddressAlreadyClaimed)))),
724                15 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::WorkerCreationRejected(CreationRejection::InitializationFailed))),
725                16 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::WorkerCreationRejected(CreationRejection::EnvironmentFailed))),
726                17 => Ok(Exit::SupervisionFailed(SupervisionFailureReason::WorkerFactoryRejected)),
727                18 => Err(Crash::Failed),
728                19 => Err(Crash::EnvironmentFailed),
729                20 => Err(Crash::Panicked),
730                21 => Err(Crash::Cancelled),
731                _ => Err(Crash::CapabilityFailed),
732            };
733            let worker = worker_creation();
734            let mut active = child(worker, propagate_all).initialize().unwrap().behavior;
735            let first = active.transition(EventLayer::Owned(ChildStopped::new(
736                worker,
737                outcome,
738                Instant::now(),
739            ))).unwrap();
740            prop_assert_eq!(
741                first.sends.owned.reports.as_slice(),
742                [ReportTerminalOutcome::new(outcome)]
743            );
744            let duplicate = ChildStopped::new(
745                worker,
746                outcome,
747                Instant::now(),
748            );
749            let rejected = matches!(
750                active.transition(EventLayer::Owned(duplicate)),
751                Err(TerminationPropagationError::UnexpectedReport {
752                    state: TerminalPropagationState::Propagated,
753                    report,
754                }) if report == duplicate
755            );
756            prop_assert!(rejected);
757        }
758    }
759}