1use std::{cell::RefCell, fmt::Debug, rc::Rc};
17
18use nautilus_common::{
19 actor::{
20 DataActor, DataActorCore, DataActorNative, data_actor::DataActorConfig,
21 registry::try_get_actor_unchecked,
22 },
23 component::Component,
24 msgbus::{Endpoint, MStr, TypedHandler, get_message_bus},
25 nautilus_actor,
26};
27use nautilus_model::identifiers::{ActorId, StrategyId};
28use nautilus_trading::{Strategy, StrategyNative};
29
30use crate::{messages::ControllerCommand, trader::Trader};
31
32#[derive(Debug)]
33pub struct Controller {
34 core: DataActorCore,
35 trader: Rc<RefCell<Trader>>,
36}
37
38impl Controller {
39 pub const EXECUTE_ENDPOINT: &str = "Controller.execute";
40
41 #[must_use]
42 pub fn new(trader: Rc<RefCell<Trader>>, config: Option<DataActorConfig>) -> Self {
43 Self {
44 core: DataActorCore::new(config.unwrap_or_default()),
45 trader,
46 }
47 }
48
49 pub fn send(command: &ControllerCommand) -> anyhow::Result<()> {
55 let endpoint = Self::execute_endpoint();
56 let handler = {
57 let msgbus = get_message_bus();
58 msgbus
59 .borrow_mut()
60 .endpoint_map::<ControllerCommand>()
61 .get(endpoint)
62 .cloned()
63 };
64
65 let Some(handler) = handler else {
66 anyhow::bail!(
67 "Controller execute endpoint '{}' not registered",
68 endpoint.as_str()
69 );
70 };
71
72 handler.handle(command);
73 Ok(())
74 }
75
76 pub fn execute(&mut self, command: ControllerCommand) -> anyhow::Result<()> {
82 match command {
83 ControllerCommand::CreateActor(command) => {
84 Self::unsupported_create_actor_command(&command)
85 }
86 ControllerCommand::StartActor(command) => self.start_actor(&command.actor_id),
87 ControllerCommand::StopActor(command) => self.stop_actor(&command.actor_id),
88 ControllerCommand::RemoveActor(command) => self.remove_actor(&command.actor_id),
89 ControllerCommand::CreateStrategy(command) => {
90 Self::unsupported_create_strategy_command(&command)
91 }
92 ControllerCommand::StartStrategy(command) => self.start_strategy(&command.strategy_id),
93 ControllerCommand::StopStrategy(command) => self.stop_strategy(&command.strategy_id),
94 ControllerCommand::ExitMarket(strategy_id) => self.exit_market(&strategy_id),
95 ControllerCommand::RemoveStrategy(command) => {
96 self.remove_strategy(&command.strategy_id)
97 }
98 }
99 }
100
101 pub fn create_actor<T>(&self, actor: T, start: bool) -> anyhow::Result<ActorId>
107 where
108 T: DataActor + DataActorNative + Component + Debug + 'static,
109 {
110 let actor_id = actor.actor_id();
111 self.trader.borrow_mut().add_actor(actor)?;
112
113 self.start_created_actor(actor_id, start)?;
114
115 Ok(actor_id)
116 }
117
118 pub fn create_actor_from_factory<F, T>(
124 &self,
125 factory: F,
126 start: bool,
127 ) -> anyhow::Result<ActorId>
128 where
129 F: FnOnce() -> anyhow::Result<T>,
130 T: DataActor + DataActorNative + Component + Debug + 'static,
131 {
132 let actor = factory()?;
133 self.create_actor(actor, start)
134 }
135
136 pub fn create_strategy<T>(&self, mut strategy: T, start: bool) -> anyhow::Result<StrategyId>
142 where
143 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
144 {
145 let strategy_id = self
146 .trader
147 .borrow()
148 .prepare_strategy_for_registration(&mut strategy)?;
149 self.trader.borrow_mut().add_strategy(strategy)?;
150
151 self.start_created_strategy(strategy_id, start)?;
152
153 Ok(strategy_id)
154 }
155
156 pub fn create_strategy_from_factory<F, T>(
162 &self,
163 factory: F,
164 start: bool,
165 ) -> anyhow::Result<StrategyId>
166 where
167 F: FnOnce() -> anyhow::Result<T>,
168 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
169 {
170 let strategy = factory()?;
171 self.create_strategy(strategy, start)
172 }
173
174 pub fn start_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
180 self.trader.borrow().start_actor(actor_id)
181 }
182
183 pub fn stop_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
189 self.trader.borrow().stop_actor(actor_id)
190 }
191
192 pub fn remove_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
198 if actor_id.inner() == self.core.actor_id().inner() {
199 return Ok(());
200 }
201
202 self.trader.borrow_mut().remove_actor(actor_id)
203 }
204
205 pub fn start_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
211 self.trader.borrow().start_strategy(strategy_id)
212 }
213
214 pub fn stop_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
220 self.trader.borrow_mut().stop_strategy(strategy_id)
221 }
222
223 pub fn exit_market(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
229 Trader::market_exit_strategy(&self.trader, strategy_id)
230 }
231
232 pub fn remove_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
238 self.trader.borrow_mut().remove_strategy(strategy_id)
239 }
240
241 fn start_created_actor(&self, actor_id: ActorId, start: bool) -> anyhow::Result<()> {
242 if !start {
243 return Ok(());
244 }
245
246 if let Err(start_err) = self.start_actor(&actor_id) {
247 return Err(self.rollback_actor_start_failure(actor_id, start_err));
248 }
249
250 Ok(())
251 }
252
253 fn start_created_strategy(&self, strategy_id: StrategyId, start: bool) -> anyhow::Result<()> {
254 if !start {
255 return Ok(());
256 }
257
258 if let Err(start_err) = self.start_strategy(&strategy_id) {
259 return Err(self.rollback_strategy_start_failure(strategy_id, start_err));
260 }
261
262 Ok(())
263 }
264
265 fn rollback_actor_start_failure(
266 &self,
267 actor_id: ActorId,
268 start_err: anyhow::Error,
269 ) -> anyhow::Error {
270 match self.remove_actor(&actor_id) {
271 Ok(()) => start_err,
272 Err(rollback_err) => anyhow::anyhow!(
273 "Failed to start actor {actor_id}: {start_err}; rollback failed: {rollback_err}"
274 ),
275 }
276 }
277
278 fn rollback_strategy_start_failure(
279 &self,
280 strategy_id: StrategyId,
281 start_err: anyhow::Error,
282 ) -> anyhow::Error {
283 match self.remove_strategy(&strategy_id) {
284 Ok(()) => start_err,
285 Err(rollback_err) => anyhow::anyhow!(
286 "Failed to start strategy {strategy_id}: {start_err}; rollback failed: {rollback_err}"
287 ),
288 }
289 }
290
291 fn register_execute_endpoint(&self) {
292 let controller_id = self.core.actor_id().inner();
293 let handler = TypedHandler::from(move |command: &ControllerCommand| {
294 if let Some(mut controller) = try_get_actor_unchecked::<Self>(&controller_id) {
295 if let Err(e) = controller.execute(command.clone()) {
296 log::error!("Controller command failed for {controller_id}: {e}");
297 }
298 } else {
299 log::error!("Controller {controller_id} not found for command handling");
300 }
301 });
302
303 get_message_bus()
304 .borrow_mut()
305 .endpoint_map::<ControllerCommand>()
306 .register(Self::execute_endpoint(), handler);
307 }
308
309 fn deregister_execute_endpoint() {
310 get_message_bus()
311 .borrow_mut()
312 .endpoint_map::<ControllerCommand>()
313 .deregister(Self::execute_endpoint());
314 }
315
316 fn execute_endpoint() -> MStr<Endpoint> {
317 Self::EXECUTE_ENDPOINT.into()
318 }
319
320 fn unsupported_create_actor_command(
321 command: &crate::messages::CreateActor,
322 ) -> anyhow::Result<()> {
323 anyhow::bail!(
324 "CreateActor command for importable actor '{}' is not supported by the Rust controller",
325 command.actor_config.actor_path
326 );
327 }
328
329 fn unsupported_create_strategy_command(
330 command: &crate::messages::CreateStrategy,
331 ) -> anyhow::Result<()> {
332 anyhow::bail!(
333 "CreateStrategy command for importable strategy '{}' is not supported by the Rust controller",
334 command.strategy_config.strategy_path
335 );
336 }
337}
338
339impl DataActor for Controller {
340 fn on_start(&mut self) -> anyhow::Result<()> {
341 self.register_execute_endpoint();
342 Ok(())
343 }
344
345 fn on_stop(&mut self) -> anyhow::Result<()> {
346 Self::deregister_execute_endpoint();
347 Ok(())
348 }
349
350 fn on_resume(&mut self) -> anyhow::Result<()> {
351 self.register_execute_endpoint();
352 Ok(())
353 }
354
355 fn on_dispose(&mut self) -> anyhow::Result<()> {
356 Self::deregister_execute_endpoint();
357 Ok(())
358 }
359}
360
361nautilus_actor!(Controller);
362
363#[cfg(test)]
364mod tests {
365 use std::collections::HashMap;
366
367 use nautilus_common::{
368 actor::data_actor::ImportableActorConfig,
369 cache::Cache,
370 clock::TestClock,
371 enums::{ComponentState, Environment},
372 msgbus::{MessageBus, set_message_bus},
373 };
374 use nautilus_core::{UUID4, UnixNanos};
375 use nautilus_model::{identifiers::TraderId, stubs::TestDefault};
376 use nautilus_portfolio::portfolio::Portfolio;
377 use nautilus_trading::{
378 ImportableStrategyConfig, nautilus_strategy,
379 strategy::{StrategyConfig, StrategyCore},
380 };
381 use rstest::rstest;
382
383 use super::*;
384 use crate::{
385 clock_factory::ClockFactory,
386 messages::{
387 CreateActor, CreateStrategy, RemoveActor, RemoveStrategy, StartActor, StartStrategy,
388 StopActor, StopStrategy,
389 },
390 };
391
392 fn start_actor_command(actor_id: ActorId) -> ControllerCommand {
393 StartActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
394 }
395
396 fn stop_actor_command(actor_id: ActorId) -> ControllerCommand {
397 StopActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
398 }
399
400 fn remove_actor_command(actor_id: ActorId) -> ControllerCommand {
401 RemoveActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
402 }
403
404 fn start_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
405 StartStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
406 }
407
408 fn stop_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
409 StopStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
410 }
411
412 fn remove_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
413 RemoveStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
414 }
415
416 #[derive(Debug)]
417 struct TestDataActor {
418 core: DataActorCore,
419 }
420
421 impl TestDataActor {
422 fn new(config: DataActorConfig) -> Self {
423 Self {
424 core: DataActorCore::new(config),
425 }
426 }
427 }
428
429 impl DataActor for TestDataActor {}
430
431 nautilus_actor!(TestDataActor);
432
433 #[derive(Debug)]
434 struct TestStrategy {
435 core: StrategyCore,
436 }
437
438 impl TestStrategy {
439 fn new(config: StrategyConfig) -> Self {
440 Self {
441 core: StrategyCore::new(config),
442 }
443 }
444 }
445
446 impl DataActor for TestStrategy {}
447
448 nautilus_strategy!(TestStrategy);
449
450 #[derive(Debug)]
451 struct FailingStartActor {
452 core: DataActorCore,
453 }
454
455 impl FailingStartActor {
456 fn new(config: DataActorConfig) -> Self {
457 Self {
458 core: DataActorCore::new(config),
459 }
460 }
461 }
462
463 impl DataActor for FailingStartActor {
464 fn on_start(&mut self) -> anyhow::Result<()> {
465 anyhow::bail!("Simulated actor start failure")
466 }
467 }
468
469 nautilus_actor!(FailingStartActor);
470
471 #[derive(Debug)]
472 struct FailingStartStrategy {
473 core: StrategyCore,
474 }
475
476 impl FailingStartStrategy {
477 fn new(config: StrategyConfig) -> Self {
478 Self {
479 core: StrategyCore::new(config),
480 }
481 }
482 }
483
484 impl DataActor for FailingStartStrategy {
485 fn on_start(&mut self) -> anyhow::Result<()> {
486 anyhow::bail!("Simulated strategy start failure")
487 }
488 }
489
490 nautilus_strategy!(FailingStartStrategy);
491
492 #[derive(Debug)]
493 struct ReentrantExitStrategy {
494 core: StrategyCore,
495 actor_to_stop: ActorId,
496 }
497
498 impl ReentrantExitStrategy {
499 fn new(config: StrategyConfig, actor_to_stop: ActorId) -> Self {
500 Self {
501 core: StrategyCore::new(config),
502 actor_to_stop,
503 }
504 }
505 }
506
507 impl DataActor for ReentrantExitStrategy {}
508
509 nautilus_strategy!(ReentrantExitStrategy, {
510 fn on_market_exit(&mut self) {
511 Controller::send(&stop_actor_command(self.actor_to_stop)).unwrap();
512 }
513 });
514
515 fn create_running_controller() -> (Rc<RefCell<Trader>>, ActorId) {
516 let trader_id = TraderId::test_default();
517 let instance_id = UUID4::new();
518 let clock_factory = ClockFactory::test_default();
519 let clock = clock_factory.clock();
520 let mut clock_ref = clock.borrow_mut();
521 let test_clock = clock_ref
522 .as_any_mut()
523 .downcast_mut::<TestClock>()
524 .expect("test default clock must be TestClock");
525 test_clock.set_time(1_000_000_000u64.into());
526 drop(clock_ref);
527
528 let msgbus = Rc::new(RefCell::new(MessageBus::new(
529 trader_id,
530 instance_id,
531 Some("test".to_string()),
532 None,
533 )));
534 set_message_bus(msgbus);
535
536 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
537 let portfolio = Rc::new(RefCell::new(Portfolio::new(
538 clock.clone(),
539 cache.clone(),
540 None,
541 )));
542
543 let trader = Rc::new(RefCell::new(Trader::new(
544 trader_id,
545 instance_id,
546 Environment::Backtest,
547 clock_factory,
548 cache,
549 portfolio,
550 )));
551 trader.borrow_mut().initialize().unwrap();
552
553 let controller = Controller::new(
554 trader.clone(),
555 Some(DataActorConfig {
556 actor_id: Some(ActorId::from("Controller-001")),
557 ..Default::default()
558 }),
559 );
560 let controller_id = controller.core.actor_id();
561
562 trader.borrow_mut().add_actor(controller).unwrap();
563 trader.borrow_mut().start().unwrap();
564
565 (trader, controller_id)
566 }
567
568 #[rstest]
569 fn test_controller_rejects_importable_create_commands() {
570 let (trader, controller_id) = create_running_controller();
571 let controller_actor_id = controller_id.inner();
572
573 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
574 let actor_config = ImportableActorConfig {
575 actor_path: "tests.actors:Actor".to_string(),
576 config_path: "tests.actors:ActorConfig".to_string(),
577 config: HashMap::new(),
578 };
579 let strategy_config = ImportableStrategyConfig {
580 strategy_path: "tests.strategies:Strategy".to_string(),
581 config_path: "tests.strategies:StrategyConfig".to_string(),
582 config: HashMap::new(),
583 };
584
585 let actor_result = controller.execute(
586 CreateActor::new(actor_config, true, UUID4::new(), UnixNanos::default()).into(),
587 );
588 let strategy_result = controller.execute(
589 CreateStrategy::new(strategy_config, true, UUID4::new(), UnixNanos::default()).into(),
590 );
591
592 assert_eq!(
593 actor_result.unwrap_err().to_string(),
594 "CreateActor command for importable actor 'tests.actors:Actor' is not supported by the Rust controller"
595 );
596 assert_eq!(
597 strategy_result.unwrap_err().to_string(),
598 "CreateStrategy command for importable strategy 'tests.strategies:Strategy' is not supported by the Rust controller"
599 );
600
601 drop(controller);
602 trader.borrow_mut().stop().unwrap();
603 trader.borrow_mut().dispose_components().unwrap();
604 }
605
606 #[rstest]
607 fn test_controller_manages_actor_lifecycle_by_message() {
608 let (trader, controller_id) = create_running_controller();
609 let controller_actor_id = controller_id.inner();
610
611 let actor_id = {
612 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
613 controller
614 .create_actor(
615 TestDataActor::new(DataActorConfig {
616 actor_id: Some(ActorId::from("TestActor-001")),
617 ..Default::default()
618 }),
619 false,
620 )
621 .unwrap()
622 };
623
624 assert!(trader.borrow().actor_ids().contains(&actor_id));
625
626 Controller::send(&start_actor_command(actor_id)).unwrap();
627 let actor_registry_id = actor_id.inner();
628 assert_eq!(
629 try_get_actor_unchecked::<TestDataActor>(&actor_registry_id)
630 .unwrap()
631 .state(),
632 ComponentState::Running
633 );
634
635 Controller::send(&stop_actor_command(actor_id)).unwrap();
636 assert_eq!(
637 try_get_actor_unchecked::<TestDataActor>(&actor_registry_id)
638 .unwrap()
639 .state(),
640 ComponentState::Stopped
641 );
642
643 Controller::send(&remove_actor_command(actor_id)).unwrap();
644 assert!(!trader.borrow().actor_ids().contains(&actor_id));
645
646 trader.borrow_mut().stop().unwrap();
647 trader.borrow_mut().dispose_components().unwrap();
648 }
649
650 #[rstest]
651 fn test_controller_manages_strategy_lifecycle_and_exit_market() {
652 let (trader, controller_id) = create_running_controller();
653 let controller_actor_id = controller_id.inner();
654
655 let strategy_id = {
656 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
657 controller
658 .create_strategy(
659 TestStrategy::new(StrategyConfig {
660 strategy_id: Some(StrategyId::from("TestStrategy-001")),
661 order_id_tag: Some("001".to_string()),
662 ..Default::default()
663 }),
664 false,
665 )
666 .unwrap()
667 };
668
669 assert!(trader.borrow().strategy_ids().contains(&strategy_id));
670
671 Controller::send(&start_strategy_command(strategy_id)).unwrap();
672 let strategy_registry_id = strategy_id.inner();
673 assert_eq!(
674 try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id)
675 .unwrap()
676 .state(),
677 ComponentState::Running
678 );
679
680 Controller::send(&ControllerCommand::ExitMarket(strategy_id)).unwrap();
681 assert!(
682 try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id)
683 .unwrap()
684 .is_exiting()
685 );
686
687 Controller::send(&stop_strategy_command(strategy_id)).unwrap();
688 let strategy = try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id).unwrap();
689 assert_eq!(strategy.state(), ComponentState::Stopped);
690 assert!(!strategy.is_exiting());
691 drop(strategy);
692
693 Controller::send(&remove_strategy_command(strategy_id)).unwrap();
694 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
695
696 trader.borrow_mut().stop().unwrap();
697 trader.borrow_mut().dispose_components().unwrap();
698 }
699
700 #[rstest]
701 fn test_controller_create_actor_rolls_back_on_start_failure() {
702 let (trader, controller_id) = create_running_controller();
703 let controller_actor_id = controller_id.inner();
704 let actor_id = ActorId::from("FailingActor-001");
705
706 let result = {
707 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
708 controller.create_actor(
709 FailingStartActor::new(DataActorConfig {
710 actor_id: Some(actor_id),
711 ..Default::default()
712 }),
713 true,
714 )
715 };
716
717 assert!(result.is_err());
718 assert!(
719 result
720 .unwrap_err()
721 .to_string()
722 .contains("Simulated actor start failure")
723 );
724 assert!(!trader.borrow().actor_ids().contains(&actor_id));
725 if let Some(actor) = try_get_actor_unchecked::<FailingStartActor>(&actor_id.inner()) {
726 assert_eq!(actor.state(), ComponentState::Disposed);
727 }
728
729 trader.borrow_mut().stop().unwrap();
730 trader.borrow_mut().dispose_components().unwrap();
731 }
732
733 #[rstest]
734 fn test_controller_create_strategy_rolls_back_on_start_failure() {
735 let (trader, controller_id) = create_running_controller();
736 let controller_actor_id = controller_id.inner();
737 let strategy_id = StrategyId::from("FailingStrategy-001");
738
739 let result = {
740 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
741 controller.create_strategy(
742 FailingStartStrategy::new(StrategyConfig {
743 strategy_id: Some(strategy_id),
744 order_id_tag: Some("001".to_string()),
745 ..Default::default()
746 }),
747 true,
748 )
749 };
750
751 assert!(result.is_err());
752 assert!(
753 result
754 .unwrap_err()
755 .to_string()
756 .contains("Simulated strategy start failure")
757 );
758 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
759
760 if let Some(strategy) =
761 try_get_actor_unchecked::<FailingStartStrategy>(&strategy_id.inner())
762 {
763 assert_eq!(strategy.state(), ComponentState::Disposed);
764 }
765
766 trader.borrow_mut().stop().unwrap();
767 trader.borrow_mut().dispose_components().unwrap();
768 }
769
770 #[rstest]
771 fn test_controller_exit_market_allows_reentrant_controller_commands() {
772 let (trader, controller_id) = create_running_controller();
773 let controller_actor_id = controller_id.inner();
774
775 let helper_actor_id = {
776 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
777 controller
778 .create_actor(
779 TestDataActor::new(DataActorConfig {
780 actor_id: Some(ActorId::from("HelperActor-001")),
781 ..Default::default()
782 }),
783 true,
784 )
785 .unwrap()
786 };
787
788 let strategy_id = {
789 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
790 controller
791 .create_strategy(
792 ReentrantExitStrategy::new(
793 StrategyConfig {
794 strategy_id: Some(StrategyId::from("ReentrantStrategy-001")),
795 order_id_tag: Some("001".to_string()),
796 ..Default::default()
797 },
798 helper_actor_id,
799 ),
800 false,
801 )
802 .unwrap()
803 };
804
805 Controller::send(&start_strategy_command(strategy_id)).unwrap();
806 Controller::send(&ControllerCommand::ExitMarket(strategy_id)).unwrap();
807
808 let helper_actor =
809 try_get_actor_unchecked::<TestDataActor>(&helper_actor_id.inner()).unwrap();
810 assert_eq!(helper_actor.state(), ComponentState::Stopped);
811 drop(helper_actor);
812 assert!(
813 try_get_actor_unchecked::<ReentrantExitStrategy>(&strategy_id.inner())
814 .unwrap()
815 .is_exiting()
816 );
817
818 Controller::send(&stop_strategy_command(strategy_id)).unwrap();
819 Controller::send(&remove_strategy_command(strategy_id)).unwrap();
820 Controller::send(&remove_actor_command(helper_actor_id)).unwrap();
821 trader.borrow_mut().stop().unwrap();
822 trader.borrow_mut().dispose_components().unwrap();
823 }
824
825 #[rstest]
826 fn test_controller_send_fails_after_controller_stop() {
827 let (trader, _) = create_running_controller();
828
829 trader.borrow_mut().stop().unwrap();
830
831 let result = Controller::send(&stop_actor_command(ActorId::from("AnyActor-001")));
832 assert!(result.is_err());
833 assert_eq!(
834 result.unwrap_err().to_string(),
835 "Controller execute endpoint 'Controller.execute' not registered"
836 );
837
838 trader.borrow_mut().dispose_components().unwrap();
839 }
840}