Skip to main content

asupersync/
actor.rs

1//! Actor abstraction for region-owned, message-driven concurrency.
2//!
3//! Actors in Asupersync are region-owned tasks that process messages from a
4//! bounded mailbox. They integrate with the runtime's structured concurrency
5//! model:
6//!
7//! - **Region-owned**: Actors are spawned within a region and cannot outlive it.
8//! - **Cancel-safe mailbox**: Messages use the two-phase reserve/send pattern.
9//! - **Lifecycle hooks**: `on_start` and `on_stop` for initialization and cleanup.
10//!
11//! # Example
12//!
13//! ```ignore
14//! struct Counter {
15//!     count: u64,
16//! }
17//!
18//! impl Actor for Counter {
19//!     type Message = u64;
20//!
21//!     async fn handle(&mut self, _cx: &Cx, msg: u64) {
22//!         self.count += msg;
23//!     }
24//! }
25//!
26//! // In a scope:
27//! let (handle, stored) = scope.spawn_actor(
28//!     &mut state, &cx, Counter { count: 0 }, 32,
29//! )?;
30//! state.store_spawned_task(handle.task_id(), stored);
31//!
32//! // Send messages:
33//! handle.send(&cx, 5).await?;
34//! handle.send(&cx, 10).await?;
35//!
36//! // Stop the actor:
37//! handle.stop();
38//! let result = (&mut handle).join(&cx).await?;
39//! assert_eq!(result.count, 15);
40//! ```
41
42use std::future::Future;
43use std::pin::Pin;
44use std::sync::Arc;
45use std::sync::atomic::{AtomicU8, Ordering};
46use std::time::Duration;
47
48use crate::channel::mpsc;
49use crate::channel::mpsc::SendError;
50use crate::cx::Cx;
51use crate::runtime::{JoinError, SpawnError};
52use crate::types::{CxInner, Outcome, RegionId, TaskId, Time};
53
54/// Unique identifier for an actor.
55///
56/// For now this is a thin wrapper around the actor task's `TaskId`, which already
57/// provides arena + generation semantics. Keeping a distinct type avoids mixing
58/// actor IDs with generic tasks at call sites.
59#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub struct ActorId(TaskId);
61
62impl ActorId {
63    /// Create an actor ID from a task ID.
64    #[must_use]
65    #[inline]
66    pub const fn from_task(task_id: TaskId) -> Self {
67        Self(task_id)
68    }
69
70    /// Returns the underlying task ID.
71    #[must_use]
72    #[inline]
73    pub const fn task_id(self) -> TaskId {
74        self.0
75    }
76}
77
78impl std::fmt::Debug for ActorId {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_tuple("ActorId").field(&self.0).finish()
81    }
82}
83
84impl std::fmt::Display for ActorId {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        // Preserve the compact, deterministic formatting of TaskId while keeping
87        // a distinct type at the API level.
88        write!(f, "{}", self.0)
89    }
90}
91
92/// Lifecycle state for an actor.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum ActorState {
95    /// Actor constructed but not yet started.
96    Created,
97    /// Actor is running and processing messages.
98    Running,
99    /// Actor is stopping (cancellation requested / mailbox closed).
100    Stopping,
101    /// Actor has stopped and will not process further messages.
102    Stopped,
103}
104
105#[derive(Debug)]
106struct ActorStateCell {
107    state: AtomicU8,
108}
109
110impl ActorStateCell {
111    #[inline]
112    fn new(state: ActorState) -> Self {
113        Self {
114            state: AtomicU8::new(Self::encode(state)),
115        }
116    }
117
118    #[inline]
119    fn load(&self) -> ActorState {
120        Self::decode(self.state.load(Ordering::Acquire))
121    }
122
123    #[inline]
124    fn store(&self, state: ActorState) {
125        self.state.store(Self::encode(state), Ordering::Release);
126    }
127
128    /// Atomically compare and swap state from `current` to `new`.
129    /// Returns true if the swap succeeded, false otherwise.
130    #[inline]
131    fn compare_and_swap(&self, current: ActorState, new: ActorState) -> bool {
132        self.state
133            .compare_exchange(
134                Self::encode(current),
135                Self::encode(new),
136                Ordering::AcqRel,
137                Ordering::Acquire,
138            )
139            .is_ok()
140    }
141
142    #[inline]
143    const fn encode(state: ActorState) -> u8 {
144        match state {
145            ActorState::Created => 0,
146            ActorState::Running => 1,
147            ActorState::Stopping => 2,
148            ActorState::Stopped => 3,
149        }
150    }
151
152    #[inline]
153    const fn decode(value: u8) -> ActorState {
154        match value {
155            0 => ActorState::Created,
156            1 => ActorState::Running,
157            2 => ActorState::Stopping,
158            _ => ActorState::Stopped,
159        }
160    }
161}
162
163/// Internal runtime state for an actor.
164///
165/// This is intentionally lightweight and non-opinionated; higher-level actor
166/// features (mailbox policies, supervision trees, etc.) can extend this.
167struct ActorCell<M> {
168    mailbox: mpsc::Receiver<M>,
169    state: Arc<ActorStateCell>,
170}
171
172/// A message-driven actor that processes messages from a bounded mailbox.
173///
174/// Actors are the unit of stateful, message-driven concurrency. Each actor:
175/// - Owns mutable state (`self`)
176/// - Receives messages sequentially (no data races)
177/// - Runs inside a region (structured lifetime)
178///
179/// # Cancel Safety
180///
181/// When an actor is cancelled (region close, explicit abort), the runtime:
182/// 1. Closes the mailbox (no new messages accepted)
183/// 2. Calls `on_stop` for cleanup
184/// 3. Returns the actor state to the caller via `ActorHandle::join`
185pub trait Actor: Send + 'static {
186    /// The type of messages this actor can receive.
187    type Message: Send + 'static;
188
189    /// Called once when the actor starts, before processing any messages.
190    ///
191    /// Use this for initialization that requires the capability context.
192    /// The default implementation does nothing.
193    fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
194        Box::pin(async {})
195    }
196
197    /// Handle a single message.
198    ///
199    /// This is called sequentially for each message in the mailbox.
200    /// The actor has exclusive access to its state during handling.
201    fn handle(
202        &mut self,
203        cx: &Cx,
204        msg: Self::Message,
205    ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
206
207    /// Called once when the actor is stopping, after the mailbox is drained.
208    ///
209    /// Use this for cleanup. The default implementation does nothing.
210    fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
211        Box::pin(async {})
212    }
213}
214
215/// Handle to a running actor, used to send messages and manage its lifecycle.
216///
217/// The handle owns:
218/// - A sender for the actor's mailbox
219/// - A task handle for join/abort operations
220///
221/// When the handle is dropped, the mailbox sender is dropped, which causes
222/// the actor loop to exit after processing remaining messages.
223#[derive(Debug)]
224pub struct ActorHandle<A: Actor> {
225    actor_id: ActorId,
226    sender: mpsc::Sender<A::Message>,
227    state: Arc<ActorStateCell>,
228    task_id: TaskId,
229    receiver: crate::channel::oneshot::Receiver<Result<A, JoinError>>,
230    inner: std::sync::Weak<parking_lot::RwLock<CxInner>>,
231    completed: bool,
232}
233
234impl<A: Actor> ActorHandle<A> {
235    /// Send a message to the actor using two-phase reserve/send.
236    ///
237    /// Returns an error if the actor has stopped or the mailbox is full.
238    pub async fn send(&self, cx: &Cx, msg: A::Message) -> Outcome<(), SendError<A::Message>> {
239        match self.sender.send(cx, msg).await {
240            Ok(()) => Outcome::ok(()),
241            Err(e) => Outcome::err(e),
242        }
243    }
244
245    /// Try to send a message without blocking.
246    ///
247    /// Returns `Err(SendError::Full(msg))` if the mailbox is full, or
248    /// `Err(SendError::Disconnected(msg))` if the actor has stopped.
249    pub fn try_send(&self, msg: A::Message) -> Result<(), SendError<A::Message>> {
250        self.sender.try_send(msg)
251    }
252
253    /// Returns a lightweight, clonable reference for sending messages.
254    #[must_use]
255    pub fn sender(&self) -> ActorRef<A::Message> {
256        ActorRef {
257            actor_id: self.actor_id,
258            sender: self.sender.clone(),
259            state: Arc::clone(&self.state),
260        }
261    }
262
263    /// Returns the actor's unique identifier.
264    #[must_use]
265    pub const fn actor_id(&self) -> ActorId {
266        self.actor_id
267    }
268
269    /// Returns the task ID of the actor's underlying task.
270    #[must_use]
271    pub fn task_id(&self) -> crate::types::TaskId {
272        self.task_id
273    }
274
275    /// Signals the actor to stop gracefully.
276    ///
277    /// Sets the actor state to `Stopping`. The actor will continue processing
278    /// any currently buffered messages in its mailbox. Once the mailbox is
279    /// empty, the actor loop will exit and call `on_stop` before returning.
280    ///
281    /// Unlike [`abort`](Self::abort), this does NOT immediately request
282    /// cancellation, allowing the actor to drain pending work. The mailbox is
283    /// sealed immediately so new sends fail fast instead of extending shutdown.
284    pub fn stop(&self) {
285        self.state.store(ActorState::Stopping);
286        self.sender.close_receiver();
287    }
288
289    /// Returns true if the actor has finished.
290    #[must_use]
291    pub fn is_finished(&self) -> bool {
292        self.completed || self.receiver.is_ready() || self.receiver.is_closed()
293    }
294
295    /// Wait for the actor to finish and return its final state.
296    ///
297    /// Blocks until the actor loop completes (mailbox closed or cancelled),
298    /// then returns the actor's final state or a join error.
299    pub fn join<'a>(&'a mut self, _cx: &'a Cx) -> ActorJoinFuture<'a, A> {
300        let cx_inner = self.inner.clone();
301        let receiver = &mut self.receiver;
302        let terminal_state = &mut self.completed;
303        ActorJoinFuture {
304            inner: receiver.recv_uninterruptible(),
305            cx_inner,
306            sender: self.sender.clone(),
307            state: Arc::clone(&self.state),
308            terminal_state,
309            drop_abort_defused: false,
310        }
311    }
312
313    /// Request the actor to stop immediately by aborting its task.
314    ///
315    /// Sets `cancel_requested` on the actor's context, causing the actor loop
316    /// to exit at the next cancellation check point. The actor will call
317    /// `on_stop` before returning.
318    pub fn abort(&self) {
319        self.state.store(ActorState::Stopping);
320        self.sender.close_receiver();
321        if let Some(inner) = self.inner.upgrade() {
322            let cancel_wakers = {
323                let mut guard = inner.write();
324                guard.cancel_requested = true;
325                guard
326                    .fast_cancel
327                    .store(true, std::sync::atomic::Ordering::Release);
328                if guard.cancel_reason.is_none() {
329                    guard.cancel_reason = Some(crate::types::CancelReason::user("actor aborted"));
330                }
331                guard.cancel_waker_snapshot()
332            };
333            for waker in cancel_wakers {
334                waker.wake_by_ref();
335            }
336        }
337    }
338}
339
340/// Future returned by [`ActorHandle::join`].
341///
342/// This future aborts the actor if dropped before completion, ensuring correct
343/// cleanup in races and timeouts.
344pub struct ActorJoinFuture<'a, A: Actor> {
345    inner: crate::channel::oneshot::RecvUninterruptibleFuture<'a, Result<A, JoinError>>,
346    cx_inner: std::sync::Weak<parking_lot::RwLock<CxInner>>,
347    sender: mpsc::Sender<A::Message>,
348    state: Arc<ActorStateCell>,
349    terminal_state: &'a mut bool,
350    drop_abort_defused: bool,
351}
352
353impl<A: Actor> ActorJoinFuture<'_, A> {
354    fn closed_reason(&self) -> crate::types::CancelReason {
355        self.cx_inner
356            .upgrade()
357            .and_then(|inner| inner.read().cancel_reason.clone())
358            .unwrap_or_else(|| crate::types::CancelReason::user("join channel closed"))
359    }
360
361    fn abort(&self) {
362        self.state.store(ActorState::Stopping);
363        self.sender.close_receiver();
364        if let Some(inner) = self.cx_inner.upgrade() {
365            let cancel_wakers = {
366                let mut guard = inner.write();
367                guard.cancel_requested = true;
368                guard
369                    .fast_cancel
370                    .store(true, std::sync::atomic::Ordering::Release);
371                if guard.cancel_reason.is_none() {
372                    guard.cancel_reason = Some(crate::types::CancelReason::user("actor aborted"));
373                }
374                guard.cancel_waker_snapshot()
375            };
376            for waker in cancel_wakers {
377                waker.wake_by_ref();
378            }
379        }
380    }
381}
382
383impl<A: Actor> std::future::Future for ActorJoinFuture<'_, A> {
384    type Output = Result<A, JoinError>;
385
386    fn poll(
387        mut self: Pin<&mut Self>,
388        cx: &mut std::task::Context<'_>,
389    ) -> std::task::Poll<Self::Output> {
390        let this = &mut *self;
391        if *this.terminal_state {
392            return std::task::Poll::Ready(Err(JoinError::PolledAfterCompletion));
393        }
394
395        match Pin::new(&mut this.inner).poll(cx) {
396            std::task::Poll::Ready(Ok(res)) => {
397                *this.terminal_state = true;
398                this.drop_abort_defused = true;
399                std::task::Poll::Ready(res)
400            }
401            std::task::Poll::Ready(Err(crate::channel::oneshot::RecvError::Closed)) => {
402                *this.terminal_state = true;
403                this.drop_abort_defused = true;
404                let reason = this.closed_reason();
405                std::task::Poll::Ready(Err(JoinError::Cancelled(reason)))
406            }
407            std::task::Poll::Ready(Err(crate::channel::oneshot::RecvError::Cancelled)) => {
408                unreachable!(
409                    "RecvUninterruptibleFuture does not consult Cx cancellation and only resolves \
410                     to Ok(value), Closed, or PolledAfterCompletion"
411                );
412            }
413            std::task::Poll::Ready(Err(
414                crate::channel::oneshot::RecvError::PolledAfterCompletion,
415            )) => {
416                unreachable!(
417                    "ActorJoinFuture sets terminal_state before returning Ready, so a repoll \
418                     fails closed before the inner oneshot future can be polled again"
419                )
420            }
421            std::task::Poll::Pending => std::task::Poll::Pending,
422        }
423    }
424}
425
426impl<A: Actor> Drop for ActorJoinFuture<'_, A> {
427    fn drop(&mut self) {
428        if !*self.terminal_state && !self.drop_abort_defused {
429            if self.inner.receiver_finished() {
430                return;
431            }
432            self.abort();
433        }
434    }
435}
436
437/// A lightweight, clonable reference to an actor's mailbox.
438///
439/// Use this to send messages to an actor from multiple locations without
440/// needing to share the `ActorHandle`.
441#[derive(Debug)]
442pub struct ActorRef<M> {
443    actor_id: ActorId,
444    sender: mpsc::Sender<M>,
445    state: Arc<ActorStateCell>,
446}
447
448// Manual Clone impl without requiring M: Clone, since all fields are
449// independently clonable (ActorId is Copy, Sender<M> clones without M: Clone,
450// and Arc is always Clone).
451impl<M> Clone for ActorRef<M> {
452    fn clone(&self) -> Self {
453        Self {
454            actor_id: self.actor_id,
455            sender: self.sender.clone(),
456            state: Arc::clone(&self.state),
457        }
458    }
459}
460
461impl<M: Send + 'static> ActorRef<M> {
462    /// Send a message to the actor.
463    pub async fn send(&self, cx: &Cx, msg: M) -> Outcome<(), SendError<M>> {
464        match self.sender.send(cx, msg).await {
465            Ok(()) => Outcome::ok(()),
466            Err(e) => Outcome::err(e),
467        }
468    }
469
470    /// Reserve a slot in the mailbox (two-phase send: reserve -> commit).
471    #[must_use]
472    pub fn reserve<'a>(&'a self, cx: &'a Cx) -> mpsc::Reserve<'a, M> {
473        self.sender.reserve(cx)
474    }
475
476    /// Try to send a message without blocking.
477    pub fn try_send(&self, msg: M) -> Result<(), SendError<M>> {
478        self.sender.try_send(msg)
479    }
480
481    /// Returns true if the actor has stopped (mailbox closed).
482    #[must_use]
483    pub fn is_closed(&self) -> bool {
484        self.sender.is_closed()
485    }
486
487    /// Returns true if the actor is still alive (not fully stopped).
488    ///
489    /// Note: This is best-effort. The definitive shutdown signal is `ActorHandle::join()`.
490    #[must_use]
491    pub fn is_alive(&self) -> bool {
492        self.state.load() != ActorState::Stopped
493    }
494
495    /// Returns the actor's unique identifier.
496    #[must_use]
497    pub const fn actor_id(&self) -> ActorId {
498        self.actor_id
499    }
500}
501
502// ============================================================================
503// ActorContext: Actor-Specific Capability Extension
504// ============================================================================
505
506/// Configuration for actor mailbox.
507#[derive(Debug, Clone, Copy)]
508pub struct MailboxConfig {
509    /// Maximum number of messages the mailbox can hold.
510    pub capacity: usize,
511    /// Whether to use backpressure (block senders) or drop oldest messages.
512    pub backpressure: bool,
513}
514
515impl Default for MailboxConfig {
516    fn default() -> Self {
517        Self {
518            capacity: DEFAULT_MAILBOX_CAPACITY,
519            backpressure: true,
520        }
521    }
522}
523
524impl MailboxConfig {
525    /// Create a mailbox config with the specified capacity.
526    #[must_use]
527    pub const fn with_capacity(capacity: usize) -> Self {
528        Self {
529            capacity,
530            backpressure: true,
531        }
532    }
533}
534
535/// Messages that can be sent to a supervisor about child lifecycle events.
536#[derive(Debug, Clone)]
537pub enum SupervisorMessage {
538    /// A supervised child actor has failed.
539    ChildFailed {
540        /// The ID of the failed child.
541        child_id: ActorId,
542        /// Description of the failure.
543        reason: String,
544    },
545    /// A supervised child actor has stopped normally.
546    ChildStopped {
547        /// The ID of the stopped child.
548        child_id: ActorId,
549    },
550}
551
552/// Actor-specific capability context extending [`Cx`].
553///
554/// Provides actors with access to:
555/// - Self-reference for tell() patterns
556/// - Child management for supervision
557/// - Self-termination controls
558/// - Parent reference for escalation
559///
560/// All [`Cx`] methods are available through [`Deref`].
561///
562/// # Example
563///
564/// ```ignore
565/// async fn handle(&mut self, ctx: &ActorContext<'_, MyMessage>, msg: MyMessage) {
566///     // Access Cx methods directly
567///     if ctx.is_cancel_requested() {
568///         return;
569///     }
570///
571///     // Use actor-specific capabilities
572///     let my_id = ctx.self_actor_id();
573///     ctx.trace("handling message");
574/// }
575/// ```
576pub struct ActorContext<'a, M: Send + 'static> {
577    /// Underlying capability context.
578    cx: &'a Cx,
579    /// Reference to this actor's mailbox sender.
580    self_ref: ActorRef<M>,
581    /// This actor's unique identifier.
582    actor_id: ActorId,
583    /// Parent supervisor reference (None for root actors).
584    parent: Option<ActorRef<SupervisorMessage>>,
585    /// IDs of children currently supervised by this actor.
586    children: Vec<ActorId>,
587    /// Whether this actor has been requested to stop.
588    stopping: bool,
589}
590
591#[allow(clippy::elidable_lifetime_names)]
592impl<'a, M: Send + 'static> ActorContext<'a, M> {
593    /// Create a new actor context.
594    ///
595    /// This is typically called internally by the actor runtime.
596    #[must_use]
597    pub fn new(
598        cx: &'a Cx,
599        self_ref: ActorRef<M>,
600        actor_id: ActorId,
601        parent: Option<ActorRef<SupervisorMessage>>,
602    ) -> Self {
603        Self {
604            cx,
605            self_ref,
606            actor_id,
607            parent,
608            children: Vec::new(),
609            stopping: false,
610        }
611    }
612
613    /// Returns this actor's unique identifier.
614    ///
615    /// Unlike `self_ref()`, this avoids cloning the actor reference and is
616    /// useful for logging, debugging, or identity comparisons.
617    #[must_use]
618    pub const fn self_actor_id(&self) -> ActorId {
619        self.actor_id
620    }
621
622    /// Returns the underlying actor ID (alias for `self_actor_id`).
623    #[must_use]
624    pub const fn actor_id(&self) -> ActorId {
625        self.actor_id
626    }
627
628    // ========================================================================
629    // Child Management Methods
630    // ========================================================================
631
632    /// Register a child actor as supervised by this actor.
633    ///
634    /// Called internally when spawning supervised children.
635    pub fn register_child(&mut self, child_id: ActorId) {
636        self.children.push(child_id);
637    }
638
639    /// Unregister a child actor (after it has stopped).
640    ///
641    /// Returns true if the child was found and removed.
642    pub fn unregister_child(&mut self, child_id: ActorId) -> bool {
643        if let Some(pos) = self.children.iter().position(|&id| id == child_id) {
644            self.children.swap_remove(pos);
645            true
646        } else {
647            false
648        }
649    }
650
651    /// Returns the list of currently supervised child actor IDs.
652    #[must_use]
653    pub fn children(&self) -> &[ActorId] {
654        &self.children
655    }
656
657    /// Returns true if this actor has any supervised children.
658    #[must_use]
659    pub fn has_children(&self) -> bool {
660        !self.children.is_empty()
661    }
662
663    /// Returns the number of supervised children.
664    #[must_use]
665    pub fn child_count(&self) -> usize {
666        self.children.len()
667    }
668
669    // ========================================================================
670    // Self-Termination Methods
671    // ========================================================================
672
673    /// Request this actor to stop gracefully.
674    ///
675    /// Sets the stopping flag. The actor loop will exit after the current
676    /// message is processed and the mailbox is drained.
677    pub fn stop_self(&mut self) {
678        self.stopping = true;
679    }
680
681    /// Returns true if this actor has been requested to stop.
682    #[must_use]
683    pub fn is_stopping(&self) -> bool {
684        self.stopping
685    }
686
687    // ========================================================================
688    // Parent Interaction Methods
689    // ========================================================================
690
691    /// Returns a reference to the parent supervisor, if any.
692    ///
693    /// Root actors spawned without supervision return `None`.
694    #[must_use]
695    pub fn parent(&self) -> Option<&ActorRef<SupervisorMessage>> {
696        self.parent.as_ref()
697    }
698
699    /// Returns true if this actor has a parent supervisor.
700    #[must_use]
701    pub fn has_parent(&self) -> bool {
702        self.parent.is_some()
703    }
704
705    /// Escalate an error to the parent supervisor.
706    ///
707    /// Sends a `SupervisorMessage::ChildFailed` to the parent if one exists.
708    /// Does nothing if this is a root actor.
709    pub async fn escalate(&self, reason: String) {
710        if let Some(parent) = &self.parent {
711            let msg = SupervisorMessage::ChildFailed {
712                child_id: self.actor_id,
713                reason,
714            };
715            // Best-effort: ignore send failures (parent may have stopped)
716            let _ = parent.send(self.cx, msg).await;
717        }
718    }
719
720    // ========================================================================
721    // Cx Delegation Methods
722    // ========================================================================
723
724    /// Check for cancellation and return early if requested.
725    ///
726    /// This is a convenience method that checks both actor stopping
727    /// and Cx cancellation.
728    #[allow(clippy::result_large_err)]
729    pub fn checkpoint(&self) -> Result<(), crate::error::Error> {
730        if self.stopping {
731            let reason = crate::types::CancelReason::user("actor stopping")
732                .with_region(self.cx.region_id())
733                .with_task(self.cx.task_id());
734            return Err(crate::error::Error::cancelled(&reason));
735        }
736        self.cx.checkpoint()
737    }
738
739    /// Returns true if cancellation has been requested.
740    ///
741    /// Checks both actor stopping flag and Cx cancellation.
742    #[must_use]
743    pub fn is_cancel_requested(&self) -> bool {
744        self.stopping || self.cx.checkpoint().is_err()
745    }
746
747    /// Returns the current budget.
748    #[must_use]
749    pub fn budget(&self) -> crate::types::Budget {
750        self.cx.budget()
751    }
752
753    /// Returns the deadline from the budget, if set.
754    #[must_use]
755    pub fn deadline(&self) -> Option<Time> {
756        self.cx.budget().deadline
757    }
758
759    /// Emit a trace event.
760    pub fn trace(&self, event: &str) {
761        self.cx.trace(event);
762    }
763
764    /// Returns a clonable reference to this actor's mailbox.
765    ///
766    /// Use this to give other actors a way to send messages to this actor.
767    /// The `ActorRef<M>` type is always Clone regardless of whether M is Clone.
768    #[must_use]
769    pub fn self_ref(&self) -> ActorRef<M> {
770        self.self_ref.clone()
771    }
772
773    /// Returns a reference to the underlying Cx.
774    #[must_use]
775    pub const fn cx(&self) -> &Cx {
776        self.cx
777    }
778}
779
780impl<M: Send + 'static> std::ops::Deref for ActorContext<'_, M> {
781    type Target = Cx;
782
783    fn deref(&self) -> &Self::Target {
784        self.cx
785    }
786}
787
788impl<M: Send + 'static> std::fmt::Debug for ActorContext<'_, M> {
789    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
790        f.debug_struct("ActorContext")
791            .field("actor_id", &self.actor_id)
792            .field("children", &self.children.len())
793            .field("stopping", &self.stopping)
794            .field("has_parent", &self.parent.is_some())
795            .finish()
796    }
797}
798
799/// The default mailbox capacity for actors.
800pub const DEFAULT_MAILBOX_CAPACITY: usize = 64;
801
802struct OnStopMaskGuard(Arc<parking_lot::RwLock<CxInner>>);
803
804impl Drop for OnStopMaskGuard {
805    fn drop(&mut self) {
806        let mut g = self.0.write();
807        g.mask_depth = g.mask_depth.saturating_sub(1);
808    }
809}
810
811/// Internal: runs the actor message loop.
812///
813/// This function is the core of the actor runtime. It:
814/// 1. Calls `on_start`
815/// 2. Receives and handles messages until the mailbox is closed or cancelled
816/// 3. Drains remaining buffered messages (no silent drops)
817/// 4. Calls `on_stop`
818/// 5. Returns the actor state
819async fn run_actor_loop<A: Actor>(mut actor: A, cx: Cx, cell: &mut ActorCell<A::Message>) -> A {
820    use crate::tracing_compat::debug;
821
822    // Only transition to Running if stop() wasn't called before the actor started.
823    // stop() sets Stopping before scheduling; we must honour that signal so the
824    // poll_fn guard in the message loop can detect the pre-stop and break.
825    // Use compare_and_swap to avoid TOCTOU race between load() and store().
826    cell.state
827        .compare_and_swap(ActorState::Created, ActorState::Running);
828
829    // Phase 1: Initialization
830    // We always run on_start, even if cancelled or pre-stopped, because
831    // it serves as the actor's initial setup and matches the expectation
832    // that lifecycle hooks are symmetrically executed.
833    cx.trace("actor::on_start");
834    actor.on_start(&cx).await;
835
836    // Phase 2: Message loop with fairness yielding
837    // br-asupersync-foa8ir: Add periodic yielding to prevent mailbox starvation
838    let mut messages_processed = 0u32;
839    const YIELD_INTERVAL: u32 = 8; // Yield every 8 messages for fairness
840
841    loop {
842        // Check for cancellation
843        if cx.checkpoint().is_err() {
844            cx.trace("actor::cancel_requested");
845            break;
846        }
847
848        let recv_result = std::future::poll_fn(|task_cx| {
849            match cell.mailbox.poll_recv(&cx, task_cx) {
850                std::task::Poll::Pending if cell.state.load() == ActorState::Stopping => {
851                    // Graceful stop requested and mailbox is empty. Break the loop.
852                    std::task::Poll::Ready(Err(crate::channel::mpsc::RecvError::Disconnected))
853                }
854                other => other,
855            }
856        })
857        .await;
858
859        match recv_result {
860            Ok(msg) => {
861                actor.handle(&cx, msg).await;
862
863                // Yield periodically to maintain fairness with other tasks
864                messages_processed += 1;
865                if messages_processed >= YIELD_INTERVAL {
866                    messages_processed = 0;
867                    // Use budget consumption check as yield mechanism - if budget is consumed,
868                    // this will cause the scheduler to potentially switch to other tasks
869                    if cx.budget().poll_quota == 0 {
870                        cx.trace("actor::yield_on_budget_exhaustion");
871                        // Let the next checkpoint handle budget exhaustion
872                    }
873                }
874            }
875            Err(crate::channel::mpsc::RecvError::Disconnected) => {
876                // All senders dropped - graceful shutdown
877                cx.trace("actor::mailbox_disconnected");
878                break;
879            }
880            Err(crate::channel::mpsc::RecvError::Cancelled) => {
881                // Cancellation requested
882                cx.trace("actor::recv_cancelled");
883                break;
884            }
885            Err(crate::channel::mpsc::RecvError::Empty) => {
886                // Shouldn't happen with recv() (only try_recv), but handle gracefully
887                break;
888            }
889        }
890    }
891
892    cell.state.store(ActorState::Stopping);
893
894    let is_aborted = cx.checkpoint().is_err();
895
896    // Phase 3: Drain remaining buffered messages.
897    // Two-phase mailbox guarantee: no message silently dropped (unless aborted).
898    // We seal the mailbox to prevent any new reservations or commits, then
899    // process remaining messages if gracefully stopped. If aborted, we just
900    // empty the mailbox to drop the messages.
901    cell.mailbox.close();
902
903    if is_aborted {
904        while let Ok(_msg) = cell.mailbox.try_recv() {}
905    } else {
906        let mut drained: u64 = 0;
907        let mut drain_yield_counter = 0u32;
908        while let Ok(msg) = cell.mailbox.try_recv() {
909            actor.handle(&cx, msg).await;
910            drained += 1;
911
912            // br-asupersync-foa8ir: Yield during drain to prevent starvation
913            drain_yield_counter += 1;
914            if drain_yield_counter >= YIELD_INTERVAL {
915                drain_yield_counter = 0;
916                if cx.budget().poll_quota == 0 {
917                    cx.trace("actor::yield_during_drain");
918                }
919            }
920        }
921        if drained > 0 {
922            debug!(drained = drained, "actor::mailbox_drained");
923            cx.trace("actor::mailbox_drained");
924        }
925    }
926
927    // Phase 4: Cleanup — mask cancellation so on_stop runs to completion.
928    // Without masking, an aborted actor's on_stop could observe a stale
929    // cancel_requested=true and bail early via cx.checkpoint().
930
931    cx.trace("actor::on_stop");
932    let inner = cx.inner.clone();
933    {
934        let mut guard = inner.write();
935        // Enforce mask depth cap to prevent overflow and infinite recursion
936        // This maintains INV-MASK-BOUNDED invariant in both debug and release builds
937        assert!(
938            guard.mask_depth < crate::types::task_context::MAX_MASK_DEPTH,
939            "mask depth exceeded MAX_MASK_DEPTH ({}) in actor::on_stop: \
940             this violates INV-MASK-BOUNDED and prevents cancellation from ever \
941             being observed. Reduce nesting of masked sections.",
942            crate::types::task_context::MAX_MASK_DEPTH
943        );
944        guard.mask_depth += 1;
945    }
946    let mask_guard = OnStopMaskGuard(inner);
947    actor.on_stop(&cx).await;
948    drop(mask_guard);
949
950    actor
951}
952
953fn actor_cancel_join_error(cx: &Cx) -> JoinError {
954    JoinError::Cancelled(
955        cx.cancel_reason()
956            .unwrap_or_else(|| crate::types::CancelReason::user("actor supervision cancelled")),
957    )
958}
959
960fn supervised_restart_timestamp(cx: &Cx) -> u64 {
961    cx.timer_driver().map_or_else(
962        || crate::time::wall_now().as_nanos(),
963        |td| td.now().as_nanos(),
964    )
965}
966
967#[inline]
968fn try_commit_supervised_restart(state: &ActorStateCell) -> bool {
969    state.compare_and_swap(ActorState::Running, ActorState::Created)
970}
971
972async fn wait_supervised_restart_delay(cx: &Cx, delay: Duration) -> Outcome<(), JoinError> {
973    if cx.checkpoint().is_err() {
974        return Outcome::err(actor_cancel_join_error(cx));
975    }
976    if delay.is_zero() {
977        return Outcome::ok(());
978    }
979
980    let mut sleeper = cx.timer_driver().map_or_else(
981        || crate::time::sleep(crate::time::wall_now(), delay),
982        |driver| {
983            let delay_nanos = u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX);
984            let deadline = driver.now().saturating_add_nanos(delay_nanos);
985            crate::time::Sleep::with_timer_driver(deadline, driver)
986        },
987    );
988    std::future::poll_fn(|task_cx| {
989        if cx.checkpoint().is_err() {
990            return std::task::Poll::Ready(Outcome::err(actor_cancel_join_error(cx)));
991        }
992        Pin::new(&mut sleeper)
993            .poll(task_cx)
994            .map(|()| Outcome::ok(()))
995    })
996    .await
997}
998
999fn join_result_to_task_outcome<A>(result: &Result<A, JoinError>) -> Outcome<(), ()> {
1000    match result {
1001        Ok(_) => Outcome::Ok(()),
1002        Err(JoinError::Cancelled(reason)) => Outcome::Cancelled(reason.clone()),
1003        Err(JoinError::Panicked(payload)) => Outcome::Panicked(payload.clone()),
1004        Err(JoinError::PolledAfterCompletion) => {
1005            // br-supervision-fix.1 — Return error instead of panicking to preserve
1006            // process isolation. PolledAfterCompletion indicates a runtime bug but
1007            // should not crash the supervision tree.
1008            Outcome::Err(())
1009        }
1010    }
1011}
1012
1013// Extension for Scope to spawn actors
1014impl<P: crate::types::Policy> crate::cx::Scope<'_, P> {
1015    /// Spawns a new actor in this scope with the given mailbox capacity.
1016    ///
1017    /// The actor runs as a region-owned task. Messages are delivered through
1018    /// a bounded MPSC channel with two-phase send semantics.
1019    ///
1020    /// # Arguments
1021    ///
1022    /// * `state` - Runtime state for task creation
1023    /// * `cx` - Capability context
1024    /// * `actor` - The actor instance
1025    /// * `mailbox_capacity` - Bounded mailbox size
1026    ///
1027    /// # Returns
1028    ///
1029    /// A tuple of `(ActorHandle, StoredTask)`. The `StoredTask` must be
1030    /// registered with the runtime via `state.store_spawned_task()`.
1031    pub fn spawn_actor<A: Actor>(
1032        &self,
1033        state: &mut crate::runtime::state::RuntimeState,
1034        cx: &Cx,
1035        actor: A,
1036        mailbox_capacity: usize,
1037    ) -> Result<(ActorHandle<A>, crate::runtime::stored_task::StoredTask), SpawnError> {
1038        use crate::channel::oneshot;
1039        use crate::cx::scope::CatchUnwind;
1040        use crate::runtime::stored_task::StoredTask;
1041        use crate::tracing_compat::{debug, debug_span};
1042
1043        // Create the actor's mailbox
1044        let (msg_tx, msg_rx) = mpsc::channel::<A::Message>(mailbox_capacity);
1045
1046        // Create oneshot for returning the actor state
1047        let (result_tx, result_rx) = oneshot::channel::<Result<A, JoinError>>();
1048
1049        // Create task record
1050        let task_id = self.create_task_record(state)?;
1051        let actor_id = ActorId::from_task(task_id);
1052        let actor_state = Arc::new(ActorStateCell::new(ActorState::Created));
1053        let region_id = self.region_id();
1054
1055        // Create child context
1056        let (_, child_cx) = self.build_child_task_cx(state, cx, task_id);
1057
1058        // Link Cx to TaskRecord
1059        if let Some(record) = state.task_mut(task_id) {
1060            record.set_cx_inner(child_cx.inner.clone());
1061            record.set_cx(child_cx.clone());
1062        }
1063        let spawned_at = state
1064            .timer_driver()
1065            .map_or(state.now, crate::time::TimerDriverHandle::now);
1066        let spawn_effects = state.prepare_task_spawn_effects(
1067            task_id,
1068            region_id,
1069            self.budget(),
1070            crate::runtime::state::TaskSpawnSource::Scope,
1071            spawned_at,
1072        );
1073
1074        let inner_weak = Arc::downgrade(&child_cx.inner);
1075        let state_for_task = Arc::clone(&actor_state);
1076
1077        let mut cell = ActorCell {
1078            mailbox: msg_rx,
1079            state: Arc::clone(&actor_state),
1080        };
1081
1082        // Create the actor loop future
1083        let wrapped = async move {
1084            spawn_effects.dispatch();
1085            let result = CatchUnwind {
1086                inner: Box::pin(async move {
1087                    {
1088                        let _span = debug_span!(
1089                            "actor_spawn",
1090                            task_id = ?task_id,
1091                            region_id = ?region_id,
1092                            mailbox_capacity = mailbox_capacity,
1093                        )
1094                        .entered();
1095                        debug!(
1096                            task_id = ?task_id,
1097                            region_id = ?region_id,
1098                            mailbox_capacity = mailbox_capacity,
1099                            "actor spawned"
1100                        );
1101                    }
1102                    run_actor_loop(actor, child_cx, &mut cell).await
1103                }),
1104            }
1105            .await;
1106            state_for_task.store(ActorState::Stopped);
1107            match result {
1108                Ok(actor_final) => {
1109                    let _ = result_tx.send_blocking(Ok(actor_final));
1110                    Outcome::Ok(())
1111                }
1112                Err(payload) => {
1113                    let msg = crate::cx::scope::payload_to_string(&payload);
1114                    std::mem::forget(payload);
1115                    let panic_payload = crate::types::PanicPayload::new(msg);
1116                    let _ =
1117                        result_tx.send_blocking(Err(JoinError::Panicked(panic_payload.clone())));
1118                    Outcome::Panicked(panic_payload)
1119                }
1120            }
1121        };
1122
1123        let stored = StoredTask::new_with_id(wrapped, task_id);
1124
1125        let handle = ActorHandle {
1126            actor_id,
1127            sender: msg_tx,
1128            state: actor_state,
1129            task_id,
1130            receiver: result_rx,
1131            inner: inner_weak,
1132            completed: false,
1133        };
1134
1135        Ok((handle, stored))
1136    }
1137
1138    /// Spawns a supervised actor with explicit supervision semantics.
1139    ///
1140    /// Unlike `spawn_actor`, this method takes a factory closure that can
1141    /// produce new actor instances for restarts. The mailbox persists across
1142    /// restarts, so messages sent while a restartable failure is being handled
1143    /// are buffered for the next instance.
1144    ///
1145    /// Because [`Actor`] has no explicit error return channel, supervised
1146    /// crashes are treated as restartable failures when the strategy is
1147    /// [`crate::supervision::SupervisionStrategy::Restart`]. If supervision
1148    /// ultimately stops or escalates, the original panic payload is still
1149    /// surfaced as `JoinError::Panicked`.
1150    ///
1151    /// # Arguments
1152    ///
1153    /// * `state` - Runtime state for task creation
1154    /// * `cx` - Capability context
1155    /// * `factory` - Closure that creates actor instances (called on each restart)
1156    /// * `strategy` - Supervision strategy (Stop, Restart, Escalate)
1157    /// * `mailbox_capacity` - Bounded mailbox size
1158    pub fn spawn_supervised_actor<A, F>(
1159        &self,
1160        state: &mut crate::runtime::state::RuntimeState,
1161        cx: &Cx,
1162        mut factory: F,
1163        strategy: crate::supervision::SupervisionStrategy,
1164        mailbox_capacity: usize,
1165    ) -> Result<(ActorHandle<A>, crate::runtime::stored_task::StoredTask), SpawnError>
1166    where
1167        A: Actor,
1168        F: FnMut() -> A + Send + 'static,
1169    {
1170        use crate::channel::oneshot;
1171        use crate::runtime::stored_task::StoredTask;
1172        use crate::supervision::Supervisor;
1173        use crate::tracing_compat::{debug, debug_span};
1174
1175        let (msg_tx, msg_rx) = mpsc::channel::<A::Message>(mailbox_capacity);
1176        let (result_tx, result_rx) = oneshot::channel::<Result<A, JoinError>>();
1177        let task_id = self.create_task_record(state)?;
1178        let actor_id = ActorId::from_task(task_id);
1179        let actor_state = Arc::new(ActorStateCell::new(ActorState::Created));
1180        let region_id = self.region_id();
1181
1182        let (_, child_cx) = self.build_child_task_cx(state, cx, task_id);
1183
1184        if let Some(record) = state.task_mut(task_id) {
1185            record.set_cx_inner(child_cx.inner.clone());
1186            record.set_cx(child_cx.clone());
1187        }
1188        let spawned_at = state
1189            .timer_driver()
1190            .map_or(state.now, crate::time::TimerDriverHandle::now);
1191        let spawn_effects = state.prepare_task_spawn_effects(
1192            task_id,
1193            region_id,
1194            self.budget(),
1195            crate::runtime::state::TaskSpawnSource::Scope,
1196            spawned_at,
1197        );
1198
1199        let inner_weak = Arc::downgrade(&child_cx.inner);
1200        let state_for_task = Arc::clone(&actor_state);
1201
1202        let mut cell = ActorCell {
1203            mailbox: msg_rx,
1204            state: Arc::clone(&actor_state),
1205        };
1206
1207        let wrapped = async move {
1208            spawn_effects.dispatch();
1209            let result = match (crate::cx::scope::CatchUnwind {
1210                inner: Box::pin(async move {
1211                    {
1212                        let _span = debug_span!(
1213                            "supervised_actor_spawn",
1214                            task_id = ?task_id,
1215                            region_id = ?region_id,
1216                            mailbox_capacity = mailbox_capacity,
1217                        )
1218                        .entered();
1219                        debug!(
1220                            task_id = ?task_id,
1221                            region_id = ?region_id,
1222                            "supervised actor spawned"
1223                        );
1224                    }
1225                    let actor = factory();
1226                    run_supervised_loop(
1227                        actor,
1228                        &mut factory,
1229                        child_cx,
1230                        &mut cell,
1231                        Supervisor::new(strategy),
1232                        task_id,
1233                        region_id,
1234                    )
1235                    .await
1236                }),
1237            })
1238            .await
1239            {
1240                Ok(result) => result,
1241                Err(payload) => {
1242                    let message = crate::cx::scope::payload_to_string(&payload);
1243                    std::mem::forget(payload);
1244                    Err(JoinError::Panicked(crate::types::PanicPayload::new(
1245                        message,
1246                    )))
1247                }
1248            };
1249            state_for_task.store(ActorState::Stopped);
1250            let outcome = join_result_to_task_outcome(&result).map_err(|_| ());
1251            let _ = result_tx.send_blocking(result);
1252            outcome
1253        };
1254
1255        let stored = StoredTask::new_with_id(wrapped, task_id);
1256
1257        let handle = ActorHandle {
1258            actor_id,
1259            sender: msg_tx,
1260            state: actor_state,
1261            task_id,
1262            receiver: result_rx,
1263            inner: inner_weak,
1264            completed: false,
1265        };
1266
1267        Ok((handle, stored))
1268    }
1269}
1270
1271/// Outcome of a supervised actor run.
1272#[derive(Debug)]
1273pub enum SupervisedOutcome {
1274    /// Actor stopped normally (no failure).
1275    Stopped,
1276    /// Actor stopped after restart budget exhaustion.
1277    RestartBudgetExhausted {
1278        /// Total restarts before budget was exhausted.
1279        total_restarts: u32,
1280    },
1281    /// Failure was escalated to parent region.
1282    Escalated,
1283}
1284
1285/// Internal: runs a supervised actor loop with restart support.
1286///
1287/// The mailbox receiver is shared across restarts — messages sent while the
1288/// actor is restarting are buffered and processed by the new instance.
1289async fn run_supervised_loop<A, F>(
1290    initial_actor: A,
1291    factory: &mut F,
1292    cx: Cx,
1293    cell: &mut ActorCell<A::Message>,
1294    mut supervisor: crate::supervision::Supervisor,
1295    task_id: TaskId,
1296    region_id: RegionId,
1297) -> Result<A, JoinError>
1298where
1299    A: Actor,
1300    F: FnMut() -> A,
1301{
1302    use crate::cx::scope::CatchUnwind;
1303    use crate::supervision::SupervisionDecision;
1304    use crate::types::Outcome;
1305
1306    let mut current_actor = initial_actor;
1307
1308    loop {
1309        // Run the actor until it finishes (normally or via panic)
1310        let result = CatchUnwind {
1311            inner: Box::pin(run_actor_loop(current_actor, cx.clone(), cell)),
1312        }
1313        .await;
1314
1315        match result {
1316            Ok(actor_final) => {
1317                // Actor completed normally — no supervision needed
1318                return Ok(actor_final);
1319            }
1320            Err(payload) => {
1321                let msg = crate::cx::scope::payload_to_string(&payload);
1322                let panic_payload = crate::types::PanicPayload::new(msg);
1323                cx.trace("supervised_actor::failure");
1324
1325                // Explicit shutdown wins over restart policy. If the owner has
1326                // already requested stop/abort, a panic during mailbox drain or
1327                // on_stop is terminal and must not resurrect the actor.
1328                if cell.state.load() == ActorState::Stopping || cx.checkpoint().is_err() {
1329                    cx.trace("supervised_actor::shutdown_panic");
1330                    return Err(JoinError::Panicked(panic_payload));
1331                }
1332
1333                // Actors do not have a typed `Err` path. A crash is therefore
1334                // the only recoverable failure signal available to the actor
1335                // supervision layer, so present it to the generic supervisor as
1336                // a restartable failure while preserving the original payload to
1337                // surface if supervision ultimately stops or escalates.
1338                let outcome = Outcome::Err(());
1339                let now = supervised_restart_timestamp(&cx);
1340                let decision = supervisor.on_failure(task_id, region_id, None, &outcome, now);
1341
1342                match decision {
1343                    SupervisionDecision::Restart { delay, .. } => {
1344                        cx.trace("supervised_actor::restart");
1345
1346                        // Graceful shutdown may arrive after the crash but
1347                        // before the delayed restart starts running. That stop
1348                        // must suppress the restart rather than instantiate a
1349                        // fresh actor during shutdown.
1350                        if cell.state.load() == ActorState::Stopping {
1351                            cx.trace("supervised_actor::restart_suppressed");
1352                            return Err(JoinError::Panicked(panic_payload));
1353                        }
1354
1355                        // Apply backoff delay if the supervisor computed one.
1356                        if let Some(backoff) = delay {
1357                            match wait_supervised_restart_delay(&cx, backoff).await {
1358                                Outcome::Ok(()) => {}
1359                                Outcome::Err(err) => return Err(err),
1360                                Outcome::Cancelled(_) => return Err(actor_cancel_join_error(&cx)),
1361                                Outcome::Panicked(payload) => {
1362                                    return Err(JoinError::Panicked(payload));
1363                                }
1364                            }
1365                        }
1366
1367                        if cx.checkpoint().is_err() {
1368                            cx.trace("supervised_actor::restart_suppressed");
1369                            return Err(JoinError::Panicked(panic_payload));
1370                        }
1371
1372                        // Commit the restart only while the actor is still
1373                        // Running. A concurrent stop transitions it to
1374                        // Stopping; the conditional swap must preserve that
1375                        // shutdown request instead of resurrecting the actor.
1376                        if !try_commit_supervised_restart(&cell.state) {
1377                            cx.trace("supervised_actor::restart_suppressed");
1378                            return Err(JoinError::Panicked(panic_payload));
1379                        }
1380                        current_actor = factory();
1381                    }
1382                    SupervisionDecision::Stop { .. } => {
1383                        cx.trace("supervised_actor::stopped");
1384                        return Err(JoinError::Panicked(panic_payload));
1385                    }
1386                    SupervisionDecision::Escalate { .. } => {
1387                        cx.trace("supervised_actor::escalated");
1388                        return Err(JoinError::Panicked(panic_payload));
1389                    }
1390                }
1391            }
1392        }
1393    }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398    #![allow(
1399        clippy::pedantic,
1400        clippy::nursery,
1401        clippy::expect_fun_call,
1402        clippy::map_unwrap_or,
1403        clippy::cast_possible_wrap,
1404        clippy::future_not_send
1405    )]
1406    use super::*;
1407    use crate::cx::macaroon::MacaroonToken;
1408    use crate::cx::registry::{RegistryCap, RegistryHandle};
1409    use crate::remote::{NodeId, RemoteCap};
1410    use crate::runtime::state::RuntimeState;
1411    use crate::security::key::AuthKey;
1412    use crate::types::Budget;
1413    use crate::types::SystemPressure;
1414    use crate::types::policy::FailFast;
1415    use std::sync::Arc;
1416    use std::task::{Context, Poll, Waker};
1417
1418    fn init_test(name: &str) {
1419        crate::test_utils::init_test_logging();
1420        crate::test_phase!(name);
1421    }
1422
1423    fn actor_join_future_from_receiver<'a, A: Actor>(
1424        receiver: &'a mut crate::channel::oneshot::Receiver<Result<A, JoinError>>,
1425        terminal_state: &'a mut bool,
1426    ) -> ActorJoinFuture<'a, A> {
1427        let (sender, _mailbox_rx) = mpsc::channel::<A::Message>(4);
1428        ActorJoinFuture {
1429            inner: receiver.recv_uninterruptible(),
1430            cx_inner: std::sync::Weak::new(),
1431            sender,
1432            state: Arc::new(ActorStateCell::new(ActorState::Running)),
1433            terminal_state,
1434            drop_abort_defused: false,
1435        }
1436    }
1437
1438    fn counting_waker(counter: Arc<std::sync::atomic::AtomicUsize>) -> Waker {
1439        struct CountingWaker {
1440            counter: Arc<std::sync::atomic::AtomicUsize>,
1441        }
1442
1443        impl std::task::Wake for CountingWaker {
1444            fn wake(self: Arc<Self>) {
1445                self.counter.fetch_add(1, Ordering::Relaxed);
1446            }
1447
1448            fn wake_by_ref(self: &Arc<Self>) {
1449                self.counter.fetch_add(1, Ordering::Relaxed);
1450            }
1451        }
1452
1453        Waker::from(Arc::new(CountingWaker { counter }))
1454    }
1455
1456    fn terminal_state_waker(
1457        state: Arc<ActorStateCell>,
1458        counter: Arc<std::sync::atomic::AtomicUsize>,
1459    ) -> Waker {
1460        struct TerminalStateWaker {
1461            state: Arc<ActorStateCell>,
1462            counter: Arc<std::sync::atomic::AtomicUsize>,
1463        }
1464
1465        impl TerminalStateWaker {
1466            fn record_wake(&self) {
1467                assert_eq!(
1468                    self.state.load(),
1469                    ActorState::Stopped,
1470                    "join receiver must not wake before actor state is terminal"
1471                );
1472                self.counter.fetch_add(1, Ordering::Relaxed);
1473            }
1474        }
1475
1476        impl std::task::Wake for TerminalStateWaker {
1477            fn wake(self: Arc<Self>) {
1478                self.record_wake();
1479            }
1480
1481            fn wake_by_ref(self: &Arc<Self>) {
1482                self.record_wake();
1483            }
1484        }
1485
1486        Waker::from(Arc::new(TerminalStateWaker { state, counter }))
1487    }
1488
1489    /// Simple counter actor for testing.
1490    #[derive(Debug)]
1491    struct Counter {
1492        count: u64,
1493        started: bool,
1494        stopped: bool,
1495    }
1496
1497    impl Counter {
1498        fn new() -> Self {
1499            Self {
1500                count: 0,
1501                started: false,
1502                stopped: false,
1503            }
1504        }
1505    }
1506
1507    impl Actor for Counter {
1508        type Message = u64;
1509
1510        fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1511            self.started = true;
1512            Box::pin(async {})
1513        }
1514
1515        fn handle(&mut self, _cx: &Cx, msg: u64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1516            self.count += msg;
1517            Box::pin(async {})
1518        }
1519
1520        fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1521            self.stopped = true;
1522            Box::pin(async {})
1523        }
1524    }
1525
1526    fn assert_actor<A: Actor>() {}
1527
1528    #[derive(Debug, Clone, PartialEq, Eq)]
1529    struct CapabilitySnapshot {
1530        same_registry: bool,
1531        same_remote: bool,
1532        same_io: bool,
1533        same_pressure: bool,
1534        same_macaroon: bool,
1535        has_timer: bool,
1536    }
1537
1538    struct CapabilityProbeActor {
1539        snapshot: Arc<parking_lot::Mutex<Option<CapabilitySnapshot>>>,
1540        expected_registry: Arc<dyn RegistryCap>,
1541        expected_remote_node: String,
1542        expected_io: Arc<dyn crate::io::IoCap>,
1543        expected_pressure: Arc<SystemPressure>,
1544        expected_macaroon: Arc<MacaroonToken>,
1545    }
1546
1547    impl Actor for CapabilityProbeActor {
1548        type Message = ();
1549
1550        fn on_start(&mut self, cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1551            let child_registry = cx
1552                .registry_handle()
1553                .expect("actor child Cx must inherit registry")
1554                .as_arc();
1555            let child_io = cx
1556                .io_cap_handle()
1557                .expect("actor child Cx must inherit io capability");
1558            let child_pressure = cx
1559                .pressure_handle()
1560                .expect("actor child Cx must inherit system pressure");
1561            let child_macaroon = cx
1562                .macaroon_handle()
1563                .expect("actor child Cx must inherit macaroon");
1564            let remote_node = cx
1565                .remote()
1566                .map(|remote| remote.local_node().as_str().to_owned());
1567
1568            *self.snapshot.lock() = Some(CapabilitySnapshot {
1569                same_registry: Arc::ptr_eq(&child_registry, &self.expected_registry),
1570                same_remote: remote_node.as_deref() == Some(self.expected_remote_node.as_str()),
1571                same_io: Arc::ptr_eq(&child_io, &self.expected_io),
1572                same_pressure: Arc::ptr_eq(&child_pressure, &self.expected_pressure),
1573                same_macaroon: Arc::ptr_eq(&child_macaroon, &self.expected_macaroon),
1574                has_timer: cx.has_timer(),
1575            });
1576
1577            Box::pin(async {})
1578        }
1579
1580        fn handle(&mut self, _cx: &Cx, _msg: ()) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1581            Box::pin(async {})
1582        }
1583    }
1584
1585    fn capability_rich_parent_cx(
1586        runtime: &crate::lab::LabRuntime,
1587        region: crate::types::RegionId,
1588    ) -> (
1589        Cx,
1590        Arc<dyn RegistryCap>,
1591        Arc<dyn crate::io::IoCap>,
1592        Arc<SystemPressure>,
1593        Arc<MacaroonToken>,
1594    ) {
1595        let registry = crate::cx::NameRegistry::new();
1596        let registry_handle = RegistryHandle::new(Arc::new(registry));
1597        let registry_arc = registry_handle.as_arc();
1598        let io_cap: Arc<dyn crate::io::IoCap> = Arc::new(crate::io::LabIoCap::new_for_tests());
1599        let pressure = Arc::new(SystemPressure::with_headroom(0.25));
1600        let macaroon_token =
1601            MacaroonToken::mint(&AuthKey::from_seed(7), "scope:actor", "actor/tests");
1602
1603        let parent_cx = Cx::new_with_drivers(
1604            region,
1605            crate::types::TaskId::new_for_test(77, 0),
1606            Budget::INFINITE,
1607            None,
1608            None,
1609            Some(Arc::clone(&io_cap)),
1610            runtime.state.timer_driver_handle(),
1611            None,
1612        )
1613        .with_registry_handle(Some(registry_handle))
1614        .with_remote_cap(RemoteCap::new().with_local_node(NodeId::new("actor-origin")))
1615        .with_pressure(Arc::clone(&pressure))
1616        .with_macaroon(macaroon_token);
1617
1618        let macaroon = parent_cx
1619            .macaroon_handle()
1620            .expect("parent actor test Cx must retain macaroon");
1621
1622        (parent_cx, registry_arc, io_cap, pressure, macaroon)
1623    }
1624
1625    #[test]
1626    fn actor_trait_object_safety() {
1627        init_test("actor_trait_object_safety");
1628
1629        // Verify Counter implements Actor with the right bounds
1630        assert_actor::<Counter>();
1631
1632        crate::test_complete!("actor_trait_object_safety");
1633    }
1634
1635    #[test]
1636    fn actor_handle_creation() {
1637        init_test("actor_handle_creation");
1638
1639        let mut state = RuntimeState::new();
1640        let root = state.create_root_region(Budget::INFINITE);
1641        let cx: Cx = Cx::for_testing();
1642        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
1643
1644        let result = scope.spawn_actor(&mut state, &cx, Counter::new(), 32);
1645        assert!(result.is_ok(), "spawn_actor should succeed");
1646
1647        let (handle, stored) = result.unwrap();
1648        state.store_spawned_task(handle.task_id(), stored);
1649
1650        // Handle should have valid task ID
1651        let _tid = handle.task_id();
1652
1653        // Actor should not be finished yet (not polled)
1654        assert!(!handle.is_finished());
1655
1656        crate::test_complete!("actor_handle_creation");
1657    }
1658
1659    #[test]
1660    fn spawn_actor_inherits_child_cx_capabilities() {
1661        init_test("spawn_actor_inherits_child_cx_capabilities");
1662
1663        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1664        let region = runtime.state.create_root_region(Budget::INFINITE);
1665        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1666        let (parent_cx, registry_arc, io_cap, pressure, macaroon) =
1667            capability_rich_parent_cx(&runtime, region);
1668        let snapshot = Arc::new(parking_lot::Mutex::new(None));
1669
1670        let actor = CapabilityProbeActor {
1671            snapshot: Arc::clone(&snapshot),
1672            expected_registry: registry_arc,
1673            expected_remote_node: "actor-origin".to_string(),
1674            expected_io: io_cap,
1675            expected_pressure: pressure,
1676            expected_macaroon: macaroon,
1677        };
1678
1679        let (handle, stored) = scope
1680            .spawn_actor(&mut runtime.state, &parent_cx, actor, 8)
1681            .expect("spawn actor");
1682        let task_id = handle.task_id();
1683        runtime.state.store_spawned_task(task_id, stored);
1684
1685        runtime.scheduler.lock().schedule(task_id, 0);
1686        runtime.run_until_idle();
1687
1688        let observed = snapshot
1689            .lock()
1690            .clone()
1691            .expect("actor on_start should capture inherited capabilities");
1692        assert_eq!(
1693            observed,
1694            CapabilitySnapshot {
1695                same_registry: true,
1696                same_remote: true,
1697                same_io: true,
1698                same_pressure: true,
1699                same_macaroon: true,
1700                has_timer: true,
1701            }
1702        );
1703
1704        drop(handle);
1705        runtime.run_until_quiescent();
1706
1707        crate::test_complete!("spawn_actor_inherits_child_cx_capabilities");
1708    }
1709
1710    #[test]
1711    fn spawn_supervised_actor_inherits_child_cx_capabilities() {
1712        init_test("spawn_supervised_actor_inherits_child_cx_capabilities");
1713
1714        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1715        let region = runtime.state.create_root_region(Budget::INFINITE);
1716        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1717        let (parent_cx, registry_arc, io_cap, pressure, macaroon) =
1718            capability_rich_parent_cx(&runtime, region);
1719        let snapshot = Arc::new(parking_lot::Mutex::new(None));
1720
1721        let snapshot_for_factory = Arc::clone(&snapshot);
1722        let strategy = crate::supervision::SupervisionStrategy::Stop;
1723        let (handle, stored) = scope
1724            .spawn_supervised_actor(
1725                &mut runtime.state,
1726                &parent_cx,
1727                move || CapabilityProbeActor {
1728                    snapshot: Arc::clone(&snapshot_for_factory),
1729                    expected_registry: Arc::clone(&registry_arc),
1730                    expected_remote_node: "actor-origin".to_string(),
1731                    expected_io: Arc::clone(&io_cap),
1732                    expected_pressure: Arc::clone(&pressure),
1733                    expected_macaroon: Arc::clone(&macaroon),
1734                },
1735                strategy,
1736                8,
1737            )
1738            .expect("spawn supervised actor");
1739        let task_id = handle.task_id();
1740        runtime.state.store_spawned_task(task_id, stored);
1741
1742        runtime.scheduler.lock().schedule(task_id, 0);
1743        runtime.run_until_idle();
1744
1745        let observed = snapshot
1746            .lock()
1747            .clone()
1748            .expect("supervised actor on_start should capture inherited capabilities");
1749        assert_eq!(
1750            observed,
1751            CapabilitySnapshot {
1752                same_registry: true,
1753                same_remote: true,
1754                same_io: true,
1755                same_pressure: true,
1756                same_macaroon: true,
1757                has_timer: true,
1758            }
1759        );
1760
1761        drop(handle);
1762        runtime.run_until_quiescent();
1763
1764        crate::test_complete!("spawn_supervised_actor_inherits_child_cx_capabilities");
1765    }
1766
1767    #[test]
1768    fn actor_id_generation_distinct() {
1769        init_test("actor_id_generation_distinct");
1770
1771        let id1 = ActorId::from_task(TaskId::new_for_test(1, 1));
1772        let id2 = ActorId::from_task(TaskId::new_for_test(1, 2));
1773        assert!(id1 != id2, "generation must distinguish actor reuse");
1774
1775        crate::test_complete!("actor_id_generation_distinct");
1776    }
1777
1778    #[test]
1779    fn actor_ref_is_cloneable() {
1780        init_test("actor_ref_is_cloneable");
1781
1782        let mut state = RuntimeState::new();
1783        let root = state.create_root_region(Budget::INFINITE);
1784        let cx: Cx = Cx::for_testing();
1785        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
1786
1787        let (handle, stored) = scope
1788            .spawn_actor(&mut state, &cx, Counter::new(), 32)
1789            .unwrap();
1790        state.store_spawned_task(handle.task_id(), stored);
1791
1792        // Get multiple refs
1793        let ref1 = handle.sender();
1794        let ref2 = ref1.clone();
1795
1796        // Actor identity is preserved across clones
1797        assert_eq!(ref1.actor_id(), handle.actor_id());
1798        assert_eq!(ref2.actor_id(), handle.actor_id());
1799
1800        // Actor is alive at creation time (even before first poll)
1801        assert!(ref1.is_alive());
1802        assert!(ref2.is_alive());
1803
1804        // Both should be open
1805        assert!(!ref1.is_closed());
1806        assert!(!ref2.is_closed());
1807
1808        crate::test_complete!("actor_ref_is_cloneable");
1809    }
1810
1811    // ---- E2E Actor Scenarios ----
1812
1813    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1814
1815    /// Observable counter actor: writes final count to shared state during on_stop.
1816    /// Used by E2E tests to verify actor behavior without needing join().
1817    struct ObservableCounter {
1818        count: u64,
1819        on_stop_count: Arc<AtomicU64>,
1820        started: Arc<AtomicBool>,
1821        stopped: Arc<AtomicBool>,
1822    }
1823
1824    impl ObservableCounter {
1825        fn new(
1826            on_stop_count: Arc<AtomicU64>,
1827            started: Arc<AtomicBool>,
1828            stopped: Arc<AtomicBool>,
1829        ) -> Self {
1830            Self {
1831                count: 0,
1832                on_stop_count,
1833                started,
1834                stopped,
1835            }
1836        }
1837    }
1838
1839    impl Actor for ObservableCounter {
1840        type Message = u64;
1841
1842        fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1843            self.started.store(true, Ordering::SeqCst);
1844            Box::pin(async {})
1845        }
1846
1847        fn handle(&mut self, _cx: &Cx, msg: u64) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1848            self.count += msg;
1849            Box::pin(async {})
1850        }
1851
1852        fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1853            self.on_stop_count.store(self.count, Ordering::SeqCst);
1854            self.stopped.store(true, Ordering::SeqCst);
1855            Box::pin(async {})
1856        }
1857    }
1858
1859    fn observable_state() -> (Arc<AtomicU64>, Arc<AtomicBool>, Arc<AtomicBool>) {
1860        (
1861            Arc::new(AtomicU64::new(u64::MAX)),
1862            Arc::new(AtomicBool::new(false)),
1863            Arc::new(AtomicBool::new(false)),
1864        )
1865    }
1866
1867    /// E2E: Actor processes all messages sent before channel disconnect.
1868    /// Verifies: messages delivered, on_start called, on_stop called.
1869    #[test]
1870    fn actor_processes_all_messages() {
1871        init_test("actor_processes_all_messages");
1872
1873        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1874        let region = runtime.state.create_root_region(Budget::INFINITE);
1875        let cx: Cx = Cx::for_testing();
1876        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1877
1878        let (on_stop_count, started, stopped) = observable_state();
1879        let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
1880
1881        let (handle, stored) = scope
1882            .spawn_actor(&mut runtime.state, &cx, actor, 32)
1883            .unwrap();
1884        let task_id = handle.task_id();
1885        runtime.state.store_spawned_task(task_id, stored);
1886
1887        // Pre-fill mailbox with 5 messages (each adding 1)
1888        for _ in 0..5 {
1889            handle.try_send(1).unwrap();
1890        }
1891
1892        // Drop handle to disconnect channel — actor will process buffered
1893        // messages via recv, then see Disconnected and stop gracefully.
1894        drop(handle);
1895
1896        runtime.scheduler.lock().schedule(task_id, 0);
1897        runtime.run_until_quiescent();
1898
1899        assert_eq!(
1900            on_stop_count.load(Ordering::SeqCst),
1901            5,
1902            "all messages processed"
1903        );
1904        assert!(started.load(Ordering::SeqCst), "on_start was called");
1905        assert!(stopped.load(Ordering::SeqCst), "on_stop was called");
1906
1907        crate::test_complete!("actor_processes_all_messages");
1908    }
1909
1910    /// E2E: Mailbox drain on cancellation.
1911    /// Pre-fills mailbox, cancels actor before it runs, verifies all messages
1912    /// are still processed during the drain phase (no silent drops).
1913    #[test]
1914    fn actor_drains_mailbox_on_cancel() {
1915        init_test("actor_drains_mailbox_on_cancel");
1916
1917        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1918        let region = runtime.state.create_root_region(Budget::INFINITE);
1919        let cx: Cx = Cx::for_testing();
1920        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1921
1922        let (on_stop_count, started, stopped) = observable_state();
1923        let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
1924
1925        let (handle, stored) = scope
1926            .spawn_actor(&mut runtime.state, &cx, actor, 32)
1927            .unwrap();
1928        let task_id = handle.task_id();
1929        runtime.state.store_spawned_task(task_id, stored);
1930
1931        // Pre-fill mailbox with 5 messages
1932        for _ in 0..5 {
1933            handle.try_send(1).unwrap();
1934        }
1935
1936        // Cancel the actor BEFORE running.
1937        // The actor loop will: on_start → check cancel → break → drain → on_stop
1938        handle.stop();
1939        let stopped_ref = handle.sender();
1940        assert!(
1941            stopped_ref.is_closed(),
1942            "stop() seals the mailbox immediately"
1943        );
1944        assert!(
1945            matches!(handle.try_send(99), Err(SendError::Disconnected(99))),
1946            "stop() must reject new messages instead of extending shutdown"
1947        );
1948
1949        runtime.scheduler.lock().schedule(task_id, 0);
1950        runtime.run_until_quiescent();
1951
1952        // All 5 messages processed during drain phase
1953        assert_eq!(
1954            on_stop_count.load(Ordering::SeqCst),
1955            5,
1956            "drain processed all messages"
1957        );
1958        assert!(started.load(Ordering::SeqCst), "on_start was called");
1959        assert!(stopped.load(Ordering::SeqCst), "on_stop was called");
1960
1961        crate::test_complete!("actor_drains_mailbox_on_cancel");
1962    }
1963
1964    /// E2E: ActorRef liveness tracks actor lifecycle (Created -> Stopping -> Stopped).
1965    #[test]
1966    fn actor_ref_is_alive_transitions() {
1967        init_test("actor_ref_is_alive_transitions");
1968
1969        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
1970        let region = runtime.state.create_root_region(Budget::INFINITE);
1971        let cx: Cx = Cx::for_testing();
1972        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
1973
1974        let (on_stop_count, started, stopped) = observable_state();
1975        let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
1976
1977        let (handle, stored) = scope
1978            .spawn_actor(&mut runtime.state, &cx, actor, 32)
1979            .unwrap();
1980        let task_id = handle.task_id();
1981        runtime.state.store_spawned_task(task_id, stored);
1982
1983        let actor_ref = handle.sender();
1984        assert!(actor_ref.is_alive(), "created actor should be alive");
1985        assert_eq!(actor_ref.actor_id(), handle.actor_id());
1986
1987        handle.stop();
1988        assert!(actor_ref.is_alive(), "stopping actor is still alive");
1989
1990        runtime.scheduler.lock().schedule(task_id, 0);
1991        runtime.run_until_quiescent();
1992
1993        assert!(
1994            handle.is_finished(),
1995            "actor should be finished after stop + run"
1996        );
1997        assert!(!actor_ref.is_alive(), "finished actor is not alive");
1998
1999        // Sanity: the actor ran its hooks.
2000        assert!(started.load(Ordering::SeqCst), "on_start was called");
2001        assert!(stopped.load(Ordering::SeqCst), "on_stop was called");
2002        assert_ne!(
2003            on_stop_count.load(Ordering::SeqCst),
2004            u64::MAX,
2005            "on_stop_count updated"
2006        );
2007
2008        crate::test_complete!("actor_ref_is_alive_transitions");
2009    }
2010
2011    #[test]
2012    fn dropped_join_future_marks_actor_stopping_like_abort() {
2013        init_test("dropped_join_future_marks_actor_stopping_like_abort");
2014
2015        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2016        let region = runtime.state.create_root_region(Budget::INFINITE);
2017        let cx: Cx = Cx::for_testing();
2018        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2019
2020        let (on_stop_count, started, stopped) = observable_state();
2021        let actor = ObservableCounter::new(on_stop_count.clone(), started.clone(), stopped.clone());
2022
2023        let (mut handle, stored) = scope
2024            .spawn_actor(&mut runtime.state, &cx, actor, 32)
2025            .unwrap();
2026        let task_id = handle.task_id();
2027        runtime.state.store_spawned_task(task_id, stored);
2028
2029        runtime.scheduler.lock().schedule(task_id, 0);
2030        runtime.run_until_idle();
2031        assert_eq!(
2032            handle.state.load(),
2033            ActorState::Running,
2034            "actor should be running before join drop requests abort"
2035        );
2036
2037        drop(handle.join(&cx));
2038
2039        assert_eq!(
2040            handle.state.load(),
2041            ActorState::Stopping,
2042            "dropping join future should mirror ActorHandle::abort state transition"
2043        );
2044        assert!(
2045            matches!(handle.try_send(1), Err(SendError::Disconnected(1))),
2046            "join-drop abort must seal the mailbox immediately"
2047        );
2048
2049        runtime.run_until_quiescent();
2050        assert!(
2051            handle.is_finished(),
2052            "actor should stop after join future drop"
2053        );
2054        assert!(started.load(Ordering::SeqCst), "on_start should have run");
2055        assert!(stopped.load(Ordering::SeqCst), "on_stop should have run");
2056        assert_eq!(
2057            on_stop_count.load(Ordering::SeqCst),
2058            0,
2059            "idle actor should stop without processing phantom messages"
2060        );
2061
2062        crate::test_complete!("dropped_join_future_marks_actor_stopping_like_abort");
2063    }
2064
2065    #[test]
2066    fn actor_stop_unblocks_pending_sender_with_disconnect() {
2067        init_test("actor_stop_unblocks_pending_sender_with_disconnect");
2068
2069        let mut state = RuntimeState::new();
2070        let root = state.create_root_region(Budget::INFINITE);
2071        let cx: Cx = Cx::for_testing();
2072        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2073
2074        let (handle, stored) = scope
2075            .spawn_actor(&mut state, &cx, Counter::new(), 1)
2076            .unwrap();
2077        state.store_spawned_task(handle.task_id(), stored);
2078
2079        handle.try_send(1).expect("fill mailbox");
2080        let sender = handle.sender();
2081        let mut send_fut = Box::pin(sender.send(&cx, 2));
2082        let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2083        let waker = counting_waker(Arc::clone(&wake_count));
2084        let mut task_cx = Context::from_waker(&waker);
2085
2086        let first_poll = send_fut.as_mut().poll(&mut task_cx);
2087        assert!(
2088            matches!(first_poll, Poll::Pending),
2089            "send should wait while the mailbox is full"
2090        );
2091
2092        handle.stop();
2093
2094        assert_eq!(
2095            wake_count.load(Ordering::SeqCst),
2096            1,
2097            "stop() must wake a sender blocked on mailbox capacity"
2098        );
2099        let second_poll = send_fut.as_mut().poll(&mut task_cx);
2100        assert!(
2101            matches!(
2102                second_poll,
2103                Poll::Ready(Outcome::Err(SendError::Disconnected(2)))
2104            ),
2105            "pending sender must fail fast once stop seals the mailbox"
2106        );
2107
2108        crate::test_complete!("actor_stop_unblocks_pending_sender_with_disconnect");
2109    }
2110
2111    /// E2E: Supervised actor crashes restart under Restart strategy.
2112    #[test]
2113    fn supervised_actor_panic_restarts_under_restart_strategy() {
2114        use std::sync::atomic::AtomicU32;
2115
2116        #[derive(Debug)]
2117        struct PanickingCounter {
2118            count: u64,
2119            panic_on: u64,
2120            final_count: Arc<AtomicU64>,
2121        }
2122
2123        impl Actor for PanickingCounter {
2124            type Message = u64;
2125
2126            fn handle(
2127                &mut self,
2128                _cx: &Cx,
2129                msg: u64,
2130            ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2131                assert!(msg != self.panic_on, "threshold exceeded: {msg}");
2132                self.count += msg;
2133                Box::pin(async {})
2134            }
2135
2136            fn on_stop(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2137                self.final_count.store(self.count, Ordering::SeqCst);
2138                Box::pin(async {})
2139            }
2140        }
2141
2142        init_test("supervised_actor_panic_restarts_under_restart_strategy");
2143
2144        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2145        let region = runtime.state.create_root_region(Budget::INFINITE);
2146        let cx: Cx = Cx::for_testing();
2147        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2148
2149        let final_count = Arc::new(AtomicU64::new(u64::MAX));
2150        let restart_count = Arc::new(AtomicU32::new(0));
2151        let fc = final_count.clone();
2152        let rc = restart_count.clone();
2153
2154        let strategy = crate::supervision::SupervisionStrategy::Restart(
2155            crate::supervision::RestartConfig::new(3, std::time::Duration::from_secs(60))
2156                .with_backoff(crate::supervision::BackoffStrategy::None),
2157        );
2158
2159        let (mut handle, stored) = scope
2160            .spawn_supervised_actor(
2161                &mut runtime.state,
2162                &cx,
2163                move || {
2164                    rc.fetch_add(1, Ordering::Relaxed);
2165                    PanickingCounter {
2166                        count: 0,
2167                        panic_on: 999,
2168                        final_count: fc.clone(),
2169                    }
2170                },
2171                strategy,
2172                32,
2173            )
2174            .unwrap();
2175        let task_id = handle.task_id();
2176        runtime.state.store_spawned_task(task_id, stored);
2177
2178        // Message sequence:
2179        // 1. Normal message (count += 1)
2180        // 2. Panic trigger
2181        // 3. Queued message that should run on the restarted actor instance
2182        handle.try_send(1).unwrap();
2183        handle.try_send(999).unwrap(); // triggers panic
2184        handle.try_send(1).unwrap();
2185
2186        runtime.scheduler.lock().schedule(task_id, 0);
2187        runtime.run_until_idle();
2188        handle.abort();
2189        runtime.run_until_quiescent();
2190
2191        let join = futures_lite::future::block_on(handle.join(&cx));
2192        let actor = join.expect("aborting the restarted actor should still return final state");
2193        assert_eq!(
2194            restart_count.load(Ordering::SeqCst),
2195            2,
2196            "panic must trigger exactly one supervised restart, got {} factory calls",
2197            restart_count.load(Ordering::SeqCst)
2198        );
2199        assert_eq!(
2200            actor.count, 1,
2201            "restarted actor should keep the post-crash message count"
2202        );
2203        assert_eq!(
2204            final_count.load(Ordering::SeqCst),
2205            1,
2206            "restarted actor should process the queued post-crash message before abort"
2207        );
2208
2209        crate::test_complete!("supervised_actor_panic_restarts_under_restart_strategy");
2210    }
2211
2212    #[test]
2213    fn supervised_restart_window_expires_without_timer_driver() {
2214        use std::thread;
2215
2216        init_test("supervised_restart_window_expires_without_timer_driver");
2217
2218        let region_id = RegionId::new_for_test(0, 1);
2219        let cx = Cx::new(region_id, TaskId::new_for_test(1, 1), Budget::INFINITE);
2220        let mut supervisor =
2221            crate::supervision::Supervisor::new(crate::supervision::SupervisionStrategy::Restart(
2222                crate::supervision::RestartConfig::new(1, Duration::from_millis(2))
2223                    .with_backoff(crate::supervision::BackoffStrategy::None),
2224            ));
2225        let outcome = Outcome::Err(());
2226        let task_id = TaskId::new_for_test(2, 1);
2227
2228        let first = supervisor.on_failure(
2229            task_id,
2230            region_id,
2231            None,
2232            &outcome,
2233            supervised_restart_timestamp(&cx),
2234        );
2235        assert!(
2236            matches!(
2237                first,
2238                crate::supervision::SupervisionDecision::Restart { attempt: 1, .. }
2239            ),
2240            "first failure should allow a restart"
2241        );
2242
2243        thread::sleep(Duration::from_millis(5));
2244
2245        let second = supervisor.on_failure(
2246            task_id,
2247            region_id,
2248            None,
2249            &outcome,
2250            supervised_restart_timestamp(&cx),
2251        );
2252        assert!(
2253            matches!(
2254                second,
2255                crate::supervision::SupervisionDecision::Restart { attempt: 1, .. }
2256            ),
2257            "wall-clock fallback must let the restart window expire without a timer driver"
2258        );
2259
2260        crate::test_complete!("supervised_restart_window_expires_without_timer_driver");
2261    }
2262
2263    #[test]
2264    fn supervised_restart_delay_uses_explicit_timer_driver_without_ambient_cx() {
2265        init_test("supervised_restart_delay_uses_explicit_timer_driver_without_ambient_cx");
2266
2267        let clock = Arc::new(crate::time::VirtualClock::new());
2268        let timer = crate::time::TimerDriverHandle::with_virtual_clock(Arc::clone(&clock));
2269        let cx = Cx::new_with_drivers(
2270            RegionId::new_for_test(4, 0),
2271            TaskId::new_for_test(4, 0),
2272            Budget::INFINITE,
2273            None,
2274            None,
2275            None,
2276            Some(timer.clone()),
2277            None,
2278        );
2279        let mut wait = Box::pin(wait_supervised_restart_delay(&cx, Duration::from_millis(5)));
2280        let mut task_cx = Context::from_waker(Waker::noop());
2281
2282        assert!(matches!(
2283            Future::poll(wait.as_mut(), &mut task_cx),
2284            Poll::Pending
2285        ));
2286
2287        clock.advance(5_000_000);
2288        assert_eq!(
2289            timer.process_timers(),
2290            1,
2291            "restart delay must register with the explicit timer driver"
2292        );
2293
2294        assert!(matches!(
2295            Future::poll(wait.as_mut(), &mut task_cx),
2296            Poll::Ready(Outcome::Ok(()))
2297        ));
2298
2299        crate::test_complete!(
2300            "supervised_restart_delay_uses_explicit_timer_driver_without_ambient_cx"
2301        );
2302    }
2303
2304    #[test]
2305    fn supervised_actor_stop_prevents_restart_after_panic() {
2306        use std::sync::atomic::AtomicU32;
2307
2308        #[derive(Debug)]
2309        struct StopThenPanicActor;
2310
2311        impl Actor for StopThenPanicActor {
2312            type Message = ();
2313
2314            fn handle(
2315                &mut self,
2316                _cx: &Cx,
2317                _msg: (),
2318            ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2319                panic!("panic during shutdown");
2320            }
2321        }
2322
2323        init_test("supervised_actor_stop_prevents_restart_after_panic");
2324
2325        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2326        let region = runtime.state.create_root_region(Budget::INFINITE);
2327        let cx: Cx = Cx::for_testing();
2328        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2329
2330        let restart_count = Arc::new(AtomicU32::new(0));
2331        let rc = Arc::clone(&restart_count);
2332        let strategy = crate::supervision::SupervisionStrategy::Restart(
2333            crate::supervision::RestartConfig::new(3, Duration::from_secs(60))
2334                .with_backoff(crate::supervision::BackoffStrategy::None),
2335        );
2336
2337        let (mut handle, stored) = scope
2338            .spawn_supervised_actor(
2339                &mut runtime.state,
2340                &cx,
2341                move || {
2342                    rc.fetch_add(1, Ordering::Relaxed);
2343                    StopThenPanicActor
2344                },
2345                strategy,
2346                8,
2347            )
2348            .expect("spawn supervised actor");
2349        let task_id = handle.task_id();
2350        runtime.state.store_spawned_task(task_id, stored);
2351
2352        handle.try_send(()).expect("queue panic message");
2353        handle.stop();
2354
2355        runtime.scheduler.lock().schedule(task_id, 0);
2356        runtime.run_until_quiescent();
2357
2358        assert_eq!(
2359            restart_count.load(Ordering::SeqCst),
2360            1,
2361            "explicit stop must suppress supervised restarts"
2362        );
2363
2364        let join = futures_lite::future::block_on(handle.join(&cx));
2365        match join {
2366            Err(JoinError::Panicked(payload)) => {
2367                assert_eq!(
2368                    payload.message(),
2369                    "panic during shutdown",
2370                    "shutdown panic should surface without restarting"
2371                );
2372            }
2373            other => panic!("expected shutdown panic without restart, got {other:?}"),
2374        }
2375
2376        crate::test_complete!("supervised_actor_stop_prevents_restart_after_panic");
2377    }
2378
2379    #[test]
2380    fn supervised_actor_stop_during_restart_backoff_prevents_new_instance() {
2381        use std::sync::atomic::AtomicU32;
2382
2383        #[derive(Debug)]
2384        struct DelayedRestartActor {
2385            starts: Arc<AtomicU32>,
2386        }
2387
2388        impl Actor for DelayedRestartActor {
2389            type Message = ();
2390
2391            fn on_start(&mut self, _cx: &Cx) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2392                let starts = Arc::clone(&self.starts);
2393                Box::pin(async move {
2394                    starts.fetch_add(1, Ordering::Relaxed);
2395                })
2396            }
2397
2398            fn handle(
2399                &mut self,
2400                _cx: &Cx,
2401                _msg: (),
2402            ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2403                panic!("panic before delayed restart");
2404            }
2405        }
2406
2407        init_test("supervised_actor_stop_during_restart_backoff_prevents_new_instance");
2408
2409        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
2410        let region = runtime.state.create_root_region(Budget::INFINITE);
2411        let cx: Cx = Cx::for_testing();
2412        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2413
2414        let factory_count = Arc::new(AtomicU32::new(0));
2415        let starts = Arc::new(AtomicU32::new(0));
2416        let fc = Arc::clone(&factory_count);
2417        let starts_for_factory = Arc::clone(&starts);
2418        let strategy = crate::supervision::SupervisionStrategy::Restart(
2419            crate::supervision::RestartConfig::new(3, Duration::from_secs(60)).with_backoff(
2420                crate::supervision::BackoffStrategy::Fixed(Duration::from_secs(5)),
2421            ),
2422        );
2423
2424        let (mut handle, stored) = scope
2425            .spawn_supervised_actor(
2426                &mut runtime.state,
2427                &cx,
2428                move || {
2429                    fc.fetch_add(1, Ordering::Relaxed);
2430                    DelayedRestartActor {
2431                        starts: Arc::clone(&starts_for_factory),
2432                    }
2433                },
2434                strategy,
2435                8,
2436            )
2437            .expect("spawn supervised actor");
2438        let task_id = handle.task_id();
2439        runtime.state.store_spawned_task(task_id, stored);
2440
2441        handle.try_send(()).expect("queue panic message");
2442
2443        runtime.scheduler.lock().schedule(task_id, 0);
2444        runtime.run_until_idle();
2445        assert_eq!(
2446            runtime.pending_timer_count(),
2447            1,
2448            "supervised actor should be waiting on restart backoff"
2449        );
2450
2451        handle.stop();
2452        let report = runtime.run_with_auto_advance();
2453
2454        assert!(
2455            matches!(
2456                report.termination,
2457                crate::lab::AutoAdvanceTermination::Quiescent
2458            ),
2459            "runtime should quiesce after stop suppresses restart: {report:?}"
2460        );
2461        assert_eq!(
2462            factory_count.load(Ordering::SeqCst),
2463            1,
2464            "graceful stop during backoff must prevent a replacement actor from being constructed"
2465        );
2466        assert_eq!(
2467            starts.load(Ordering::SeqCst),
2468            1,
2469            "graceful stop during backoff must prevent restarted actor lifecycle hooks from running"
2470        );
2471
2472        let join = futures_lite::future::block_on(handle.join(&cx));
2473        match join {
2474            Err(JoinError::Panicked(payload)) => {
2475                assert_eq!(
2476                    payload.message(),
2477                    "panic before delayed restart",
2478                    "original panic should surface when restart is suppressed"
2479                );
2480            }
2481            other => panic!(
2482                "expected original panic when stop suppresses delayed restart, got {other:?}"
2483            ),
2484        }
2485
2486        crate::test_complete!("supervised_actor_stop_during_restart_backoff_prevents_new_instance");
2487    }
2488
2489    #[test]
2490    fn spawn_actor_panic_surfaces_as_task_outcome() {
2491        init_test("spawn_actor_panic_surfaces_as_task_outcome");
2492
2493        #[derive(Debug)]
2494        struct PanicActor;
2495
2496        impl Actor for PanicActor {
2497            type Message = ();
2498
2499            fn handle(
2500                &mut self,
2501                _cx: &Cx,
2502                _msg: (),
2503            ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2504                panic!("actor boom");
2505            }
2506        }
2507
2508        let mut state = RuntimeState::new();
2509        let root = state.create_root_region(Budget::INFINITE);
2510        let cx: Cx = Cx::for_testing();
2511        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2512
2513        let (mut handle, mut stored) = scope
2514            .spawn_actor(&mut state, &cx, PanicActor, 8)
2515            .expect("spawn actor");
2516        handle.try_send(()).expect("queue panic message");
2517
2518        let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2519        let waker = terminal_state_waker(Arc::clone(&handle.state), Arc::clone(&wake_count));
2520        let mut poll_cx = Context::from_waker(&waker);
2521        let join = std::pin::pin!(handle.join(&cx));
2522        let mut join = join;
2523        assert!(matches!(join.as_mut().poll(&mut poll_cx), Poll::Pending));
2524
2525        match stored.poll(&mut poll_cx) {
2526            Poll::Ready(Outcome::Panicked(payload)) => {
2527                assert_eq!(payload.message(), "actor boom", "panic payload preserved");
2528            }
2529            other => panic!("panicking actor task must return Outcome::Panicked: {other:?}"),
2530        }
2531        assert_eq!(wake_count.load(Ordering::Relaxed), 1);
2532
2533        match join.as_mut().poll(&mut poll_cx) {
2534            Poll::Ready(Err(JoinError::Panicked(payload))) => {
2535                assert_eq!(
2536                    payload.message(),
2537                    "actor boom",
2538                    "join preserves panic payload"
2539                );
2540            }
2541            other => panic!("join must surface actor panic: {other:?}"),
2542        }
2543
2544        crate::test_complete!("spawn_actor_panic_surfaces_as_task_outcome");
2545    }
2546
2547    #[test]
2548    fn spawn_supervised_actor_panic_surfaces_as_task_outcome() {
2549        init_test("spawn_supervised_actor_panic_surfaces_as_task_outcome");
2550
2551        #[derive(Debug)]
2552        struct PanicActor;
2553
2554        impl Actor for PanicActor {
2555            type Message = ();
2556
2557            fn handle(
2558                &mut self,
2559                _cx: &Cx,
2560                _msg: (),
2561            ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2562                panic!("supervised actor boom");
2563            }
2564        }
2565
2566        let mut state = RuntimeState::new();
2567        let root = state.create_root_region(Budget::INFINITE);
2568        let cx: Cx = Cx::for_testing();
2569        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2570
2571        let (mut handle, mut stored) = scope
2572            .spawn_supervised_actor(
2573                &mut state,
2574                &cx,
2575                || PanicActor,
2576                crate::supervision::SupervisionStrategy::Stop,
2577                8,
2578            )
2579            .expect("spawn supervised actor");
2580        handle.try_send(()).expect("queue panic message");
2581
2582        let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2583        let waker = terminal_state_waker(Arc::clone(&handle.state), Arc::clone(&wake_count));
2584        let mut poll_cx = Context::from_waker(&waker);
2585        let join = std::pin::pin!(handle.join(&cx));
2586        let mut join = join;
2587        assert!(matches!(join.as_mut().poll(&mut poll_cx), Poll::Pending));
2588
2589        match stored.poll(&mut poll_cx) {
2590            Poll::Ready(Outcome::Panicked(payload)) => {
2591                assert_eq!(
2592                    payload.message(),
2593                    "supervised actor boom",
2594                    "panic payload preserved"
2595                );
2596            }
2597            other => {
2598                panic!("panicking supervised actor task must return Outcome::Panicked: {other:?}")
2599            }
2600        }
2601        assert_eq!(wake_count.load(Ordering::Relaxed), 1);
2602
2603        match join.as_mut().poll(&mut poll_cx) {
2604            Poll::Ready(Err(JoinError::Panicked(payload))) => {
2605                assert_eq!(
2606                    payload.message(),
2607                    "supervised actor boom",
2608                    "join preserves panic payload"
2609                );
2610            }
2611            other => panic!("join must surface supervised actor panic: {other:?}"),
2612        }
2613
2614        crate::test_complete!("spawn_supervised_actor_panic_surfaces_as_task_outcome");
2615    }
2616
2617    #[test]
2618    fn supervised_restart_factory_panic_reaches_join_and_marks_stopped() {
2619        use std::sync::atomic::AtomicU32;
2620
2621        init_test("supervised_restart_factory_panic_reaches_join_and_marks_stopped");
2622
2623        #[derive(Debug)]
2624        struct PanicActor;
2625
2626        impl Actor for PanicActor {
2627            type Message = ();
2628
2629            fn handle(
2630                &mut self,
2631                _cx: &Cx,
2632                _msg: (),
2633            ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
2634                panic!("actor panic before restart factory");
2635            }
2636        }
2637
2638        let mut state = RuntimeState::new();
2639        let root = state.create_root_region(Budget::INFINITE);
2640        let cx: Cx = Cx::for_testing();
2641        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2642        let factory_calls = Arc::new(AtomicU32::new(0));
2643        let calls = Arc::clone(&factory_calls);
2644
2645        let (mut handle, mut stored) = scope
2646            .spawn_supervised_actor(
2647                &mut state,
2648                &cx,
2649                move || {
2650                    let attempt = calls.fetch_add(1, Ordering::Relaxed);
2651                    if attempt == 0 {
2652                        PanicActor
2653                    } else {
2654                        panic!("restart factory boom");
2655                    }
2656                },
2657                crate::supervision::SupervisionStrategy::Restart(
2658                    crate::supervision::RestartConfig::new(1, Duration::from_secs(60))
2659                        .with_backoff(crate::supervision::BackoffStrategy::None),
2660                ),
2661                8,
2662            )
2663            .expect("spawn supervised actor");
2664        assert_eq!(
2665            factory_calls.load(Ordering::Relaxed),
2666            0,
2667            "supervised actor factory must stay lazy until the stored task's first poll"
2668        );
2669        handle.try_send(()).expect("queue panic message");
2670
2671        let actor_ref = handle.sender();
2672        let wake_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2673        let waker = terminal_state_waker(Arc::clone(&handle.state), Arc::clone(&wake_count));
2674        let mut poll_cx = Context::from_waker(&waker);
2675        let join = std::pin::pin!(handle.join(&cx));
2676        let mut join = join;
2677        assert!(matches!(join.as_mut().poll(&mut poll_cx), Poll::Pending));
2678
2679        match stored.poll(&mut poll_cx) {
2680            Poll::Ready(Outcome::Panicked(payload)) => {
2681                assert_eq!(payload.message(), "restart factory boom");
2682            }
2683            other => panic!("restart factory panic must terminate the task: {other:?}"),
2684        }
2685
2686        assert_eq!(factory_calls.load(Ordering::Relaxed), 2);
2687        assert_eq!(wake_count.load(Ordering::Relaxed), 1);
2688        assert!(
2689            !actor_ref.is_alive(),
2690            "restart factory panic must publish the terminal actor state"
2691        );
2692
2693        match join.as_mut().poll(&mut poll_cx) {
2694            Poll::Ready(Err(JoinError::Panicked(payload))) => {
2695                assert_eq!(payload.message(), "restart factory boom");
2696            }
2697            other => panic!("join must preserve the restart factory panic: {other:?}"),
2698        }
2699
2700        crate::test_complete!("supervised_restart_factory_panic_reaches_join_and_marks_stopped");
2701    }
2702
2703    #[test]
2704    fn supervised_restart_delay_honors_cancellation() {
2705        init_test("supervised_restart_delay_honors_cancellation");
2706
2707        let cx = Cx::for_testing();
2708        cx.cancel_fast(crate::types::CancelKind::User);
2709
2710        let mut delay = std::pin::pin!(wait_supervised_restart_delay(
2711            &cx,
2712            std::time::Duration::from_secs(60),
2713        ));
2714        let first_poll =
2715            futures_lite::future::block_on(futures_lite::future::poll_once(&mut delay));
2716
2717        match first_poll {
2718            Some(Outcome::Err(JoinError::Cancelled(reason))) => {
2719                assert_eq!(reason.kind, crate::types::CancelKind::User);
2720            }
2721            other => panic!("expected immediate cancellation, got {other:?}"),
2722        }
2723
2724        crate::test_complete!("supervised_restart_delay_honors_cancellation");
2725    }
2726
2727    /// E2E: Deterministic replay — same seed produces same actor execution.
2728    #[test]
2729    fn actor_deterministic_replay() {
2730        fn run_scenario(seed: u64) -> u64 {
2731            let config = crate::lab::LabConfig::new(seed);
2732            let mut runtime = crate::lab::LabRuntime::new(config);
2733            let region = runtime.state.create_root_region(Budget::INFINITE);
2734            let cx: Cx = Cx::for_testing();
2735            let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
2736
2737            let (on_stop_count, started, stopped) = observable_state();
2738            let actor = ObservableCounter::new(on_stop_count.clone(), started, stopped);
2739
2740            let (handle, stored) = scope
2741                .spawn_actor(&mut runtime.state, &cx, actor, 32)
2742                .unwrap();
2743            let task_id = handle.task_id();
2744            runtime.state.store_spawned_task(task_id, stored);
2745
2746            for i in 1..=10 {
2747                handle.try_send(i).unwrap();
2748            }
2749            drop(handle);
2750
2751            runtime.scheduler.lock().schedule(task_id, 0);
2752            runtime.run_until_quiescent();
2753
2754            on_stop_count.load(Ordering::SeqCst)
2755        }
2756
2757        init_test("actor_deterministic_replay");
2758
2759        // Run the same scenario twice with the same seed
2760        let result1 = run_scenario(0xDEAD_BEEF);
2761        let result2 = run_scenario(0xDEAD_BEEF);
2762
2763        assert_eq!(
2764            result1, result2,
2765            "deterministic replay: same seed → same result"
2766        );
2767        assert_eq!(result1, 55, "sum of 1..=10");
2768
2769        crate::test_complete!("actor_deterministic_replay");
2770    }
2771
2772    // ---- ActorContext Tests ----
2773
2774    #[test]
2775    fn actor_context_self_reference() {
2776        init_test("actor_context_self_reference");
2777
2778        let mut state = RuntimeState::new();
2779        let root = state.create_root_region(Budget::INFINITE);
2780        let cx: Cx = Cx::for_testing();
2781        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
2782
2783        let (handle, stored) = scope
2784            .spawn_actor(&mut state, &cx, Counter::new(), 32)
2785            .unwrap();
2786        state.store_spawned_task(handle.task_id(), stored);
2787
2788        // Create an ActorContext using the handle's sender
2789        let actor_ref = handle.sender();
2790        let actor_id = handle.actor_id();
2791        let ctx: ActorContext<'_, u64> = ActorContext::new(&cx, actor_ref, actor_id, None);
2792
2793        // Test self_actor_id() - doesn't require Clone
2794        assert_eq!(ctx.self_actor_id(), actor_id);
2795        assert_eq!(ctx.actor_id(), actor_id);
2796
2797        crate::test_complete!("actor_context_self_reference");
2798    }
2799
2800    #[test]
2801    fn actor_context_child_management() {
2802        init_test("actor_context_child_management");
2803
2804        let cx: Cx = Cx::for_testing();
2805        let (sender, _receiver) = mpsc::channel::<u64>(32);
2806        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2807        let actor_ref = ActorRef {
2808            actor_id,
2809            sender,
2810            state: Arc::new(ActorStateCell::new(ActorState::Running)),
2811        };
2812
2813        let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2814
2815        // Initially no children
2816        assert!(!ctx.has_children());
2817        assert_eq!(ctx.child_count(), 0);
2818        assert!(ctx.children().is_empty());
2819
2820        // Register children
2821        let child1 = ActorId::from_task(TaskId::new_for_test(2, 1));
2822        let child2 = ActorId::from_task(TaskId::new_for_test(3, 1));
2823
2824        ctx.register_child(child1);
2825        assert!(ctx.has_children());
2826        assert_eq!(ctx.child_count(), 1);
2827
2828        ctx.register_child(child2);
2829        assert_eq!(ctx.child_count(), 2);
2830
2831        // Unregister child
2832        assert!(ctx.unregister_child(child1));
2833        assert_eq!(ctx.child_count(), 1);
2834
2835        // Unregistering non-existent child returns false
2836        assert!(!ctx.unregister_child(child1));
2837
2838        crate::test_complete!("actor_context_child_management");
2839    }
2840
2841    #[test]
2842    fn actor_context_stopping() {
2843        init_test("actor_context_stopping");
2844
2845        let cx: Cx = Cx::for_testing();
2846        let (sender, _receiver) = mpsc::channel::<u64>(32);
2847        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2848        let actor_ref = ActorRef {
2849            actor_id,
2850            sender,
2851            state: Arc::new(ActorStateCell::new(ActorState::Running)),
2852        };
2853
2854        let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2855
2856        // Initially not stopping
2857        assert!(!ctx.is_stopping());
2858        assert!(ctx.checkpoint().is_ok());
2859
2860        // Request stop
2861        ctx.stop_self();
2862        assert!(ctx.is_stopping());
2863        assert!(ctx.checkpoint().is_err());
2864        assert!(cx.checkpoint().is_ok());
2865        assert!(ctx.is_cancel_requested());
2866
2867        crate::test_complete!("actor_context_stopping");
2868    }
2869
2870    #[test]
2871    fn actor_context_parent_none() {
2872        init_test("actor_context_parent_none");
2873
2874        let cx: Cx = Cx::for_testing();
2875        let (sender, _receiver) = mpsc::channel::<u64>(32);
2876        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2877        let actor_ref = ActorRef {
2878            actor_id,
2879            sender,
2880            state: Arc::new(ActorStateCell::new(ActorState::Running)),
2881        };
2882
2883        let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2884
2885        // Root actor has no parent
2886        assert!(!ctx.has_parent());
2887        assert!(ctx.parent().is_none());
2888
2889        crate::test_complete!("actor_context_parent_none");
2890    }
2891
2892    #[test]
2893    fn actor_context_cx_delegation() {
2894        init_test("actor_context_cx_delegation");
2895
2896        let cx: Cx = Cx::for_testing();
2897        let (sender, _receiver) = mpsc::channel::<u64>(32);
2898        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2899        let actor_ref = ActorRef {
2900            actor_id,
2901            sender,
2902            state: Arc::new(ActorStateCell::new(ActorState::Running)),
2903        };
2904
2905        let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2906
2907        // Test Cx delegation via Deref
2908        let _budget = ctx.budget();
2909        ctx.trace("test_event");
2910
2911        // Test cx() accessor
2912        let _cx_ref = ctx.cx();
2913
2914        crate::test_complete!("actor_context_cx_delegation");
2915    }
2916
2917    #[test]
2918    fn actor_context_debug() {
2919        init_test("actor_context_debug");
2920
2921        let cx: Cx = Cx::for_testing();
2922        let (sender, _receiver) = mpsc::channel::<u64>(32);
2923        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
2924        let actor_ref = ActorRef {
2925            actor_id,
2926            sender,
2927            state: Arc::new(ActorStateCell::new(ActorState::Running)),
2928        };
2929
2930        let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
2931
2932        // Debug formatting should work
2933        let debug_str = format!("{ctx:?}");
2934        assert!(debug_str.contains("ActorContext"));
2935        assert!(debug_str.contains("actor_id"));
2936
2937        crate::test_complete!("actor_context_debug");
2938    }
2939
2940    // ---- Invariant Tests ----
2941
2942    /// Invariant: `ActorStateCell` encode/decode roundtrips correctly for all
2943    /// valid states, and unknown u8 values map to `Stopped` (fail-safe).
2944    #[test]
2945    fn actor_state_cell_encode_decode_roundtrip() {
2946        init_test("actor_state_cell_encode_decode_roundtrip");
2947
2948        let states = [
2949            ActorState::Created,
2950            ActorState::Running,
2951            ActorState::Stopping,
2952            ActorState::Stopped,
2953        ];
2954
2955        for &state in &states {
2956            let cell = ActorStateCell::new(state);
2957            let loaded = cell.load();
2958            crate::assert_with_log!(loaded == state, "roundtrip", state, loaded);
2959        }
2960
2961        // Unknown values (4+) should map to Stopped (fail-safe).
2962        for raw in 4_u8..=10 {
2963            let decoded = ActorStateCell::decode(raw);
2964            let is_stopped = decoded == ActorState::Stopped;
2965            crate::assert_with_log!(is_stopped, "unknown u8 -> Stopped", true, is_stopped);
2966        }
2967
2968        crate::test_complete!("actor_state_cell_encode_decode_roundtrip");
2969    }
2970
2971    /// Invariant: `MailboxConfig::default()` has documented capacity and
2972    /// backpressure enabled.
2973    #[test]
2974    fn mailbox_config_defaults() {
2975        init_test("mailbox_config_defaults");
2976
2977        let config = MailboxConfig::default();
2978        crate::assert_with_log!(
2979            config.capacity == DEFAULT_MAILBOX_CAPACITY,
2980            "default capacity",
2981            DEFAULT_MAILBOX_CAPACITY,
2982            config.capacity
2983        );
2984        crate::assert_with_log!(
2985            config.backpressure,
2986            "backpressure enabled by default",
2987            true,
2988            config.backpressure
2989        );
2990
2991        let custom = MailboxConfig::with_capacity(8);
2992        crate::assert_with_log!(
2993            custom.capacity == 8,
2994            "custom capacity",
2995            8usize,
2996            custom.capacity
2997        );
2998        crate::assert_with_log!(
2999            custom.backpressure,
3000            "with_capacity enables backpressure",
3001            true,
3002            custom.backpressure
3003        );
3004
3005        crate::test_complete!("mailbox_config_defaults");
3006    }
3007
3008    /// Invariant: `try_send` on a full mailbox returns an error without
3009    /// blocking, and the message is recoverable from the error.
3010    #[test]
3011    fn actor_try_send_full_mailbox_returns_error() {
3012        init_test("actor_try_send_full_mailbox_returns_error");
3013
3014        let mut state = RuntimeState::new();
3015        let root = state.create_root_region(Budget::INFINITE);
3016        let cx: Cx = Cx::for_testing();
3017        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
3018
3019        // Create actor with capacity=2 mailbox.
3020        let (handle, stored) = scope
3021            .spawn_actor(&mut state, &cx, Counter::new(), 2)
3022            .unwrap();
3023        state.store_spawned_task(handle.task_id(), stored);
3024
3025        // Fill the mailbox.
3026        let ok1 = handle.try_send(1).is_ok();
3027        crate::assert_with_log!(ok1, "first send ok", true, ok1);
3028        let ok2 = handle.try_send(2).is_ok();
3029        crate::assert_with_log!(ok2, "second send ok", true, ok2);
3030
3031        // Third send should fail — mailbox full.
3032        let result = handle.try_send(3);
3033        let is_full = result.is_err();
3034        crate::assert_with_log!(is_full, "third send fails (full)", true, is_full);
3035
3036        crate::test_complete!("actor_try_send_full_mailbox_returns_error");
3037    }
3038
3039    /// Invariant: `ActorContext` with a parent supervisor set exposes it
3040    /// and reports `has_parent() == true`.
3041    #[test]
3042    fn actor_context_with_parent_supervisor() {
3043        init_test("actor_context_with_parent_supervisor");
3044
3045        let cx: Cx = Cx::for_testing();
3046
3047        // Create parent supervisor channel.
3048        let (parent_sender, _parent_receiver) = mpsc::channel::<SupervisorMessage>(8);
3049        let parent_id = ActorId::from_task(TaskId::new_for_test(10, 1));
3050        let parent_ref = ActorRef {
3051            actor_id: parent_id,
3052            sender: parent_sender,
3053            state: Arc::new(ActorStateCell::new(ActorState::Running)),
3054        };
3055
3056        // Create child actor context with parent.
3057        let (child_sender, _child_receiver) = mpsc::channel::<u64>(32);
3058        let child_id = ActorId::from_task(TaskId::new_for_test(20, 1));
3059        let child_ref = ActorRef {
3060            actor_id: child_id,
3061            sender: child_sender,
3062            state: Arc::new(ActorStateCell::new(ActorState::Running)),
3063        };
3064
3065        let ctx = ActorContext::new(&cx, child_ref, child_id, Some(parent_ref));
3066
3067        let has_parent = ctx.has_parent();
3068        crate::assert_with_log!(has_parent, "has parent", true, has_parent);
3069
3070        let parent = ctx.parent().expect("parent should be Some");
3071        let parent_id_matches = parent.actor_id() == parent_id;
3072        crate::assert_with_log!(
3073            parent_id_matches,
3074            "parent id matches",
3075            true,
3076            parent_id_matches
3077        );
3078
3079        crate::test_complete!("actor_context_with_parent_supervisor");
3080    }
3081
3082    // ---- Pure Data Type Tests (no runtime needed) ----
3083
3084    #[test]
3085    fn actor_id_debug_format() {
3086        let id = ActorId::from_task(TaskId::new_for_test(5, 3));
3087        let dbg = format!("{id:?}");
3088        assert!(dbg.contains("ActorId"), "{dbg}");
3089    }
3090
3091    #[test]
3092    fn actor_id_display_delegates_to_task_id() {
3093        let tid = TaskId::new_for_test(7, 2);
3094        let aid = ActorId::from_task(tid);
3095        assert_eq!(format!("{aid}"), format!("{tid}"));
3096    }
3097
3098    #[test]
3099    fn actor_id_from_task_roundtrip() {
3100        let tid = TaskId::new_for_test(3, 1);
3101        let aid = ActorId::from_task(tid);
3102        assert_eq!(aid.task_id(), tid);
3103    }
3104
3105    #[test]
3106    fn actor_id_copy_clone() {
3107        let id = ActorId::from_task(TaskId::new_for_test(1, 1));
3108        let copied = id; // Copy
3109        let cloned = id;
3110        assert_eq!(id, copied);
3111        assert_eq!(id, cloned);
3112    }
3113
3114    #[test]
3115    fn actor_id_hash_consistency() {
3116        use crate::util::DetHasher;
3117        use std::hash::{Hash, Hasher};
3118
3119        let id1 = ActorId::from_task(TaskId::new_for_test(4, 2));
3120        let id2 = ActorId::from_task(TaskId::new_for_test(4, 2));
3121        assert_eq!(id1, id2);
3122
3123        let mut h1 = DetHasher::default();
3124        let mut h2 = DetHasher::default();
3125        id1.hash(&mut h1);
3126        id2.hash(&mut h2);
3127        assert_eq!(h1.finish(), h2.finish(), "equal IDs must hash equal");
3128    }
3129
3130    #[test]
3131    fn actor_state_debug_all_variants() {
3132        for (state, expected) in [
3133            (ActorState::Created, "Created"),
3134            (ActorState::Running, "Running"),
3135            (ActorState::Stopping, "Stopping"),
3136            (ActorState::Stopped, "Stopped"),
3137        ] {
3138            let dbg = format!("{state:?}");
3139            assert_eq!(dbg, expected, "ActorState::{expected}");
3140        }
3141    }
3142
3143    #[test]
3144    fn actor_state_clone_copy_eq() {
3145        let s = ActorState::Running;
3146        let copied = s;
3147        let cloned = s;
3148        assert_eq!(s, copied);
3149        assert_eq!(s, cloned);
3150    }
3151
3152    #[test]
3153    fn actor_state_exhaustive_inequality() {
3154        let all = [
3155            ActorState::Created,
3156            ActorState::Running,
3157            ActorState::Stopping,
3158            ActorState::Stopped,
3159        ];
3160        for (i, a) in all.iter().enumerate() {
3161            for (j, b) in all.iter().enumerate() {
3162                if i == j {
3163                    assert_eq!(a, b);
3164                } else {
3165                    assert_ne!(a, b);
3166                }
3167            }
3168        }
3169    }
3170
3171    #[test]
3172    fn actor_state_cell_sequential_transitions() {
3173        let cell = ActorStateCell::new(ActorState::Created);
3174        assert_eq!(cell.load(), ActorState::Created);
3175
3176        cell.store(ActorState::Running);
3177        assert_eq!(cell.load(), ActorState::Running);
3178
3179        cell.store(ActorState::Stopping);
3180        assert_eq!(cell.load(), ActorState::Stopping);
3181
3182        cell.store(ActorState::Stopped);
3183        assert_eq!(cell.load(), ActorState::Stopped);
3184    }
3185
3186    #[test]
3187    fn supervised_restart_commit_preserves_a_winning_stop() {
3188        let restart_wins = ActorStateCell::new(ActorState::Running);
3189        assert!(try_commit_supervised_restart(&restart_wins));
3190        assert_eq!(restart_wins.load(), ActorState::Created);
3191
3192        let stop_wins = ActorStateCell::new(ActorState::Running);
3193        stop_wins.store(ActorState::Stopping);
3194        assert!(
3195            !try_commit_supervised_restart(&stop_wins),
3196            "restart commit must fail after a concurrent stop linearizes"
3197        );
3198        assert_eq!(
3199            stop_wins.load(),
3200            ActorState::Stopping,
3201            "failed restart commit must not overwrite the stop request"
3202        );
3203    }
3204
3205    #[test]
3206    fn supervisor_message_debug_child_failed() {
3207        let msg = SupervisorMessage::ChildFailed {
3208            child_id: ActorId::from_task(TaskId::new_for_test(1, 1)),
3209            reason: "panicked".to_string(),
3210        };
3211        let dbg = format!("{msg:?}");
3212        assert!(dbg.contains("ChildFailed"), "{dbg}");
3213        assert!(dbg.contains("panicked"), "{dbg}");
3214    }
3215
3216    #[test]
3217    fn supervisor_message_debug_child_stopped() {
3218        let msg = SupervisorMessage::ChildStopped {
3219            child_id: ActorId::from_task(TaskId::new_for_test(2, 1)),
3220        };
3221        let dbg = format!("{msg:?}");
3222        assert!(dbg.contains("ChildStopped"), "{dbg}");
3223    }
3224
3225    #[test]
3226    fn supervisor_message_clone() {
3227        let msg = SupervisorMessage::ChildFailed {
3228            child_id: ActorId::from_task(TaskId::new_for_test(1, 1)),
3229            reason: "boom".to_string(),
3230        };
3231        let cloned = msg.clone();
3232        let (a, b) = (format!("{msg:?}"), format!("{cloned:?}"));
3233        assert_eq!(a, b);
3234    }
3235
3236    #[test]
3237    fn supervised_outcome_debug_all_variants() {
3238        let variants: Vec<SupervisedOutcome> = vec![
3239            SupervisedOutcome::Stopped,
3240            SupervisedOutcome::RestartBudgetExhausted { total_restarts: 5 },
3241            SupervisedOutcome::Escalated,
3242        ];
3243        for v in &variants {
3244            let dbg = format!("{v:?}");
3245            assert!(!dbg.is_empty());
3246        }
3247        assert!(format!("{variants0:?}", variants0 = variants[0]).contains("Stopped"));
3248        assert!(format!("{variants1:?}", variants1 = variants[1]).contains('5'));
3249        assert!(format!("{variants2:?}", variants2 = variants[2]).contains("Escalated"));
3250    }
3251
3252    #[test]
3253    fn mailbox_config_debug_clone_copy() {
3254        let cfg = MailboxConfig::default();
3255        let dbg = format!("{cfg:?}");
3256        assert!(dbg.contains("MailboxConfig"), "{dbg}");
3257        assert!(dbg.contains("64"), "{dbg}");
3258
3259        let copied = cfg;
3260        let cloned = cfg;
3261        assert_eq!(copied.capacity, cfg.capacity);
3262        assert_eq!(cloned.backpressure, cfg.backpressure);
3263    }
3264
3265    #[test]
3266    fn mailbox_config_zero_capacity() {
3267        let cfg = MailboxConfig::with_capacity(0);
3268        assert_eq!(cfg.capacity, 0);
3269        assert!(cfg.backpressure);
3270    }
3271
3272    #[test]
3273    fn mailbox_config_max_capacity() {
3274        let cfg = MailboxConfig::with_capacity(usize::MAX);
3275        assert_eq!(cfg.capacity, usize::MAX);
3276    }
3277
3278    #[test]
3279    fn default_mailbox_capacity_is_64() {
3280        assert_eq!(DEFAULT_MAILBOX_CAPACITY, 64);
3281    }
3282
3283    #[test]
3284    fn actor_context_duplicate_child_registration() {
3285        let cx: Cx = Cx::for_testing();
3286        let (sender, _receiver) = mpsc::channel::<u64>(32);
3287        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3288        let actor_ref = ActorRef {
3289            actor_id,
3290            sender,
3291            state: Arc::new(ActorStateCell::new(ActorState::Running)),
3292        };
3293
3294        let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3295        let child = ActorId::from_task(TaskId::new_for_test(2, 1));
3296
3297        ctx.register_child(child);
3298        ctx.register_child(child); // duplicate
3299        assert_eq!(ctx.child_count(), 2, "register_child does not dedup");
3300
3301        // Unregister removes first occurrence
3302        assert!(ctx.unregister_child(child));
3303        assert_eq!(ctx.child_count(), 1, "one copy remains");
3304        assert!(ctx.unregister_child(child));
3305        assert_eq!(ctx.child_count(), 0);
3306        assert!(!ctx.unregister_child(child), "nothing left to remove");
3307    }
3308
3309    #[test]
3310    fn actor_context_stop_self_is_idempotent() {
3311        let cx: Cx = Cx::for_testing();
3312        let (sender, _receiver) = mpsc::channel::<u64>(32);
3313        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3314        let actor_ref = ActorRef {
3315            actor_id,
3316            sender,
3317            state: Arc::new(ActorStateCell::new(ActorState::Running)),
3318        };
3319
3320        let mut ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3321        ctx.stop_self();
3322        assert!(ctx.is_stopping());
3323        ctx.stop_self(); // idempotent
3324        assert!(ctx.is_stopping());
3325    }
3326
3327    #[test]
3328    fn actor_context_self_ref_returns_working_ref() {
3329        let cx: Cx = Cx::for_testing();
3330        let (sender, _receiver) = mpsc::channel::<u64>(32);
3331        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3332        let actor_ref = ActorRef {
3333            actor_id,
3334            sender,
3335            state: Arc::new(ActorStateCell::new(ActorState::Running)),
3336        };
3337
3338        let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3339        let self_ref = ctx.self_ref();
3340        assert_eq!(self_ref.actor_id(), actor_id);
3341        assert!(self_ref.is_alive());
3342    }
3343
3344    #[test]
3345    fn actor_context_deadline_reflects_budget() {
3346        let cx: Cx = Cx::for_testing();
3347        let (sender, _receiver) = mpsc::channel::<u64>(32);
3348        let actor_id = ActorId::from_task(TaskId::new_for_test(1, 1));
3349        let actor_ref = ActorRef {
3350            actor_id,
3351            sender,
3352            state: Arc::new(ActorStateCell::new(ActorState::Running)),
3353        };
3354
3355        let ctx = ActorContext::new(&cx, actor_ref, actor_id, None);
3356        // for_testing() Cx has INFINITE budget, which has no deadline
3357        assert!(ctx.deadline().is_none());
3358    }
3359
3360    #[test]
3361    fn actor_handle_debug() {
3362        let mut state = RuntimeState::new();
3363        let root = state.create_root_region(Budget::INFINITE);
3364        let cx: Cx = Cx::for_testing();
3365        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
3366
3367        let (handle, stored) = scope
3368            .spawn_actor(&mut state, &cx, Counter::new(), 32)
3369            .unwrap();
3370        state.store_spawned_task(handle.task_id(), stored);
3371
3372        let dbg = format!("{handle:?}");
3373        assert!(dbg.contains("ActorHandle"), "{dbg}");
3374    }
3375
3376    #[test]
3377    fn actor_handle_second_join_fails_closed() {
3378        init_test("actor_handle_second_join_fails_closed");
3379
3380        let mut runtime = crate::lab::LabRuntime::new(crate::lab::LabConfig::default());
3381        let region = runtime.state.create_root_region(Budget::INFINITE);
3382        let cx = Cx::for_testing();
3383        let scope = crate::cx::Scope::<FailFast>::new(region, Budget::INFINITE);
3384
3385        let (mut handle, stored) = scope
3386            .spawn_actor(&mut runtime.state, &cx, Counter::new(), 32)
3387            .unwrap();
3388        let task_id = handle.task_id();
3389        runtime.state.store_spawned_task(task_id, stored);
3390
3391        handle.stop();
3392        runtime.scheduler.lock().schedule(task_id, 0);
3393        runtime.run_until_quiescent();
3394        assert!(handle.is_finished(), "stopped actor should report finished");
3395
3396        let final_state = futures_lite::future::block_on(handle.join(&cx)).expect("first join");
3397        assert_eq!(final_state.count, 0, "join should return final actor state");
3398
3399        let second = futures_lite::future::block_on(handle.join(&cx));
3400        assert!(
3401            matches!(second, Err(JoinError::PolledAfterCompletion)),
3402            "second join must fail closed, got {second:?}"
3403        );
3404
3405        crate::test_complete!("actor_handle_second_join_fails_closed");
3406    }
3407
3408    #[test]
3409    fn actor_join_future_closed_inner_maps_to_cancelled_reason() {
3410        init_test("actor_join_future_closed_inner_maps_to_cancelled_reason");
3411
3412        let (result_tx, mut result_rx) =
3413            crate::channel::oneshot::channel::<Result<Counter, JoinError>>();
3414        drop(result_tx);
3415        let mut terminal_state = false;
3416        let poll_result = {
3417            let mut join = std::pin::pin!(actor_join_future_from_receiver::<Counter>(
3418                &mut result_rx,
3419                &mut terminal_state,
3420            ));
3421            let waker = counting_waker(Arc::new(std::sync::atomic::AtomicUsize::new(0)));
3422            let mut poll_cx = Context::from_waker(&waker);
3423            join.as_mut().poll(&mut poll_cx)
3424        };
3425
3426        match poll_result {
3427            Poll::Ready(Err(JoinError::Cancelled(reason))) => {
3428                assert_eq!(reason.kind, crate::types::CancelKind::User);
3429                assert_eq!(
3430                    reason.message.as_deref(),
3431                    Some("join channel closed"),
3432                    "closed inner oneshot should surface the explicit join-channel reason"
3433                );
3434            }
3435            other => panic!("closed inner join future must map to Cancelled, got {other:?}"),
3436        }
3437
3438        assert!(
3439            terminal_state,
3440            "closed join future should mark terminal state"
3441        );
3442        crate::test_complete!("actor_join_future_closed_inner_maps_to_cancelled_reason");
3443    }
3444
3445    #[test]
3446    fn actor_join_future_repoll_fails_before_inner_polled_after_completion() {
3447        init_test("actor_join_future_repoll_fails_before_inner_polled_after_completion");
3448
3449        let (result_tx, mut result_rx) =
3450            crate::channel::oneshot::channel::<Result<Counter, JoinError>>();
3451        let cx: Cx = Cx::for_testing();
3452        result_tx
3453            .send(&cx, Ok(Counter::new()))
3454            .expect("seed join result");
3455        let mut terminal_state = false;
3456        let (first_poll, second_poll) = {
3457            let mut join = std::pin::pin!(actor_join_future_from_receiver::<Counter>(
3458                &mut result_rx,
3459                &mut terminal_state,
3460            ));
3461            let waker = counting_waker(Arc::new(std::sync::atomic::AtomicUsize::new(0)));
3462            let mut poll_cx = Context::from_waker(&waker);
3463            let first_poll = join.as_mut().poll(&mut poll_cx);
3464            let second_poll = join.as_mut().poll(&mut poll_cx);
3465            (first_poll, second_poll)
3466        };
3467
3468        match first_poll {
3469            Poll::Ready(Ok(actor)) => {
3470                assert_eq!(actor.count, 0, "seeded actor state should round-trip");
3471            }
3472            other => panic!("first poll should return actor state, got {other:?}"),
3473        }
3474
3475        assert!(terminal_state, "successful join should mark terminal state");
3476
3477        match second_poll {
3478            Poll::Ready(Err(JoinError::PolledAfterCompletion)) => {}
3479            other => panic!(
3480                "re-poll should fail closed before the inner oneshot can return PolledAfterCompletion, got {other:?}"
3481            ),
3482        }
3483
3484        crate::test_complete!(
3485            "actor_join_future_repoll_fails_before_inner_polled_after_completion"
3486        );
3487    }
3488
3489    #[test]
3490    fn actor_ref_debug() {
3491        let mut state = RuntimeState::new();
3492        let root = state.create_root_region(Budget::INFINITE);
3493        let cx: Cx = Cx::for_testing();
3494        let scope = crate::cx::Scope::<FailFast>::new(root, Budget::INFINITE);
3495
3496        let (handle, stored) = scope
3497            .spawn_actor(&mut state, &cx, Counter::new(), 32)
3498            .unwrap();
3499        state.store_spawned_task(handle.task_id(), stored);
3500
3501        let actor_ref = handle.sender();
3502        let dbg = format!("{actor_ref:?}");
3503        assert!(dbg.contains("ActorRef"), "{dbg}");
3504    }
3505
3506    #[test]
3507    fn actor_state_cell_debug() {
3508        let cell = ActorStateCell::new(ActorState::Running);
3509        let dbg = format!("{cell:?}");
3510        assert!(dbg.contains("ActorStateCell"), "{dbg}");
3511    }
3512
3513    #[test]
3514    fn actor_id_clone_copy_eq_hash() {
3515        use std::collections::HashSet;
3516
3517        let id = ActorId::from_task(TaskId::new_for_test(1, 0));
3518        let dbg = format!("{id:?}");
3519        assert!(dbg.contains("ActorId"));
3520
3521        let id2 = id;
3522        assert_eq!(id, id2);
3523
3524        // Copy
3525        let id3 = id;
3526        assert_eq!(id, id3);
3527
3528        // Hash
3529        let mut set = HashSet::new();
3530        set.insert(id);
3531        set.insert(ActorId::from_task(TaskId::new_for_test(2, 0)));
3532        assert_eq!(set.len(), 2);
3533    }
3534
3535    #[test]
3536    fn actor_state_debug_clone_copy_eq() {
3537        let s = ActorState::Running;
3538        let dbg = format!("{s:?}");
3539        assert!(dbg.contains("Running"));
3540
3541        let s2 = s;
3542        assert_eq!(s, s2);
3543
3544        let s3 = s;
3545        assert_eq!(s, s3);
3546
3547        assert_ne!(ActorState::Created, ActorState::Stopped);
3548    }
3549
3550    #[test]
3551    fn mailbox_config_debug_clone_copy_default() {
3552        let c = MailboxConfig::default();
3553        let dbg = format!("{c:?}");
3554        assert!(dbg.contains("MailboxConfig"));
3555
3556        let c2 = c;
3557        assert_eq!(c2.capacity, c.capacity);
3558        assert_eq!(c2.backpressure, c.backpressure);
3559
3560        // Copy
3561        let c3 = c;
3562        assert_eq!(c3.capacity, c.capacity);
3563    }
3564}
3565
3566// ============================================================================
3567// Conformance Tests
3568// ============================================================================
3569
3570#[cfg(test)]
3571#[path = "actor_conformance_tests.rs"]
3572mod actor_conformance_tests;
3573
3574#[cfg(test)]
3575mod conformance_integration {
3576    use super::actor_conformance_tests::{ActorConformanceHarness, TestVerdict};
3577
3578    #[test]
3579    fn actor_conformance_suite() {
3580        crate::test_utils::init_test_logging();
3581
3582        let mut harness = ActorConformanceHarness::new();
3583
3584        // Run the full conformance test suite
3585        let results = harness.run_full_suite();
3586
3587        let mut failures = Vec::new();
3588        let mut passes = 0;
3589
3590        for result in results {
3591            match result.verdict {
3592                TestVerdict::Pass => {
3593                    passes += 1;
3594                }
3595                TestVerdict::Fail(reason) => {
3596                    failures.push(format!("{}: {}", result.test_name, reason));
3597                }
3598            }
3599        }
3600
3601        assert!(
3602            failures.is_empty(),
3603            "Actor conformance failures:\n{}",
3604            failures.join("\n")
3605        );
3606
3607        assert!(
3608            passes > 0,
3609            "No conformance tests passed - harness may be broken"
3610        );
3611
3612        crate::test_complete!("actor_conformance_suite");
3613    }
3614}