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