1use std::sync::Arc;
2
3use async_trait::async_trait;
4use tokio::sync::OnceCell;
5
6use camel_api::{
7 CamelError, RuntimeCommand, RuntimeCommandBus, RuntimeCommandResult, RuntimeQuery,
8 RuntimeQueryBus, RuntimeQueryResult,
9};
10
11use crate::lifecycle::application::commands::{
12 CommandDeps, execute_command, handle_register_internal,
13};
14use crate::lifecycle::application::ports::RouteRegistrationPort;
15use crate::lifecycle::application::ports::{
16 CommandDedupPort, EventPublisherPort, InFlightCountResult, ProjectionStorePort,
17 RouteRepositoryPort, RuntimeExecutionPort, RuntimeUnitOfWorkPort,
18};
19use crate::lifecycle::application::queries::{QueryDeps, execute_query};
20use crate::lifecycle::application::route_definition::RouteDefinition;
21use crate::lifecycle::domain::DomainError;
22use camel_component_api::HealthCheckRegistry as HealthCheckRegistryTrait;
23
24impl From<InFlightCountResult> for RuntimeQueryResult {
25 fn from(r: InFlightCountResult) -> Self {
26 match r {
27 InFlightCountResult::InFlightCount { route_id, count } => {
28 RuntimeQueryResult::InFlightCount { route_id, count }
29 }
30 InFlightCountResult::RouteNotFound { route_id } => {
31 RuntimeQueryResult::RouteNotFound { route_id }
32 }
33 }
34 }
35}
36
37pub struct RuntimeBus {
38 repo: Arc<dyn RouteRepositoryPort>,
39 projections: Arc<dyn ProjectionStorePort>,
40 events: Arc<dyn EventPublisherPort>,
41 dedup: Arc<dyn CommandDedupPort>,
42 uow: Option<Arc<dyn RuntimeUnitOfWorkPort>>,
43 execution: Option<Arc<dyn RuntimeExecutionPort>>,
44 health_registry: Option<Arc<dyn HealthCheckRegistryTrait>>,
45 journal_recovered_once: OnceCell<()>,
46}
47
48impl RuntimeBus {
49 pub fn new(
50 repo: Arc<dyn RouteRepositoryPort>,
51 projections: Arc<dyn ProjectionStorePort>,
52 events: Arc<dyn EventPublisherPort>,
53 dedup: Arc<dyn CommandDedupPort>,
54 ) -> Self {
55 Self {
56 repo,
57 projections,
58 events,
59 dedup,
60 uow: None,
61 execution: None,
62 health_registry: None,
63 journal_recovered_once: OnceCell::new(),
64 }
65 }
66
67 pub fn with_uow(mut self, uow: Arc<dyn RuntimeUnitOfWorkPort>) -> Self {
68 self.uow = Some(uow);
69 self
70 }
71
72 pub fn with_execution(mut self, execution: Arc<dyn RuntimeExecutionPort>) -> Self {
73 self.execution = Some(execution);
74 self
75 }
76
77 pub fn with_health_registry(
78 mut self,
79 health_registry: Arc<crate::health_registry::HealthCheckRegistry>,
80 ) -> Self {
81 self.health_registry = Some(health_registry);
82 self
83 }
84
85 pub fn repo(&self) -> &Arc<dyn RouteRepositoryPort> {
86 &self.repo
87 }
88
89 pub async fn reconcile_transient_states(&self) -> Result<(), CamelError> {
93 self.ensure_journal_recovered().await?;
94 let deps = self.deps();
95 crate::lifecycle::application::commands::reconcile_transient_states(&deps).await
96 }
97
98 pub(crate) async fn register_aggregate_only(&self, route_id: String) -> Result<(), CamelError> {
99 self.ensure_journal_recovered().await?;
100 let deps = self.deps();
101 if deps.repo.load(&route_id).await?.is_some() {
102 return Err(CamelError::RouteError(format!(
103 "route '{route_id}' already registered"
104 )));
105 }
106 let (aggregate, events) =
107 crate::lifecycle::domain::RouteRuntimeAggregate::register(route_id.clone());
108 if let Some(uow) = &deps.uow {
109 uow.persist_upsert(
110 aggregate.clone(),
111 None,
112 crate::lifecycle::application::commands::project_from_aggregate(&aggregate),
113 &events,
114 )
115 .await?;
116 } else {
117 deps.repo.save(aggregate.clone()).await?;
118 if let Some(primary_error) =
119 crate::lifecycle::application::commands::upsert_projection_with_reconciliation(
120 &*deps.projections,
121 crate::lifecycle::application::commands::project_from_aggregate(&aggregate),
122 )
123 .await?
124 {
125 deps.events.publish(&events).await?;
126 return Err(CamelError::RouteError(format!(
127 "post-effect reconciliation recovered after runtime persistence error: {primary_error}"
128 )));
129 }
130 deps.events.publish(&events).await?;
131 }
132 Ok(())
133 }
134
135 fn deps(&self) -> CommandDeps {
136 CommandDeps {
137 repo: Arc::clone(&self.repo),
138 projections: Arc::clone(&self.projections),
139 events: Arc::clone(&self.events),
140 uow: self.uow.clone(),
141 execution: self.execution.clone(),
142 health_registry: self.health_registry.clone(),
143 }
144 }
145
146 fn query_deps(&self) -> QueryDeps {
147 QueryDeps {
148 projections: Arc::clone(&self.projections),
149 }
150 }
151
152 async fn ensure_journal_recovered(&self) -> Result<(), CamelError> {
153 let Some(uow) = &self.uow else {
154 return Ok(());
155 };
156
157 self.journal_recovered_once
158 .get_or_try_init(|| async {
159 uow.recover_from_journal().await?;
160 Ok::<(), CamelError>(())
161 })
162 .await?;
163 Ok(())
164 }
165}
166
167#[async_trait]
168impl RuntimeCommandBus for RuntimeBus {
169 async fn execute(&self, cmd: RuntimeCommand) -> Result<RuntimeCommandResult, CamelError> {
170 if let RuntimeCommand::ReloadTlsCerts {
174 scheme, host, port, ..
175 } = &cmd
176 {
177 let registry = camel_component_api::tls_source::TlsReloadRegistry::global();
178 match registry.find(scheme, host, *port) {
179 Some(handler) => {
180 handler.reload().await?;
181 return Ok(RuntimeCommandResult::TlsCertsReloaded {
182 scheme: scheme.clone(),
183 host: host.clone(),
184 port: *port,
185 });
186 }
187 None => {
188 return Err(CamelError::Config(format!(
189 "no TLS server found for {scheme}://{host}:{port}"
190 )));
191 }
192 }
193 }
194 self.ensure_journal_recovered().await?;
197 let command_id = cmd.command_id().to_string();
198 if !self.dedup.first_seen(&command_id).await? {
199 return Ok(RuntimeCommandResult::Duplicate { command_id });
200 }
201 let deps = self.deps();
202 match execute_command(&deps, cmd).await {
203 Ok(result) => Ok(result),
204 Err(err) => {
205 let _ = self.dedup.forget_seen(&command_id).await;
206 Err(err)
207 }
208 }
209 }
210}
211
212#[async_trait]
213impl RuntimeQueryBus for RuntimeBus {
214 async fn ask(&self, query: RuntimeQuery) -> Result<RuntimeQueryResult, CamelError> {
215 self.ensure_journal_recovered().await?;
216
217 match query {
218 RuntimeQuery::InFlightCount { route_id } => {
219 if let Some(execution) = &self.execution {
220 execution
221 .in_flight_count(&route_id)
222 .await
223 .map(|r| r.into())
224 .map_err(Into::into)
225 } else {
226 Ok(RuntimeQueryResult::RouteNotFound { route_id })
227 }
228 }
229 other => {
230 let deps = self.query_deps();
231 execute_query(&deps, other).await
232 }
233 }
234 }
235}
236
237#[async_trait]
238impl RouteRegistrationPort for RuntimeBus {
239 async fn register_route(&self, def: RouteDefinition) -> Result<(), DomainError> {
240 self.ensure_journal_recovered()
241 .await
242 .map_err(|e| DomainError::InvalidState(e.to_string()))?;
243 let deps = self.deps();
244 handle_register_internal(&deps, def)
245 .await
246 .map(|_| ())
247 .map_err(|e| match e {
248 CamelError::RouteError(msg) => DomainError::InvalidState(msg),
249 other => DomainError::InvalidState(other.to_string()),
250 })
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use crate::lifecycle::domain::DomainError;
257
258 use super::*;
259 use std::collections::{HashMap, HashSet};
260 use std::sync::Mutex;
261
262 use crate::lifecycle::application::ports::RouteRegistrationPort as InternalRuntimeCommandBus;
263 use crate::lifecycle::application::ports::RouteStatusProjection;
264 use crate::lifecycle::application::route_definition::RouteDefinition;
265 use crate::lifecycle::domain::{RouteRuntimeAggregate, RuntimeEvent};
266
267 #[derive(Clone, Default)]
268 struct InMemoryTestRepo {
269 routes: Arc<Mutex<HashMap<String, RouteRuntimeAggregate>>>,
270 }
271
272 #[async_trait]
273 impl RouteRepositoryPort for InMemoryTestRepo {
274 async fn load(&self, route_id: &str) -> Result<Option<RouteRuntimeAggregate>, DomainError> {
275 Ok(self
276 .routes
277 .lock()
278 .expect("lock test routes")
279 .get(route_id)
280 .cloned())
281 }
282
283 async fn save(&self, aggregate: RouteRuntimeAggregate) -> Result<(), DomainError> {
284 self.routes
285 .lock()
286 .expect("lock test routes")
287 .insert(aggregate.route_id().to_string(), aggregate);
288 Ok(())
289 }
290
291 async fn save_if_version(
292 &self,
293 aggregate: RouteRuntimeAggregate,
294 expected_version: u64,
295 ) -> Result<(), DomainError> {
296 let route_id = aggregate.route_id().to_string();
297 let mut routes = self.routes.lock().expect("lock test routes");
298 let current = routes.get(&route_id).ok_or_else(|| {
299 DomainError::InvalidState(format!(
300 "optimistic lock conflict for route '{route_id}': route not found"
301 ))
302 })?;
303
304 if current.version() != expected_version {
305 return Err(DomainError::InvalidState(format!(
306 "optimistic lock conflict for route '{route_id}': expected version {expected_version}, actual {}",
307 current.version()
308 )));
309 }
310
311 routes.insert(route_id, aggregate);
312 Ok(())
313 }
314
315 async fn delete(&self, route_id: &str) -> Result<(), DomainError> {
316 self.routes
317 .lock()
318 .expect("lock test routes")
319 .remove(route_id);
320 Ok(())
321 }
322 }
323
324 #[derive(Clone, Default)]
325 struct InMemoryTestProjectionStore {
326 statuses: Arc<Mutex<HashMap<String, RouteStatusProjection>>>,
327 }
328
329 #[async_trait]
330 impl ProjectionStorePort for InMemoryTestProjectionStore {
331 async fn upsert_status(&self, status: RouteStatusProjection) -> Result<(), DomainError> {
332 self.statuses
333 .lock()
334 .expect("lock test statuses")
335 .insert(status.route_id.clone(), status);
336 Ok(())
337 }
338
339 async fn get_status(
340 &self,
341 route_id: &str,
342 ) -> Result<Option<RouteStatusProjection>, DomainError> {
343 Ok(self
344 .statuses
345 .lock()
346 .expect("lock test statuses")
347 .get(route_id)
348 .cloned())
349 }
350
351 async fn list_statuses(&self) -> Result<Vec<RouteStatusProjection>, DomainError> {
352 Ok(self
353 .statuses
354 .lock()
355 .expect("lock test statuses")
356 .values()
357 .cloned()
358 .collect())
359 }
360
361 async fn remove_status(&self, route_id: &str) -> Result<(), DomainError> {
362 self.statuses
363 .lock()
364 .expect("lock test statuses")
365 .remove(route_id);
366 Ok(())
367 }
368 }
369
370 #[derive(Clone, Default)]
371 struct InMemoryTestEventPublisher;
372
373 #[async_trait]
374 impl EventPublisherPort for InMemoryTestEventPublisher {
375 async fn publish(&self, _events: &[RuntimeEvent]) -> Result<(), DomainError> {
376 Ok(())
377 }
378 }
379
380 #[derive(Clone, Default)]
381 struct InMemoryTestDedup {
382 seen: Arc<Mutex<HashSet<String>>>,
383 }
384
385 #[derive(Clone, Default)]
386 struct InspectableDedup {
387 seen: Arc<Mutex<HashSet<String>>>,
388 forget_calls: Arc<Mutex<u32>>,
389 }
390
391 #[async_trait]
392 impl CommandDedupPort for InMemoryTestDedup {
393 async fn first_seen(&self, command_id: &str) -> Result<bool, DomainError> {
394 let mut seen = self.seen.lock().expect("lock dedup set");
395 Ok(seen.insert(command_id.to_string()))
396 }
397
398 async fn forget_seen(&self, command_id: &str) -> Result<(), DomainError> {
399 self.seen.lock().expect("lock dedup set").remove(command_id);
400 Ok(())
401 }
402 }
403
404 #[async_trait]
405 impl CommandDedupPort for InspectableDedup {
406 async fn first_seen(&self, command_id: &str) -> Result<bool, DomainError> {
407 let mut seen = self.seen.lock().expect("lock dedup set");
408 Ok(seen.insert(command_id.to_string()))
409 }
410
411 async fn forget_seen(&self, command_id: &str) -> Result<(), DomainError> {
412 self.seen.lock().expect("lock dedup set").remove(command_id);
413 let mut calls = self.forget_calls.lock().expect("forget calls");
414 *calls += 1;
415 Ok(())
416 }
417 }
418
419 fn build_test_runtime_bus() -> RuntimeBus {
420 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
421 let projections: Arc<dyn ProjectionStorePort> =
422 Arc::new(InMemoryTestProjectionStore::default());
423 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
424 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
425 RuntimeBus::new(repo, projections, events, dedup)
426 }
427
428 #[derive(Default)]
429 struct CountingUow {
430 recover_calls: Arc<Mutex<u32>>,
431 }
432
433 #[derive(Default)]
434 struct FailingRecoverUow;
435
436 #[async_trait]
437 impl RuntimeUnitOfWorkPort for CountingUow {
438 async fn persist_upsert(
439 &self,
440 _aggregate: RouteRuntimeAggregate,
441 _expected_version: Option<u64>,
442 _projection: RouteStatusProjection,
443 _events: &[RuntimeEvent],
444 ) -> Result<(), DomainError> {
445 Ok(())
446 }
447
448 async fn persist_delete(
449 &self,
450 _route_id: &str,
451 _events: &[RuntimeEvent],
452 ) -> Result<(), DomainError> {
453 Ok(())
454 }
455
456 async fn recover_from_journal(&self) -> Result<(), DomainError> {
457 let mut calls = self.recover_calls.lock().expect("recover_calls");
458 *calls += 1;
459 Ok(())
460 }
461 }
462
463 #[async_trait]
464 impl RuntimeUnitOfWorkPort for FailingRecoverUow {
465 async fn persist_upsert(
466 &self,
467 _aggregate: RouteRuntimeAggregate,
468 _expected_version: Option<u64>,
469 _projection: RouteStatusProjection,
470 _events: &[RuntimeEvent],
471 ) -> Result<(), DomainError> {
472 Ok(())
473 }
474
475 async fn persist_delete(
476 &self,
477 _route_id: &str,
478 _events: &[RuntimeEvent],
479 ) -> Result<(), DomainError> {
480 Ok(())
481 }
482
483 async fn recover_from_journal(&self) -> Result<(), DomainError> {
484 Err(DomainError::InvalidState("recover failed".into()))
485 }
486 }
487
488 #[derive(Default)]
489 struct InFlightExecutionPort;
490
491 #[async_trait]
492 impl RuntimeExecutionPort for InFlightExecutionPort {
493 async fn register_route(&self, _definition: RouteDefinition) -> Result<(), DomainError> {
494 Ok(())
495 }
496 async fn start_route(&self, _route_id: &str) -> Result<(), DomainError> {
497 Ok(())
498 }
499 async fn stop_route(&self, _route_id: &str) -> Result<(), DomainError> {
500 Ok(())
501 }
502 async fn suspend_route(&self, _route_id: &str) -> Result<(), DomainError> {
503 Ok(())
504 }
505 async fn resume_route(&self, _route_id: &str) -> Result<(), DomainError> {
506 Ok(())
507 }
508 async fn reload_route(&self, _route_id: &str) -> Result<(), DomainError> {
509 Ok(())
510 }
511 async fn remove_route(&self, _route_id: &str) -> Result<(), DomainError> {
512 Ok(())
513 }
514 async fn in_flight_count(
515 &self,
516 route_id: &str,
517 ) -> Result<InFlightCountResult, DomainError> {
518 if route_id == "known" {
519 Ok(InFlightCountResult::InFlightCount {
520 route_id: route_id.to_string(),
521 count: 3,
522 })
523 } else {
524 Ok(InFlightCountResult::RouteNotFound {
525 route_id: route_id.to_string(),
526 })
527 }
528 }
529 }
530
531 #[tokio::test]
532 async fn runtime_bus_implements_internal_command_bus() {
533 let bus = build_test_runtime_bus();
534 let def = RouteDefinition::new("timer:test", vec![]).with_route_id("internal-route");
535 let result = InternalRuntimeCommandBus::register_route(&bus, def).await;
536 assert!(
537 result.is_ok(),
538 "internal bus registration failed: {:?}",
539 result
540 );
541
542 let status = bus
543 .ask(RuntimeQuery::GetRouteStatus {
544 route_id: "internal-route".to_string(),
545 })
546 .await
547 .unwrap();
548 match status {
549 RuntimeQueryResult::RouteStatus { status, .. } => {
550 assert_eq!(status, "Registered");
551 }
552 _ => panic!("unexpected query result"),
553 }
554 }
555
556 #[tokio::test]
557 async fn execute_returns_duplicate_for_replayed_command_id() {
558 use camel_api::runtime::{CanonicalRouteSpec, CanonicalStepSpec, RuntimeCommand};
559
560 let bus = build_test_runtime_bus();
561
562 let mut spec = CanonicalRouteSpec::new("dup-route", "timer:tick");
563 spec.steps = vec![CanonicalStepSpec::Stop];
564
565 let cmd = RuntimeCommand::RegisterRoute {
566 spec: spec.clone(),
567 command_id: "dup-cmd".into(),
568 causation_id: None,
569 };
570 let first = bus.execute(cmd).await.unwrap();
571 assert!(matches!(
572 first,
573 RuntimeCommandResult::RouteRegistered { route_id } if route_id == "dup-route"
574 ));
575
576 let second = bus
577 .execute(RuntimeCommand::RegisterRoute {
578 spec,
579 command_id: "dup-cmd".into(),
580 causation_id: None,
581 })
582 .await
583 .unwrap();
584 assert!(matches!(
585 second,
586 RuntimeCommandResult::Duplicate { command_id } if command_id == "dup-cmd"
587 ));
588 }
589
590 #[tokio::test]
591 async fn ask_in_flight_count_without_execution_returns_route_not_found() {
592 let bus = build_test_runtime_bus();
593 let res = bus
594 .ask(RuntimeQuery::InFlightCount {
595 route_id: "missing".into(),
596 })
597 .await
598 .unwrap();
599 assert!(matches!(
600 res,
601 RuntimeQueryResult::RouteNotFound { route_id } if route_id == "missing"
602 ));
603 }
604
605 #[tokio::test]
606 async fn ask_in_flight_count_with_execution_delegates_to_adapter() {
607 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
608 let projections: Arc<dyn ProjectionStorePort> =
609 Arc::new(InMemoryTestProjectionStore::default());
610 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
611 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
612 let execution: Arc<dyn RuntimeExecutionPort> = Arc::new(InFlightExecutionPort);
613 let bus = RuntimeBus::new(repo, projections, events, dedup).with_execution(execution);
614
615 let known = bus
616 .ask(RuntimeQuery::InFlightCount {
617 route_id: "known".into(),
618 })
619 .await
620 .unwrap();
621 assert!(matches!(
622 known,
623 RuntimeQueryResult::InFlightCount { route_id, count }
624 if route_id == "known" && count == 3
625 ));
626 }
627
628 #[tokio::test]
629 async fn journal_recovery_runs_once_even_with_multiple_commands() {
630 use camel_api::runtime::{CanonicalRouteSpec, CanonicalStepSpec, RuntimeCommand};
631
632 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
633 let projections: Arc<dyn ProjectionStorePort> =
634 Arc::new(InMemoryTestProjectionStore::default());
635 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
636 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
637 let uow = Arc::new(CountingUow::default());
638 let bus = RuntimeBus::new(repo, projections, events, dedup).with_uow(uow.clone());
639
640 let mut spec_a = CanonicalRouteSpec::new("a", "timer:a");
641 spec_a.steps = vec![CanonicalStepSpec::Stop];
642 let mut spec_b = CanonicalRouteSpec::new("b", "timer:b");
643 spec_b.steps = vec![CanonicalStepSpec::Stop];
644
645 bus.execute(RuntimeCommand::RegisterRoute {
646 spec: spec_a,
647 command_id: "c-a".into(),
648 causation_id: None,
649 })
650 .await
651 .unwrap();
652
653 bus.execute(RuntimeCommand::RegisterRoute {
654 spec: spec_b,
655 command_id: "c-b".into(),
656 causation_id: None,
657 })
658 .await
659 .unwrap();
660
661 let calls = *uow.recover_calls.lock().expect("recover calls");
662 assert_eq!(calls, 1, "journal recovery should run once");
663 }
664
665 #[tokio::test]
666 async fn execute_on_command_error_forgets_dedup_marker() {
667 use camel_api::runtime::{CanonicalRouteSpec, RuntimeCommand};
668
669 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
670 let projections: Arc<dyn ProjectionStorePort> =
671 Arc::new(InMemoryTestProjectionStore::default());
672 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
673 let dedup = Arc::new(InspectableDedup::default());
674 let dedup_port: Arc<dyn CommandDedupPort> = dedup.clone();
675
676 let bus = RuntimeBus::new(repo, projections, events, dedup_port);
677
678 let cmd = RuntimeCommand::RegisterRoute {
680 spec: CanonicalRouteSpec::new("", "timer:tick"),
681 command_id: "err-cmd".into(),
682 causation_id: None,
683 };
684
685 let err = bus.execute(cmd).await.expect_err("must fail");
686 assert!(err.to_string().contains("route_id cannot be empty"));
687
688 assert_eq!(*dedup.forget_calls.lock().expect("forget calls"), 1);
689 assert!(!dedup.seen.lock().expect("seen").contains("err-cmd"));
690 }
691
692 #[tokio::test]
693 async fn execute_propagates_recover_error_from_uow() {
694 use camel_api::runtime::{CanonicalRouteSpec, CanonicalStepSpec, RuntimeCommand};
695
696 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
697 let projections: Arc<dyn ProjectionStorePort> =
698 Arc::new(InMemoryTestProjectionStore::default());
699 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
700 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
701 let uow: Arc<dyn RuntimeUnitOfWorkPort> = Arc::new(FailingRecoverUow);
702
703 let bus = RuntimeBus::new(repo, projections, events, dedup).with_uow(uow);
704
705 let mut spec = CanonicalRouteSpec::new("x", "timer:x");
706 spec.steps = vec![CanonicalStepSpec::Stop];
707 let err = bus
708 .execute(RuntimeCommand::RegisterRoute {
709 spec,
710 command_id: "recover-err".into(),
711 causation_id: None,
712 })
713 .await
714 .expect_err("recover should fail");
715
716 assert!(err.to_string().contains("recover failed"));
717 }
718
719 #[tokio::test]
720 async fn ask_in_flight_count_with_execution_handles_unknown_route() {
721 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
722 let projections: Arc<dyn ProjectionStorePort> =
723 Arc::new(InMemoryTestProjectionStore::default());
724 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
725 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
726 let execution: Arc<dyn RuntimeExecutionPort> = Arc::new(InFlightExecutionPort);
727 let bus = RuntimeBus::new(repo, projections, events, dedup).with_execution(execution);
728
729 let unknown = bus
730 .ask(RuntimeQuery::InFlightCount {
731 route_id: "unknown".into(),
732 })
733 .await
734 .unwrap();
735 assert!(matches!(
736 unknown,
737 RuntimeQueryResult::RouteNotFound { route_id } if route_id == "unknown"
738 ));
739 }
740}