Skip to main content

autoagents_core/
environment.rs

1use crate::error::Error;
2use crate::runtime::manager::RuntimeManager;
3use crate::runtime::{Runtime, RuntimeError};
4use crate::utils::BoxEventStream;
5use autoagents_protocol::{Event, RuntimeID};
6use futures_util::FutureExt;
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::task::JoinHandle;
10
11/// Errors emitted when managing runtimes and consuming event receivers
12#[derive(Debug, thiserror::Error)]
13pub enum EnvironmentError {
14    #[error("Runtime not found: {0}")]
15    RuntimeNotFound(RuntimeID),
16
17    #[error("No default runtime registered")]
18    NoDefaultRuntime,
19
20    #[error("Environment is already running")]
21    AlreadyRunning,
22
23    #[error("Runtime error: {0}")]
24    RuntimeError(#[from] Box<RuntimeError>),
25
26    #[error("Error when consuming receiver")]
27    EventError,
28
29    #[error("Run task join error: {0}")]
30    JoinError(#[from] tokio::task::JoinError),
31
32    #[error("Finished run task result was not yet available")]
33    RunResultNotReady,
34}
35
36/// Configuration for the process environment that owns one or more runtimes.
37#[derive(Clone)]
38pub struct EnvironmentConfig {
39    pub working_dir: PathBuf,
40}
41
42impl Default for EnvironmentConfig {
43    fn default() -> Self {
44        Self {
45            working_dir: std::env::current_dir().unwrap_or_default(),
46        }
47    }
48}
49
50/// High-level container that owns one or more runtimes, exposes a unified
51/// event receiver, and provides lifecycle helpers for running and shutting down
52/// the underlying actor system.
53///
54/// ## Run lifecycle state machine
55///
56/// An internal [`RuntimeLaunchState`] records how runtimes were last started:
57///
58/// - **`Idle`** — no active launch mode; safe to call `run()` or `run_background()`.
59/// - **`Managed`** — [`run`](Self::run) spawned a join handle tracked by this environment.
60///   Await it with [`wait`](Self::wait) or stop with [`shutdown`](Self::shutdown).
61/// - **`Background`** — [`run_background`](Self::run_background) started runtimes without storing
62///   a handle on the environment. Call [`shutdown`](Self::shutdown) before `run()`.
63///
64/// `run()` and `run_background()` both call [`reconcile_finished_managed_launch`](Self::reconcile_finished_managed_launch)
65/// first. When a managed run task finished without `wait()` or `shutdown()`, that helper joins the
66/// stale handle and surfaces any runtime or join error before a new launch proceeds.
67///
68/// [`wait`](Self::wait) temporarily takes the managed join handle. If the future is dropped early
69/// (for example when another branch wins in `tokio::select!`), [`RestoreRunHandleOnDrop`] puts the
70/// handle back so a later `shutdown()` can still stop runtimes and join the task. A successful
71/// `wait()` clears the handle and returns the launch state to `Idle`.
72pub struct Environment {
73    config: EnvironmentConfig,
74    runtime_manager: Arc<RuntimeManager>,
75    default_runtime: Option<RuntimeID>,
76    handle: Option<JoinHandle<Result<(), RuntimeError>>>,
77    launch_state: RuntimeLaunchState,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81enum RuntimeLaunchState {
82    /// No launch in progress; `run()` and `run_background()` are allowed.
83    #[default]
84    Idle,
85    /// [`Environment::run`] spawned a managed join handle stored on the environment.
86    Managed,
87    /// [`Environment::run_background`] started runtimes without a stored join handle.
88    Background,
89}
90
91impl Environment {
92    /// Create a new environment with optional configuration.
93    pub fn new(config: Option<EnvironmentConfig>) -> Self {
94        let config = config.unwrap_or_default();
95        let runtime_manager = Arc::new(RuntimeManager::new());
96
97        Self {
98            config,
99            runtime_manager,
100            default_runtime: None,
101            handle: None,
102            launch_state: RuntimeLaunchState::Idle,
103        }
104    }
105
106    /// Register a runtime with this environment and make it the default if none
107    /// is set yet.
108    pub async fn register_runtime(&mut self, runtime: Arc<dyn Runtime>) -> Result<(), Error> {
109        self.runtime_manager
110            .register_runtime(runtime.clone())
111            .await?;
112        if self.default_runtime.is_none() {
113            self.default_runtime = Some(runtime.id());
114        }
115        Ok(())
116    }
117
118    /// Access the environment configuration.
119    pub fn config(&self) -> &EnvironmentConfig {
120        &self.config
121    }
122
123    /// Get a runtime by its id, if present.
124    pub async fn get_runtime(&self, runtime_id: &RuntimeID) -> Option<Arc<dyn Runtime>> {
125        self.runtime_manager.get_runtime(runtime_id).await
126    }
127
128    /// Get the specified runtime or the default one when `None` is passed.
129    pub async fn get_runtime_or_default(
130        &self,
131        runtime_id: Option<RuntimeID>,
132    ) -> Result<Arc<dyn Runtime>, Error> {
133        let rid = match runtime_id {
134            Some(id) => id,
135            None => self
136                .default_runtime
137                .ok_or(EnvironmentError::NoDefaultRuntime)?,
138        };
139        self.get_runtime(&rid)
140            .await
141            .ok_or_else(|| EnvironmentError::RuntimeNotFound(rid).into())
142    }
143
144    /// Start all registered runtimes in the background.
145    ///
146    /// Stores the spawned task handle so [`shutdown`](Self::shutdown) can stop
147    /// runtimes and await completion. Returns [`EnvironmentError::AlreadyRunning`]
148    /// if a run task is already in progress.
149    ///
150    /// If a previous managed run task finished without [`wait`](Self::wait) or
151    /// [`shutdown`](Self::shutdown), its result is joined and returned before
152    /// spawning a new run task.
153    ///
154    /// Use [`wait`](Self::wait) to await the background run task, or
155    /// [`shutdown`](Self::shutdown) to stop runtimes and join the task.
156    #[allow(clippy::result_large_err)] // Only `AlreadyRunning` is returned from this method.
157    pub fn run(&mut self) -> Result<(), EnvironmentError> {
158        self.reconcile_finished_managed_launch()?;
159
160        if self.launch_state == RuntimeLaunchState::Background {
161            return Err(EnvironmentError::AlreadyRunning);
162        }
163
164        if self.is_running() {
165            return Err(EnvironmentError::AlreadyRunning);
166        }
167
168        let manager = self.runtime_manager.clone();
169        let handle = tokio::spawn(async move { manager.run().await });
170        self.handle = Some(handle);
171        self.launch_state = RuntimeLaunchState::Managed;
172        Ok(())
173    }
174
175    /// Await the background task started by [`run`](Self::run).
176    ///
177    /// Returns `Ok(Ok(()))` when no run task has been started. After the task
178    /// completes, the stored handle is cleared so subsequent calls return
179    /// immediately.
180    ///
181    /// If this future is dropped before completion (for example when a
182    /// `tokio::select!` branch wins elsewhere), the join handle is restored on
183    /// the environment so a later [`shutdown`](Self::shutdown) can still stop
184    /// runtimes and join the run task.
185    pub async fn wait(&mut self) -> Result<Result<(), RuntimeError>, tokio::task::JoinError> {
186        let handle = match self.handle.take() {
187            Some(handle) => handle,
188            None => return Ok(Ok(())),
189        };
190
191        let mut guard = RestoreRunHandleOnDrop {
192            environment: self,
193            handle: Some(handle),
194        };
195
196        let join_result = guard.handle.as_mut().expect("handle was just set").await;
197
198        guard.handle = None;
199        guard.environment.launch_state = RuntimeLaunchState::Idle;
200
201        join_result
202    }
203
204    /// Start all registered runtimes and return immediately without waiting
205    /// for completion.
206    ///
207    /// Cannot be combined with [`run`](Self::run) on the same environment
208    /// instance without calling [`shutdown`](Self::shutdown) first.
209    pub async fn run_background(&mut self) -> Result<(), EnvironmentError> {
210        self.reconcile_finished_managed_launch()?;
211
212        if self.launch_state != RuntimeLaunchState::Idle || self.is_running() {
213            return Err(EnvironmentError::AlreadyRunning);
214        }
215
216        let manager = self.runtime_manager.clone();
217        manager
218            .run_background()
219            .await
220            .map_err(|e| EnvironmentError::RuntimeError(Box::new(e)))?;
221        self.launch_state = RuntimeLaunchState::Background;
222        Ok(())
223    }
224
225    /// Take the event receiver for a specific runtime (or the default one) so
226    /// the caller can consume protocol events. This can only be taken once.
227    pub async fn take_event_receiver(
228        &mut self,
229        runtime_id: Option<RuntimeID>,
230    ) -> Result<BoxEventStream<Event>, EnvironmentError> {
231        let runtime = self
232            .get_runtime_or_default(runtime_id)
233            .await
234            .map_err(|err| match err {
235                Error::EnvironmentError(env_err) => env_err,
236                _ => EnvironmentError::EventError,
237            })?;
238
239        runtime
240            .take_event_receiver()
241            .await
242            .ok_or(EnvironmentError::EventError)
243    }
244
245    /// Subscribe to runtime events without consuming the receiver.
246    pub async fn subscribe_events(
247        &self,
248        runtime_id: Option<RuntimeID>,
249    ) -> Result<BoxEventStream<Event>, EnvironmentError> {
250        let runtime = self
251            .get_runtime_or_default(runtime_id)
252            .await
253            .map_err(|err| match err {
254                Error::EnvironmentError(env_err) => env_err,
255                _ => EnvironmentError::EventError,
256            })?;
257        Ok(runtime.subscribe_events().await)
258    }
259
260    /// Request shutdown on all runtimes and await the run handle if present.
261    pub async fn shutdown(&mut self) -> Result<(), EnvironmentError> {
262        let stop_result = self.runtime_manager.stop().await;
263
264        let join_result = if let Some(handle) = self.handle.take() {
265            Some(handle.await)
266        } else {
267            None
268        };
269
270        self.launch_state = RuntimeLaunchState::Idle;
271
272        if let Err(e) = stop_result {
273            return Err(EnvironmentError::RuntimeError(Box::new(e)));
274        }
275
276        match join_result {
277            None | Some(Ok(Ok(()))) => Ok(()),
278            Some(Ok(Err(e))) => Err(EnvironmentError::RuntimeError(Box::new(e))),
279            Some(Err(e)) => Err(EnvironmentError::JoinError(e)),
280        }
281    }
282
283    /// Returns whether the environment has an active runtime launch.
284    ///
285    /// For [`run`](Self::run) this checks the managed join handle. For
286    /// [`run_background`](Self::run_background) this returns `true` until
287    /// [`shutdown`](Self::shutdown) clears the launch state.
288    pub fn is_running(&self) -> bool {
289        match self.launch_state {
290            RuntimeLaunchState::Background => true,
291            RuntimeLaunchState::Managed => self
292                .handle
293                .as_ref()
294                .is_some_and(|handle| !handle.is_finished()),
295            RuntimeLaunchState::Idle => false,
296        }
297    }
298
299    #[allow(clippy::result_large_err)]
300    /// Join a managed run task that finished without `wait()` or `shutdown()`.
301    ///
302    /// No-op unless `launch_state` is [`RuntimeLaunchState::Managed`]. When the stored handle is
303    /// still running, the handle is restored and this returns `Ok(())`. When the handle finished,
304    /// it is joined via [`join_finished_handle`](Self::join_finished_handle) and `launch_state`
305    /// becomes `Idle`.
306    fn reconcile_finished_managed_launch(&mut self) -> Result<(), EnvironmentError> {
307        if self.launch_state != RuntimeLaunchState::Managed {
308            return Ok(());
309        }
310
311        let Some(handle) = self.handle.take() else {
312            self.launch_state = RuntimeLaunchState::Idle;
313            return Ok(());
314        };
315
316        if !handle.is_finished() {
317            self.handle = Some(handle);
318            return Ok(());
319        }
320
321        self.launch_state = RuntimeLaunchState::Idle;
322        Self::join_finished_handle(handle)
323    }
324
325    #[allow(clippy::result_large_err)]
326    fn join_finished_handle(
327        handle: JoinHandle<Result<(), RuntimeError>>,
328    ) -> Result<(), EnvironmentError> {
329        debug_assert!(
330            handle.is_finished(),
331            "join_finished_handle requires a finished run task"
332        );
333
334        match handle.now_or_never() {
335            Some(Ok(Ok(()))) => Ok(()),
336            Some(Ok(Err(e))) => Err(EnvironmentError::RuntimeError(Box::new(e))),
337            Some(Err(e)) => Err(EnvironmentError::JoinError(e)),
338            None => Err(EnvironmentError::RunResultNotReady),
339        }
340    }
341}
342
343/// Restores a managed run [`JoinHandle`] when a [`Environment::wait`] future is
344/// cancelled before the join completes.
345struct RestoreRunHandleOnDrop<'a> {
346    environment: &'a mut Environment,
347    handle: Option<JoinHandle<Result<(), RuntimeError>>>,
348}
349
350impl Drop for RestoreRunHandleOnDrop<'_> {
351    fn drop(&mut self) {
352        if let Some(handle) = self.handle.take() {
353            self.environment.handle = Some(handle);
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::runtime::SingleThreadedRuntime;
362    use tempfile::tempdir;
363    use tokio::sync::mpsc;
364    use uuid::Uuid;
365
366    #[test]
367    fn test_environment_config_default() {
368        let config = EnvironmentConfig::default();
369        assert_eq!(
370            config.working_dir,
371            std::env::current_dir().unwrap_or_default()
372        );
373    }
374
375    #[test]
376    fn test_environment_config_custom() {
377        let dir = tempdir().expect("Unable to create temp dir");
378        let config = EnvironmentConfig {
379            working_dir: dir.path().to_path_buf(),
380        };
381        assert_eq!(config.working_dir, dir.path().to_path_buf());
382    }
383
384    #[tokio::test]
385    async fn test_environment_get_runtime() {
386        let mut env = Environment::new(None);
387        let runtime = SingleThreadedRuntime::new(None);
388        let runtime_id = runtime.id;
389        env.register_runtime(runtime).await.unwrap();
390
391        // Test getting default runtime
392        let runtime = env.get_runtime(&runtime_id).await;
393
394        assert!(runtime.is_some());
395
396        // Test getting non-existent runtime
397        let non_existent_id = Uuid::new_v4();
398        let runtime = env.get_runtime(&non_existent_id).await;
399        assert!(runtime.is_none());
400    }
401
402    #[tokio::test]
403    async fn test_environment_take_event_receiver() {
404        let mut env = Environment::new(None);
405        let runtime = SingleThreadedRuntime::new(None);
406        let _ = runtime.id;
407        env.register_runtime(runtime).await.unwrap();
408        let receiver = env.take_event_receiver(None).await;
409        assert!(receiver.is_ok());
410
411        // Second call should return None
412        let receiver2 = env.take_event_receiver(None).await;
413        assert!(receiver2.is_err());
414    }
415
416    #[tokio::test]
417    async fn test_environment_shutdown() {
418        let mut env = Environment::new(None);
419        env.shutdown()
420            .await
421            .expect("shutdown should succeed when idle");
422    }
423
424    #[tokio::test]
425    async fn test_environment_error_runtime_not_found() {
426        let mut env = Environment::new(None);
427        let runtime = SingleThreadedRuntime::new(None);
428        let _ = runtime.id;
429        env.register_runtime(runtime).await.unwrap();
430        let non_existent_id = Uuid::new_v4();
431
432        let result = env.get_runtime_or_default(Some(non_existent_id)).await;
433        assert!(matches!(
434            result,
435            Err(Error::EnvironmentError(EnvironmentError::RuntimeNotFound(id)))
436            if id == non_existent_id
437        ));
438    }
439
440    #[test]
441    fn test_environment_error_already_running_display() {
442        let error = EnvironmentError::AlreadyRunning;
443        assert!(error.to_string().contains("already running"));
444    }
445
446    #[test]
447    fn test_environment_error_run_result_not_ready_display() {
448        let error = EnvironmentError::RunResultNotReady;
449        assert!(error.to_string().contains("not yet available"));
450    }
451
452    #[test]
453    fn test_environment_error_display() {
454        let runtime_id = Uuid::new_v4();
455        let error = EnvironmentError::RuntimeNotFound(runtime_id);
456        assert!(error.to_string().contains("Runtime not found"));
457        assert!(error.to_string().contains(&runtime_id.to_string()));
458    }
459
460    #[test]
461    fn test_environment_error_no_default_display() {
462        let error = EnvironmentError::NoDefaultRuntime;
463        assert!(error.to_string().contains("No default runtime registered"));
464    }
465
466    #[tokio::test]
467    async fn test_get_runtime_or_default_no_default_runtime() {
468        let env = Environment::new(None);
469
470        let result = env.get_runtime_or_default(None).await;
471        assert!(matches!(
472            result,
473            Err(Error::EnvironmentError(EnvironmentError::NoDefaultRuntime))
474        ));
475    }
476
477    #[tokio::test]
478    async fn test_take_event_receiver_no_default_runtime() {
479        let mut env = Environment::new(None);
480
481        let result = env.take_event_receiver(None).await;
482        assert!(matches!(result, Err(EnvironmentError::NoDefaultRuntime)));
483    }
484
485    #[tokio::test]
486    async fn test_subscribe_events_no_default_runtime() {
487        let env = Environment::new(None);
488
489        let result = env.subscribe_events(None).await;
490        assert!(matches!(result, Err(EnvironmentError::NoDefaultRuntime)));
491    }
492
493    #[tokio::test]
494    async fn test_environment_run_stores_handle() {
495        let mut env = Environment::new(None);
496        let runtime = SingleThreadedRuntime::new(None);
497        env.register_runtime(runtime).await.unwrap();
498
499        assert!(!env.is_running());
500        env.run().expect("run should succeed");
501        assert!(env.is_running());
502        assert!(matches!(env.run(), Err(EnvironmentError::AlreadyRunning)));
503
504        env.shutdown().await.expect("shutdown should succeed");
505        assert!(!env.is_running());
506    }
507
508    #[tokio::test]
509    async fn test_environment_run_can_restart_after_shutdown() {
510        let mut env = Environment::new(None);
511        let runtime = SingleThreadedRuntime::new(None);
512        env.register_runtime(runtime).await.unwrap();
513
514        env.run().expect("initial run should succeed");
515        env.shutdown().await.expect("shutdown should succeed");
516
517        // Environment allows spawning a new run task after shutdown. The same
518        // SingleThreadedRuntime instance cannot re-enter its event loop, so the
519        // second run task fails when joined.
520        env.run().expect("run after shutdown should succeed");
521        let run_result = env.wait().await.expect("wait should join run task");
522        assert!(run_result.is_err());
523
524        env.shutdown()
525            .await
526            .expect("shutdown should succeed when idle after failed run");
527    }
528
529    #[tokio::test]
530    async fn test_get_runtime_or_default_runtime_not_found_variant() {
531        let mut env = Environment::new(None);
532        let runtime = SingleThreadedRuntime::new(None);
533        env.register_runtime(runtime).await.unwrap();
534        let non_existent_id = Uuid::new_v4();
535
536        let result = env.get_runtime_or_default(Some(non_existent_id)).await;
537        assert!(matches!(
538            result,
539            Err(Error::EnvironmentError(EnvironmentError::RuntimeNotFound(id)))
540            if id == non_existent_id
541        ));
542    }
543
544    #[tokio::test]
545    async fn test_take_event_receiver_runtime_not_found() {
546        let mut env = Environment::new(None);
547        let runtime = SingleThreadedRuntime::new(None);
548        env.register_runtime(runtime).await.unwrap();
549        let non_existent_id = Uuid::new_v4();
550
551        let result = env.take_event_receiver(Some(non_existent_id)).await;
552        assert!(matches!(
553            result,
554            Err(EnvironmentError::RuntimeNotFound(id)) if id == non_existent_id
555        ));
556    }
557
558    #[tokio::test]
559    async fn test_environment_wait_restores_handle_when_cancelled() {
560        use tokio::time::{Duration, sleep};
561
562        let mut env = Environment::new(None);
563        let runtime = SingleThreadedRuntime::new(None);
564        env.register_runtime(runtime).await.unwrap();
565
566        env.run().expect("run should succeed");
567        assert!(env.is_running());
568
569        {
570            let wait_fut = env.wait();
571            tokio::pin!(wait_fut);
572
573            tokio::select! {
574                _ = &mut wait_fut => panic!("run task should not finish immediately"),
575                _ = sleep(Duration::from_millis(10)) => {}
576            }
577        }
578
579        assert!(
580            env.is_running(),
581            "cancelled wait should restore the join handle"
582        );
583
584        env.shutdown()
585            .await
586            .expect("shutdown should join the restored handle");
587        assert!(!env.is_running());
588    }
589
590    #[tokio::test]
591    async fn test_environment_wait_is_idempotent() {
592        use tokio::time::{Duration, timeout};
593
594        let mut env = Environment::new(None);
595        let runtime = SingleThreadedRuntime::new(None);
596        env.register_runtime(runtime).await.unwrap();
597
598        env.run().expect("run should succeed");
599        env.shutdown().await.expect("shutdown should succeed");
600
601        for _ in 0..2 {
602            let result = timeout(Duration::from_secs(1), env.wait())
603                .await
604                .expect("wait should not hang");
605            assert!(result.is_ok());
606        }
607    }
608
609    #[derive(Clone, Copy)]
610    enum ImmediateRuntimeBehavior {
611        Success,
612        Error,
613    }
614
615    struct ImmediateRuntime {
616        id: RuntimeID,
617        behavior: ImmediateRuntimeBehavior,
618        tx: mpsc::Sender<Event>,
619    }
620
621    #[async_trait::async_trait]
622    impl Runtime for ImmediateRuntime {
623        fn id(&self) -> RuntimeID {
624            self.id
625        }
626
627        async fn subscribe_any(
628            &self,
629            _topic_name: &str,
630            _topic_type: std::any::TypeId,
631            _actor: Arc<dyn crate::actor::AnyActor>,
632        ) -> Result<(), RuntimeError> {
633            Ok(())
634        }
635
636        async fn publish_any(
637            &self,
638            _topic_name: &str,
639            _topic_type: std::any::TypeId,
640            _message: Arc<dyn std::any::Any + Send + Sync>,
641        ) -> Result<(), RuntimeError> {
642            Ok(())
643        }
644
645        fn tx(&self) -> mpsc::Sender<Event> {
646            self.tx.clone()
647        }
648
649        async fn transport(&self) -> Arc<dyn crate::actor::Transport> {
650            Arc::new(crate::actor::LocalTransport)
651        }
652
653        async fn take_event_receiver(&self) -> Option<BoxEventStream<Event>> {
654            None
655        }
656
657        async fn subscribe_events(&self) -> BoxEventStream<Event> {
658            Box::pin(futures::stream::empty())
659        }
660
661        async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
662            match self.behavior {
663                ImmediateRuntimeBehavior::Success => Ok(()),
664                ImmediateRuntimeBehavior::Error => {
665                    Err(std::io::Error::other("immediate runtime run failed").into())
666                }
667            }
668        }
669
670        async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
671            Ok(())
672        }
673    }
674
675    #[tokio::test]
676    async fn test_environment_wait_awaits_completed_run_task() {
677        let mut env = Environment::new(None);
678        let (tx, _rx) = mpsc::channel(1);
679        let runtime = Arc::new(ImmediateRuntime {
680            id: RuntimeID::new_v4(),
681            behavior: ImmediateRuntimeBehavior::Success,
682            tx,
683        }) as Arc<dyn Runtime>;
684        env.register_runtime(runtime).await.unwrap();
685
686        env.run().expect("run should succeed");
687        let wait_result = env.wait().await.expect("wait join should succeed");
688        assert!(wait_result.is_ok());
689    }
690
691    #[tokio::test]
692    async fn test_environment_run_restarts_after_finished_handle() {
693        use tokio::time::{Duration, sleep};
694
695        let mut env = Environment::new(None);
696        let (tx, _rx) = mpsc::channel(1);
697        let runtime = Arc::new(ImmediateRuntime {
698            id: RuntimeID::new_v4(),
699            behavior: ImmediateRuntimeBehavior::Success,
700            tx,
701        }) as Arc<dyn Runtime>;
702        env.register_runtime(runtime).await.unwrap();
703
704        env.run().expect("initial run should succeed");
705        sleep(Duration::from_millis(20)).await;
706        assert!(!env.is_running());
707
708        env.run().expect("run after finished handle should succeed");
709        env.shutdown().await.expect("shutdown should succeed");
710    }
711
712    #[tokio::test]
713    async fn test_environment_run_background_starts_runtimes() {
714        let mut env = Environment::new(None);
715        let (tx, _rx) = mpsc::channel(1);
716        let runtime = Arc::new(ImmediateRuntime {
717            id: RuntimeID::new_v4(),
718            behavior: ImmediateRuntimeBehavior::Success,
719            tx,
720        }) as Arc<dyn Runtime>;
721        env.register_runtime(runtime).await.unwrap();
722
723        env.run_background()
724            .await
725            .expect("run_background should succeed");
726        assert!(
727            env.is_running(),
728            "run_background should mark the environment as running until shutdown"
729        );
730
731        env.shutdown().await.expect("shutdown should succeed");
732    }
733
734    #[tokio::test]
735    async fn test_environment_run_background_rejects_after_run() {
736        let mut env = Environment::new(None);
737        let runtime = SingleThreadedRuntime::new(None);
738        env.register_runtime(runtime).await.unwrap();
739
740        env.run().expect("run should succeed");
741        assert!(matches!(
742            env.run_background().await,
743            Err(EnvironmentError::AlreadyRunning)
744        ));
745
746        env.shutdown().await.expect("shutdown should succeed");
747    }
748
749    #[tokio::test]
750    async fn test_environment_run_rejects_after_run_background() {
751        let mut env = Environment::new(None);
752        let (tx, _rx) = mpsc::channel(1);
753        let runtime = Arc::new(ImmediateRuntime {
754            id: RuntimeID::new_v4(),
755            behavior: ImmediateRuntimeBehavior::Success,
756            tx,
757        }) as Arc<dyn Runtime>;
758        env.register_runtime(runtime).await.unwrap();
759
760        env.run_background()
761            .await
762            .expect("run_background should succeed");
763        assert!(matches!(env.run(), Err(EnvironmentError::AlreadyRunning)));
764
765        env.shutdown().await.expect("shutdown should succeed");
766    }
767
768    #[tokio::test]
769    async fn test_environment_wait_propagates_run_failure() {
770        let mut env = Environment::new(None);
771        let (tx, _rx) = mpsc::channel(1);
772        let runtime = Arc::new(ImmediateRuntime {
773            id: RuntimeID::new_v4(),
774            behavior: ImmediateRuntimeBehavior::Error,
775            tx,
776        }) as Arc<dyn Runtime>;
777        env.register_runtime(runtime).await.unwrap();
778
779        env.run().expect("run should succeed");
780        let wait_result = env.wait().await.expect("wait join should succeed");
781        assert!(wait_result.is_err());
782    }
783
784    #[tokio::test]
785    async fn test_environment_run_surfaces_prior_failure_without_wait() {
786        use tokio::time::{Duration, sleep};
787
788        let mut env = Environment::new(None);
789        let (tx, _rx) = mpsc::channel(1);
790        let runtime = Arc::new(ImmediateRuntime {
791            id: RuntimeID::new_v4(),
792            behavior: ImmediateRuntimeBehavior::Error,
793            tx,
794        }) as Arc<dyn Runtime>;
795        env.register_runtime(runtime).await.unwrap();
796
797        env.run().expect("initial run should succeed");
798        sleep(Duration::from_millis(20)).await;
799        assert!(!env.is_running());
800
801        let err = env
802            .run()
803            .expect_err("restart should surface prior run failure");
804        assert!(matches!(err, EnvironmentError::RuntimeError(_)));
805    }
806
807    #[tokio::test]
808    async fn test_environment_run_background_surfaces_prior_failure_without_wait() {
809        use tokio::time::{Duration, sleep};
810
811        let mut env = Environment::new(None);
812        let (tx, _rx) = mpsc::channel(1);
813        let runtime = Arc::new(ImmediateRuntime {
814            id: RuntimeID::new_v4(),
815            behavior: ImmediateRuntimeBehavior::Error,
816            tx,
817        }) as Arc<dyn Runtime>;
818        env.register_runtime(runtime).await.unwrap();
819
820        env.run().expect("initial run should succeed");
821        sleep(Duration::from_millis(20)).await;
822        assert!(!env.is_running());
823
824        let err = env
825            .run_background()
826            .await
827            .expect_err("run_background should surface prior run failure");
828        assert!(matches!(err, EnvironmentError::RuntimeError(_)));
829    }
830
831    #[test]
832    fn test_environment_config_accessor() {
833        let dir = tempdir().expect("Unable to create temp dir");
834        let config = EnvironmentConfig {
835            working_dir: dir.path().to_path_buf(),
836        };
837        let env = Environment::new(Some(config.clone()));
838        assert_eq!(env.config().working_dir, config.working_dir);
839    }
840
841    #[tokio::test]
842    async fn test_subscribe_events_with_default_runtime() {
843        let mut env = Environment::new(None);
844        let runtime = SingleThreadedRuntime::new(None);
845        env.register_runtime(runtime).await.unwrap();
846
847        let stream = env.subscribe_events(None).await;
848        assert!(stream.is_ok());
849    }
850
851    #[tokio::test]
852    async fn test_get_runtime_or_default_uses_default_runtime() {
853        let mut env = Environment::new(None);
854        let runtime = SingleThreadedRuntime::new(None);
855        let runtime_id = runtime.id;
856        env.register_runtime(runtime).await.unwrap();
857
858        let resolved = env
859            .get_runtime_or_default(None)
860            .await
861            .expect("default runtime should resolve");
862        assert_eq!(resolved.id(), runtime_id);
863    }
864}