Skip to main content

camel_core/lifecycle/adapters/step_compilers/
mod.rs

1//! StepCompiler registry pattern — extract from step_resolution.rs
2//!
3//! Each compiler group is responsible for matching specific `BuilderStep` variants.
4//! The registry dispatches each step to compilers in registration order; the first
5//! compiler that returns `Matched` wins. Compilers that don't handle a variant
6//! return `NotHandled(step)` to pass it to the next compiler.
7
8use std::sync::Arc;
9
10use camel_api::{
11    BodyType, BoxProcessor, CamelError, FunctionInvoker, ProducerContext, SpanKindHint,
12    StepLifecycle,
13};
14use camel_component_api::{ComponentContext, RuntimeObservability};
15use camel_endpoint::parse_uri;
16
17use crate::intercept::InterceptRules;
18use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
19use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
20use crate::lifecycle::application::route_definition::BuilderStep;
21use crate::{CacheRegistry, ClaimCheckRegistry, IdempotentRegistry};
22use camel_bean::BeanRegistry;
23
24mod control_flow;
25mod core;
26mod endpoints;
27mod error_handling;
28mod routing;
29mod splitting;
30mod transforms;
31
32/// A compiled pipeline step.
33///
34/// `Process` is the normal case: a boxed processor plus its optional body
35/// contract. `Stop` (added in Task 3b) is the Stop EIP marker — `run_steps`
36/// recognises it and produces `PipelineOutcome::Stopped` without invoking a
37/// Tower service. `Segment` (added in Task 3) wraps an `OutcomeSegment` for
38/// structural EIPs with outcome-aware sub-pipelines.
39///
40/// **Boundary:** `CompiledStep` is the compile-time representation. At runtime,
41/// `run_steps` consumes a `Vec<CompiledStep>` (Stop variants included) and
42/// produces a `PipelineOutcome`; the wrapping `Service<Exchange>` impl
43/// translates `PipelineOutcome` back to `Result<Exchange, CamelError>`. See
44/// ADR-0024.
45#[derive(Debug, Clone)]
46pub enum CompiledStep {
47    Process {
48        processor: BoxProcessor,
49        body_contract: Option<BodyType>,
50        /// Lifecycle handle for this processor, if it is stateful.
51        /// `None` for stateless processors (the common case).
52        lifecycle: Option<Arc<dyn StepLifecycle>>,
53        /// Stable span name (e.g. `to:direct`, `split`), stamped by
54        /// `StepCompilerRegistry::compile_step` from
55        /// `BuilderStep::span_label`. `None` for anonymous steps — spans
56        /// fall back to the positional `step-{index}` name. Read by
57        /// `compose_traced_pipeline` / `segment_span` in route_compiler.rs.
58        label: Option<Arc<str>>,
59        /// Span kind for this step's step span, stamped by
60        /// `StepCompilerRegistry::compile_step` from
61        /// `BuilderStep::span_kind_hint`. `Internal` for steps built
62        /// outside the registry. Read by `compose_traced_pipeline` in
63        /// route_compiler.rs.
64        kind_hint: SpanKindHint,
65    },
66    /// Stop EIP marker. `run_steps` produces `PipelineOutcome::Stopped(ex)`
67    /// without invoking a Tower service. Replaces `StopService` (Task 7).
68    Stop,
69    /// Outcome-aware structural EIP segment. `run_steps` invokes
70    /// `segment.run(ex)` and matches on the returned `PipelineOutcome`.
71    /// See ADR-0025.
72    Segment {
73        segment: camel_api::OutcomeSegment,
74        body_contract: Option<BodyType>,
75        /// Lifecycle handles from children nested inside this segment.
76        /// `Option<Vec<...>>` (not `Option<Arc<...>>`) because multiple stateful
77        /// children (e.g. Idempotent+Resequencer inside Filter) each
78        /// register independently.
79        lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
80        /// Stable span name, same contract as [`CompiledStep::Process`]'s
81        /// `label` field.
82        label: Option<Arc<str>>,
83    },
84}
85
86impl CompiledStep {
87    /// Overwrite the span label of a `Process`/`Segment` step; no-op on `Stop`
88    /// (the marker produces no step span, so it carries no label).
89    pub(crate) fn set_label(&mut self, label: Option<String>) {
90        let label = label.map(Arc::from);
91        match self {
92            CompiledStep::Process { label: slot, .. }
93            | CompiledStep::Segment { label: slot, .. } => {
94                *slot = label;
95            }
96            CompiledStep::Stop => {}
97        }
98    }
99
100    /// Overwrite the span kind hint of a `Process` step; no-op on `Stop` and
101    /// `Segment` (neither produces an individual step span, so neither
102    /// carries a kind hint).
103    pub(crate) fn set_kind_hint(&mut self, hint: SpanKindHint) {
104        match self {
105            CompiledStep::Process {
106                kind_hint: slot, ..
107            } => *slot = hint,
108            CompiledStep::Stop | CompiledStep::Segment { .. } => {}
109        }
110    }
111}
112
113/// Result from a compiler: either it handled the step, or it did not recognize
114/// the variant and returns the step for the next compiler.
115///
116/// `Result` is intentionally outside this enum (returned by `StepCompiler::compile`)
117/// so that compiler arms can use the `?` operator for propagated errors instead of
118/// wrapping every `Err` in `Matched(Err(e))`.
119pub(crate) enum CompileOutcome {
120    Matched(CompiledStep),
121    NotHandled(BuilderStep),
122}
123
124/// A compiler that can handle one or more `BuilderStep` variants.
125///
126/// The `compile` method receives ownership of the step. If the compiler recognizes
127/// the variant it returns `Ok(CompileOutcome::Matched(...))`. Otherwise it returns
128/// `Ok(CompileOutcome::NotHandled(step))` to pass the step to the next compiler.
129/// Compilation errors are returned as `Err(CamelError)`.
130pub(crate) trait StepCompiler: Send + Sync {
131    fn compile(
132        &self,
133        step: BuilderStep,
134        step_index: usize,
135        ctx: &CompilationContext,
136        registry: &StepCompilerRegistry,
137    ) -> Result<CompileOutcome, CamelError>;
138}
139
140/// Shared context passed to every compiler invocation.
141pub(crate) struct CompilationContext<'a> {
142    pub producer_ctx: &'a ProducerContext,
143    pub rt: Arc<dyn RuntimeObservability>,
144    pub languages: &'a SharedLanguageRegistry,
145    pub beans: &'a Arc<std::sync::Mutex<BeanRegistry>>,
146    pub function_invoker: Option<Arc<dyn FunctionInvoker>>,
147    pub component_ctx: Arc<dyn ComponentContext>,
148    pub route_id: Option<&'a str>,
149    pub staging_mode: &'a FunctionStagingMode,
150    /// Idempotent repository registry. Used by the `IdempotentConsumer`
151    /// compiler arm to resolve repository names into `Arc<dyn IdempotentRepository>`.
152    pub idempotent_repositories: &'a IdempotentRegistry,
153    /// Claim check repository registry. Used by the `ClaimCheck` compiler arm
154    /// to resolve repository names into `Arc<dyn ClaimCheckRepository>`.
155    pub claim_check_repositories: &'a ClaimCheckRegistry,
156    /// Cache repository registry. Used by the `Cache`, `CacheInvalidate`, and
157    /// `CachePeekStale` compiler arms to resolve repository names into
158    /// `Arc<dyn CacheRepository>`.
159    pub cache_repositories: &'a CacheRegistry,
160    /// Route send-point interception rules captured at compile time.
161    /// Consulted by send-step compilation (Task 4/5).
162    pub intercept: InterceptRules,
163}
164
165impl<'a> CompilationContext<'a> {
166    /// Recursively compile child steps. Used by compilers that have sub-pipelines
167    /// (Filter, Choice, Split, Loop, etc.).
168    pub fn compile_children(
169        &self,
170        steps: Vec<BuilderStep>,
171        registry: &StepCompilerRegistry,
172    ) -> Result<Vec<CompiledStep>, CamelError> {
173        registry.compile_steps(steps, self)
174    }
175
176    /// Recursively compile child steps and map them into outcome-aware segments.
177    ///
178    /// Each `CompiledStep` variant is converted to a `Box<dyn OutcomePipeline>`:
179    /// - `Process` → `BoxProcessorSegment`, optionally wrapped in `BodyCoercingSegment`
180    /// - `Stop` → `StopSegment` (produces `PipelineOutcome::Stopped(ex)`)
181    /// - `Segment` → its inner `OutcomeSegment` (which now implements OutcomePipeline)
182    ///
183    /// This replaces the 22-line duplicated closure in Filter/DeclarativeFilter
184    /// (and will prevent 14+ more duplicates in T9–T16).
185    #[allow(clippy::type_complexity)]
186    pub fn compile_children_segments(
187        &self,
188        steps: Vec<BuilderStep>,
189        registry: &StepCompilerRegistry,
190    ) -> Result<
191        (
192            Vec<Box<dyn camel_api::OutcomePipeline>>,
193            Vec<Arc<dyn camel_api::StepLifecycle>>,
194        ),
195        CamelError,
196    > {
197        let pairs = self.compile_children(steps, registry)?;
198        let mut lifecycle_handles: Vec<Arc<dyn camel_api::StepLifecycle>> = Vec::new();
199        let segments: Vec<Box<dyn camel_api::OutcomePipeline>> = pairs
200            .into_iter()
201            .map(|c| match c {
202                CompiledStep::Process {
203                    processor,
204                    body_contract,
205                    lifecycle,
206                    label: _,
207                    kind_hint: _,
208                } => {
209                    if let Some(lc) = lifecycle {
210                        lifecycle_handles.push(lc);
211                    }
212                    let inner: Box<dyn camel_api::OutcomePipeline> = Box::new(
213                        crate::lifecycle::adapters::route_compiler::BoxProcessorSegment::new(
214                            processor,
215                        ),
216                    );
217                    match body_contract {
218                        Some(contract) => Box::new(
219                            crate::lifecycle::adapters::route_compiler::BodyCoercingSegment::new(
220                                inner, contract,
221                            ),
222                        ),
223                        None => inner,
224                    }
225                }
226                CompiledStep::Stop => {
227                    Box::new(crate::lifecycle::adapters::route_compiler::StopSegment)
228                        as Box<dyn camel_api::OutcomePipeline>
229                }
230                CompiledStep::Segment {
231                    segment,
232                    body_contract: _,
233                    lifecycle,
234                    label: _,
235                } => {
236                    if let Some(lcs) = lifecycle {
237                        lifecycle_handles.extend(lcs);
238                    }
239                    Box::new(segment)
240                }
241            })
242            .collect();
243        Ok((segments, lifecycle_handles))
244    }
245}
246
247/// Registry of step compilers. Steps are dispatched to compilers in registration
248/// order. The first matching compiler handles the step.
249pub(crate) struct StepCompilerRegistry {
250    compilers: Vec<Box<dyn StepCompiler>>,
251}
252
253impl StepCompilerRegistry {
254    pub fn new() -> Self {
255        Self {
256            compilers: Vec::new(),
257        }
258    }
259
260    pub fn register(&mut self, compiler: Box<dyn StepCompiler>) {
261        self.compilers.push(compiler);
262    }
263
264    /// Try each compiler in order. The first to return `Matched` wins.
265    /// If all return `NotHandled`, returns `Ok(None)`. Compilation errors
266    /// short-circuit and return `Err(CamelError)`.
267    pub fn compile_step(
268        &self,
269        step: BuilderStep,
270        step_index: usize,
271        ctx: &CompilationContext,
272    ) -> Result<Option<CompiledStep>, CamelError> {
273        // Capture the span label and kind hint BEFORE the dispatch loop
274        // moves the step.
275        let label = step.span_label();
276        let kind_hint = step.span_kind_hint();
277        let mut step = step;
278        for compiler in &self.compilers {
279            match compiler.compile(step, step_index, ctx, self)? {
280                CompileOutcome::Matched(mut s) => {
281                    s.set_label(label);
282                    s.set_kind_hint(kind_hint);
283                    return Ok(Some(s));
284                }
285                CompileOutcome::NotHandled(s) => step = s,
286            }
287        }
288        Ok(None)
289    }
290
291    /// Compile all steps in a vector.
292    pub fn compile_steps(
293        &self,
294        steps: Vec<BuilderStep>,
295        ctx: &CompilationContext,
296    ) -> Result<Vec<CompiledStep>, CamelError> {
297        let mut out = Vec::with_capacity(steps.len());
298        for (i, step) in steps.into_iter().enumerate() {
299            match self.compile_step(step, i, ctx)? {
300                Some(c) => out.push(c),
301                None => {
302                    return Err(CamelError::RouteError(
303                        "no compiler registered for step variant".into(),
304                    ));
305                }
306            }
307        }
308        Ok(out)
309    }
310}
311
312/// A resolved send target: the pieces the un-intercepted `To` path produces
313/// for one URI.
314pub(super) struct ResolvedSend {
315    pub producer: BoxProcessor,
316    pub body_contract: Option<BodyType>,
317    pub lifecycle: Option<Arc<dyn StepLifecycle>>,
318}
319
320/// Resolve a send URI into producer/body-contract/lifecycle, exactly as the
321/// un-intercepted `To` path does. Canonical resolution path for send steps;
322/// [`resolve_producer_with_lifecycle`] and [`resolve_producer`] are thin
323/// wrappers over this.
324pub(super) fn resolve_send(
325    ctx: &CompilationContext,
326    uri: &str,
327) -> Result<ResolvedSend, CamelError> {
328    let parsed = parse_uri(uri)?;
329    let component = ctx
330        .component_ctx
331        .resolve_component(&parsed.scheme)
332        .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
333    let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
334    let body_contract = endpoint.body_contract();
335    let producer = endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)?;
336    // Capture the endpoint's lifecycle handle so the route controller can
337    // start/shut it down in route order (ADR-0022). Default is `None` for
338    // stateless endpoints — see `Endpoint::lifecycle` in camel-component-api.
339    let lifecycle: Option<Arc<dyn StepLifecycle>> = endpoint.lifecycle();
340    Ok(ResolvedSend {
341        producer,
342        body_contract,
343        lifecycle,
344    })
345}
346
347/// Parse a URI and create a producer, also returning the endpoint's
348/// [`StepLifecycle`] handle (if any). Thin wrapper over [`resolve_send`] that
349/// discards the body contract. Use directly when the lifecycle is needed
350/// (e.g. `WireTap` propagation).
351pub(crate) fn resolve_producer_with_lifecycle(
352    ctx: &CompilationContext,
353    uri: &str,
354) -> Result<(BoxProcessor, Option<Arc<dyn StepLifecycle>>), CamelError> {
355    let resolved = resolve_send(ctx, uri)?;
356    Ok((resolved.producer, resolved.lifecycle))
357}
358
359/// Parse a URI and create a producer, reusing `component_ctx`, `rt`, and `producer_ctx`
360/// from the compilation context.
361///
362/// Thin wrapper over [`resolve_producer_with_lifecycle`] that discards the
363/// endpoint's [`StepLifecycle`] handle.
364pub(crate) fn resolve_producer(
365    ctx: &CompilationContext,
366    uri: &str,
367) -> Result<BoxProcessor, CamelError> {
368    Ok(resolve_producer_with_lifecycle(ctx, uri)?.0)
369}
370
371/// Pack a lifecycle Vec into `None` when empty, `Some` when non-empty.
372/// Preserves the invariant that `Some` always implies ≥1 handle.
373pub(super) fn pack_lifecycles(
374    lifecycles: Vec<Arc<dyn StepLifecycle>>,
375) -> Option<Vec<Arc<dyn StepLifecycle>>> {
376    if lifecycles.is_empty() {
377        None
378    } else {
379        Some(lifecycles)
380    }
381}
382
383/// Build the full registry with all compiler groups.
384pub(crate) fn build_registry() -> StepCompilerRegistry {
385    let mut reg = StepCompilerRegistry::new();
386    reg.register(Box::new(core::CoreCompiler));
387    reg.register(Box::new(endpoints::EndpointsCompiler));
388    reg.register(Box::new(transforms::TransformsCompiler));
389    reg.register(Box::new(routing::RoutingCompiler));
390    reg.register(Box::new(control_flow::ControlFlowCompiler));
391    reg.register(Box::new(splitting::SplittingCompiler));
392    reg.register(Box::new(error_handling::ErrorHandlingCompiler));
393    reg
394}
395
396#[cfg(test)]
397mod segment_tests {
398    use super::*;
399    use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
400    use std::future::Future;
401    use std::pin::Pin;
402
403    #[derive(Clone)]
404    struct EchoSegment;
405
406    impl OutcomePipeline for EchoSegment {
407        fn clone_box(&self) -> Box<dyn OutcomePipeline> {
408            Box::new(EchoSegment)
409        }
410        fn run<'a>(
411            &'a mut self,
412            exchange: Exchange,
413        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
414            Box::pin(async move { PipelineOutcome::Completed(exchange) })
415        }
416    }
417
418    #[test]
419    fn compiled_step_segment_clone_compiles() {
420        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
421        let step = CompiledStep::Segment {
422            segment: seg,
423            body_contract: None,
424            lifecycle: None,
425            label: None,
426        };
427        let _cloned = step.clone();
428        if let CompiledStep::Segment { .. } = _cloned {
429            // ok
430        } else {
431            panic!("clone should preserve variant");
432        }
433    }
434
435    #[test]
436    fn compiled_step_segment_debug_renders() {
437        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
438        let step = CompiledStep::Segment {
439            segment: seg,
440            body_contract: None,
441            lifecycle: None,
442            label: None,
443        };
444        let s = format!("{:?}", step);
445        assert!(
446            s.contains("Segment"),
447            "debug should mention Segment variant: {s}"
448        );
449    }
450
451    #[test]
452    fn outcome_segment_satisfies_clone_send_static() {
453        fn assert_traits<T: Clone + Send + 'static>() {}
454        assert_traits::<camel_api::OutcomeSegment>();
455    }
456
457    #[tokio::test]
458    #[allow(clippy::arc_with_non_send_sync)]
459    async fn outcome_segment_survives_arcswap_swap() {
460        use arc_swap::ArcSwap;
461        use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
462        use std::sync::Arc;
463
464        #[derive(Clone)]
465        struct EchoSegment;
466        impl OutcomePipeline for EchoSegment {
467            fn clone_box(&self) -> Box<dyn OutcomePipeline> {
468                Box::new(EchoSegment)
469            }
470            fn run<'a>(
471                &'a mut self,
472                ex: Exchange,
473            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
474            {
475                Box::pin(async move { PipelineOutcome::Completed(ex) })
476            }
477        }
478
479        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
480        let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
481        slot.store(Arc::new(Some(seg.clone())));
482        slot.store(Arc::new(Some(seg)));
483
484        let mut borrowed = slot.load().as_ref().clone().unwrap();
485        let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
486        assert!(matches!(outcome, PipelineOutcome::Completed(_)));
487    }
488
489    /// Test lifecycle handle used by compile_children_segments_bubbles_child_lifecycle.
490    #[derive(Debug)]
491    struct TestLifecycle;
492
493    #[async_trait::async_trait]
494    impl camel_api::StepLifecycle for TestLifecycle {
495        fn name(&self) -> &'static str {
496            "test-lifecycle"
497        }
498        async fn shutdown(
499            &self,
500            _reason: camel_api::StepShutdownReason,
501        ) -> Result<(), camel_api::CamelError> {
502            Ok(())
503        }
504    }
505
506    /// Custom compiler that injects a lifecycle handle into every
507    /// `BuilderStep::Processor` it compiles.
508    struct LifecycleInjectorCompiler {
509        handle: Arc<dyn camel_api::StepLifecycle>,
510    }
511
512    impl StepCompiler for LifecycleInjectorCompiler {
513        fn compile(
514            &self,
515            step: BuilderStep,
516            _step_index: usize,
517            _ctx: &CompilationContext,
518            _registry: &StepCompilerRegistry,
519        ) -> Result<CompileOutcome, CamelError> {
520            match step {
521                BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
522                    processor: op.0,
523                    body_contract: None,
524                    lifecycle: Some(self.handle.clone()),
525                    label: None,
526                    kind_hint: SpanKindHint::Internal,
527                })),
528                other => Ok(CompileOutcome::NotHandled(other)),
529            }
530        }
531    }
532
533    #[tokio::test]
534    async fn compile_children_segments_bubbles_child_lifecycle() {
535        use std::collections::HashMap;
536        use std::sync::Mutex;
537
538        use camel_api::{
539            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
540        };
541        use camel_bean::BeanRegistry;
542        use camel_component_api::{
543            ComponentContext, NoOpComponentContext, RuntimeObservability,
544            test_support::NoopRuntimeObservability,
545        };
546
547        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
548
549        let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
550
551        // Register lifecycle injector + real control-flow compiler so
552        // compile_children_segments runs through a structural EIP path.
553        let mut reg = StepCompilerRegistry::new();
554        reg.register(Box::new(LifecycleInjectorCompiler {
555            handle: handle.clone(),
556        }));
557        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
558
559        let pc = ProducerContext::default();
560        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
561        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
562        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
563        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
564        let staging = FunctionStagingMode::DirectAdd;
565        let idempotent_repositories = crate::IdempotentRegistry::new();
566        let claim_check_repositories = crate::ClaimCheckRegistry::new();
567        let cache_repositories = crate::CacheRegistry::new();
568
569        let ctx = CompilationContext {
570            producer_ctx: &pc,
571            rt,
572            languages: &languages,
573            beans: &beans,
574            function_invoker: None,
575            component_ctx,
576            route_id: None,
577            staging_mode: &staging,
578            idempotent_repositories: &idempotent_repositories,
579            claim_check_repositories: &claim_check_repositories,
580            cache_repositories: &cache_repositories,
581            intercept: InterceptRules::default(),
582        };
583
584        // Compile a Filter with a child Processor step.
585        let filter_step = BuilderStep::Filter {
586            predicate: FilterPredicate::new(|_| true),
587            steps: vec![BuilderStep::Processor(OpaqueProcessor(
588                BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
589            ))],
590        };
591
592        let result = reg.compile_step(filter_step, 0, &ctx);
593        let compiled = result
594            .expect("compilation should succeed")
595            .expect("should match");
596
597        match compiled {
598            CompiledStep::Segment {
599                lifecycle,
600                body_contract,
601                ..
602            } => {
603                assert_eq!(body_contract, None, "body_contract should be None");
604                let handles = lifecycle.expect("Segment should have lifecycle handles");
605                assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
606                assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
607            }
608            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
609        }
610    }
611
612    /// A lifecycle handle with a configurable name for multi-handle tests.
613    #[derive(Debug)]
614    struct NamedLifecycle(&'static str);
615
616    #[async_trait::async_trait]
617    impl camel_api::StepLifecycle for NamedLifecycle {
618        fn name(&self) -> &'static str {
619            self.0
620        }
621        async fn shutdown(
622            &self,
623            _reason: camel_api::StepShutdownReason,
624        ) -> Result<(), camel_api::CamelError> {
625            Ok(())
626        }
627    }
628
629    /// Test A: Multiple stateful children in one Segment → Vec length 2.
630    #[tokio::test]
631    async fn compile_children_segments_multiple_stateful_children() {
632        use std::collections::HashMap;
633        use std::sync::Mutex;
634
635        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
636        use camel_api::{
637            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
638        };
639        use camel_bean::BeanRegistry;
640        use camel_component_api::{
641            ComponentContext, NoOpComponentContext, RuntimeObservability,
642            test_support::NoopRuntimeObservability,
643        };
644
645        let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
646
647        let mut reg = StepCompilerRegistry::new();
648        reg.register(Box::new(LifecycleInjectorCompiler {
649            handle: handle.clone(),
650        }));
651        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
652
653        let pc = ProducerContext::default();
654        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
655        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
656        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
657        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
658        let staging = FunctionStagingMode::DirectAdd;
659        let idempotent_repositories = crate::IdempotentRegistry::new();
660        let claim_check_repositories = crate::ClaimCheckRegistry::new();
661        let cache_repositories = crate::CacheRegistry::new();
662
663        let ctx = CompilationContext {
664            producer_ctx: &pc,
665            rt,
666            languages: &languages,
667            beans: &beans,
668            function_invoker: None,
669            component_ctx,
670            route_id: None,
671            staging_mode: &staging,
672            idempotent_repositories: &idempotent_repositories,
673            claim_check_repositories: &claim_check_repositories,
674            cache_repositories: &cache_repositories,
675            intercept: InterceptRules::default(),
676        };
677
678        // Filter with TWO child Processors → both get the same lifecycle handle.
679        let filter_step = BuilderStep::Filter {
680            predicate: FilterPredicate::new(|_| true),
681            steps: vec![
682                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
683                    Box::pin(async move { Ok(ex) })
684                }))),
685                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
686                    Box::pin(async move { Ok(ex) })
687                }))),
688            ],
689        };
690
691        let result = reg.compile_step(filter_step, 0, &ctx);
692        let compiled = result
693            .expect("compilation should succeed")
694            .expect("should match");
695
696        match compiled {
697            CompiledStep::Segment { lifecycle, .. } => {
698                let handles = lifecycle.expect("Segment should have lifecycle handles");
699                assert_eq!(
700                    handles.len(),
701                    2,
702                    "expected 2 lifecycle handles for 2 children"
703                );
704                for h in &handles {
705                    assert_eq!(h.name(), "multi", "all handles should be 'multi'");
706                }
707            }
708            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
709        }
710    }
711
712    /// Test B: Multi-branch accumulation across Choice when-clauses.
713    #[tokio::test]
714    async fn compile_children_segments_multi_branch_accumulation() {
715        use std::collections::HashMap;
716        use std::sync::Mutex;
717
718        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
719        use crate::lifecycle::application::route_definition::WhenStep;
720        use camel_api::{
721            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
722        };
723        use camel_bean::BeanRegistry;
724        use camel_component_api::{
725            ComponentContext, NoOpComponentContext, RuntimeObservability,
726            test_support::NoopRuntimeObservability,
727        };
728
729        let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
730
731        let mut reg = StepCompilerRegistry::new();
732        reg.register(Box::new(LifecycleInjectorCompiler {
733            handle: handle.clone(),
734        }));
735        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
736
737        let pc = ProducerContext::default();
738        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
739        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
740        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
741        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
742        let staging = FunctionStagingMode::DirectAdd;
743        let idempotent_repositories = crate::IdempotentRegistry::new();
744        let claim_check_repositories = crate::ClaimCheckRegistry::new();
745        let cache_repositories = crate::CacheRegistry::new();
746
747        let ctx = CompilationContext {
748            producer_ctx: &pc,
749            rt,
750            languages: &languages,
751            beans: &beans,
752            function_invoker: None,
753            component_ctx,
754            route_id: None,
755            staging_mode: &staging,
756            idempotent_repositories: &idempotent_repositories,
757            claim_check_repositories: &claim_check_repositories,
758            cache_repositories: &cache_repositories,
759            intercept: InterceptRules::default(),
760        };
761
762        // Choice with 2 when branches, each containing 1 stateful child.
763        let choice_step = BuilderStep::Choice {
764            whens: vec![
765                WhenStep {
766                    predicate: FilterPredicate::new(|_| true),
767                    steps: vec![BuilderStep::Processor(OpaqueProcessor(
768                        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
769                    ))],
770                },
771                WhenStep {
772                    predicate: FilterPredicate::new(|_| false),
773                    steps: vec![BuilderStep::Processor(OpaqueProcessor(
774                        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
775                    ))],
776                },
777            ],
778            otherwise: None,
779        };
780
781        let result = reg.compile_step(choice_step, 0, &ctx);
782        let compiled = result
783            .expect("compilation should succeed")
784            .expect("should match");
785
786        match compiled {
787            CompiledStep::Segment { lifecycle, .. } => {
788                let handles = lifecycle.expect("Segment should have lifecycle handles");
789                assert_eq!(
790                    handles.len(),
791                    2,
792                    "expected 2 lifecycle handles from 2 branches"
793                );
794                for h in &handles {
795                    assert_eq!(h.name(), "branch", "all handles should be 'branch'");
796                }
797            }
798            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
799        }
800    }
801
802    /// Test C: Nested Segment-in-Segment flattening — outer Segment contains
803    /// innermost lifecycle handle from a grandchild Processor.
804    #[tokio::test]
805    async fn compile_children_segments_nested_segment_flattening() {
806        use std::collections::HashMap;
807        use std::sync::Mutex;
808
809        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
810        use camel_api::{
811            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
812        };
813        use camel_bean::BeanRegistry;
814        use camel_component_api::{
815            ComponentContext, NoOpComponentContext, RuntimeObservability,
816            test_support::NoopRuntimeObservability,
817        };
818
819        let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
820
821        let mut reg = StepCompilerRegistry::new();
822        reg.register(Box::new(LifecycleInjectorCompiler {
823            handle: inner_handle.clone(),
824        }));
825        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
826
827        let pc = ProducerContext::default();
828        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
829        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
830        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
831        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
832        let staging = FunctionStagingMode::DirectAdd;
833        let idempotent_repositories = crate::IdempotentRegistry::new();
834        let claim_check_repositories = crate::ClaimCheckRegistry::new();
835        let cache_repositories = crate::CacheRegistry::new();
836
837        let ctx = CompilationContext {
838            producer_ctx: &pc,
839            rt,
840            languages: &languages,
841            beans: &beans,
842            function_invoker: None,
843            component_ctx,
844            route_id: None,
845            staging_mode: &staging,
846            idempotent_repositories: &idempotent_repositories,
847            claim_check_repositories: &claim_check_repositories,
848            cache_repositories: &cache_repositories,
849            intercept: InterceptRules::default(),
850        };
851
852        // Outer Filter containing an inner Filter that has a stateful Processor.
853        // The outer Segment's lifecycle should contain the innermost handle
854        // (proves recursive flattening through compile_children_segments).
855        let inner_filter = BuilderStep::Filter {
856            predicate: FilterPredicate::new(|_| true),
857            steps: vec![BuilderStep::Processor(OpaqueProcessor(
858                BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
859            ))],
860        };
861
862        let outer_filter = BuilderStep::Filter {
863            predicate: FilterPredicate::new(|_| true),
864            steps: vec![inner_filter],
865        };
866
867        let result = reg.compile_step(outer_filter, 0, &ctx);
868        let compiled = result
869            .expect("compilation should succeed")
870            .expect("should match");
871
872        match compiled {
873            CompiledStep::Segment { lifecycle, .. } => {
874                let handles = lifecycle.expect("outer Segment should have lifecycle handles");
875                assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
876                assert_eq!(
877                    handles[0].name(),
878                    "deep",
879                    "handle should be from innermost child"
880                );
881            }
882            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
883        }
884    }
885}
886
887#[cfg(test)]
888mod dispatch_tests {
889    use super::*;
890    use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
891    use camel_api::{BoxProcessor, BoxProcessorExt};
892    use camel_bean::BeanRegistry;
893    use camel_component_api::{
894        ComponentContext, NoOpComponentContext, RuntimeObservability,
895        test_support::NoopRuntimeObservability,
896    };
897    use std::collections::HashMap;
898    use std::sync::Mutex;
899
900    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Stop`.
901    struct ToStopCompiler;
902
903    impl StepCompiler for ToStopCompiler {
904        fn compile(
905            &self,
906            step: BuilderStep,
907            _step_index: usize,
908            _ctx: &CompilationContext,
909            _registry: &StepCompilerRegistry,
910        ) -> Result<CompileOutcome, CamelError> {
911            match step {
912                BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
913                other => Ok(CompileOutcome::NotHandled(other)),
914            }
915        }
916    }
917
918    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Process`.
919    struct ToProcessCompiler;
920
921    impl StepCompiler for ToProcessCompiler {
922        fn compile(
923            &self,
924            step: BuilderStep,
925            _step_index: usize,
926            _ctx: &CompilationContext,
927            _registry: &StepCompilerRegistry,
928        ) -> Result<CompileOutcome, CamelError> {
929            match step {
930                BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
931                    processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
932                    body_contract: None,
933                    lifecycle: None,
934                    label: None,
935                    kind_hint: SpanKindHint::Internal,
936                })),
937                other => Ok(CompileOutcome::NotHandled(other)),
938            }
939        }
940    }
941
942    /// Minimal outcome pipeline for building `Segment` fixtures in tests.
943    #[derive(Clone)]
944    struct NoopPipeline;
945
946    impl camel_api::OutcomePipeline for NoopPipeline {
947        fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
948            Box::new(NoopPipeline)
949        }
950
951        fn run<'a>(
952            &'a mut self,
953            exchange: camel_api::Exchange,
954        ) -> std::pin::Pin<
955            Box<dyn std::future::Future<Output = camel_api::PipelineOutcome> + Send + 'a>,
956        > {
957            Box::pin(async move { camel_api::PipelineOutcome::Completed(exchange) })
958        }
959    }
960
961    /// Compiler that passes all steps through (never handles any variant).
962    /// Returns `NotHandled(step)` for every input, proving by-value pass-through.
963    struct PassThroughCompiler;
964
965    impl StepCompiler for PassThroughCompiler {
966        fn compile(
967            &self,
968            step: BuilderStep,
969            _step_index: usize,
970            _ctx: &CompilationContext,
971            _registry: &StepCompilerRegistry,
972        ) -> Result<CompileOutcome, CamelError> {
973            Ok(CompileOutcome::NotHandled(step))
974        }
975    }
976
977    /// Shared context builder — avoids repeating the 15-line setup in every test.
978    #[allow(clippy::too_many_arguments)]
979    fn ctx<'a>(
980        pc: &'a ProducerContext,
981        rt: Arc<dyn RuntimeObservability>,
982        languages: &'a SharedLanguageRegistry,
983        beans: &'a Arc<Mutex<BeanRegistry>>,
984        component_ctx: Arc<dyn ComponentContext>,
985        staging: &'a FunctionStagingMode,
986        idempotent_repositories: &'a crate::IdempotentRegistry,
987        claim_check_repositories: &'a crate::ClaimCheckRegistry,
988        cache_repositories: &'a crate::CacheRegistry,
989    ) -> CompilationContext<'a> {
990        CompilationContext {
991            producer_ctx: pc,
992            rt,
993            languages,
994            beans,
995            function_invoker: None,
996            component_ctx,
997            route_id: None,
998            staging_mode: staging,
999            idempotent_repositories,
1000            claim_check_repositories,
1001            cache_repositories,
1002            intercept: InterceptRules::default(),
1003        }
1004    }
1005
1006    /// Test that the dispatcher respects registration order: the first matching
1007    /// compiler wins even when a later compiler also matches.
1008    #[test]
1009    fn compile_step_preserves_registry_order() {
1010        let pc = ProducerContext::default();
1011        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1012        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1013        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1014        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1015        let staging = FunctionStagingMode::DirectAdd;
1016        let idempotent_repositories = crate::IdempotentRegistry::new();
1017        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1018        let cache_repositories = crate::CacheRegistry::new();
1019
1020        let context = ctx(
1021            &pc,
1022            rt,
1023            &languages,
1024            &beans,
1025            component_ctx,
1026            &staging,
1027            &idempotent_repositories,
1028            &claim_check_repositories,
1029            &cache_repositories,
1030        );
1031
1032        // Register ToStopCompiler FIRST, ToProcessCompiler SECOND.
1033        // Both match BuilderStep::To. The first-registered should win.
1034        let mut reg = StepCompilerRegistry::new();
1035        reg.register(Box::new(ToStopCompiler));
1036        reg.register(Box::new(ToProcessCompiler));
1037
1038        let result = reg
1039            .compile_step(BuilderStep::To("test".into()), 0, &context)
1040            .expect("compilation should succeed")
1041            .expect("should match");
1042
1043        assert!(
1044            matches!(result, CompiledStep::Stop),
1045            "expected ToStopCompiler (first registered) to win, got {result:?}"
1046        );
1047    }
1048
1049    /// Task 1.2 (span-name-enrichment): `set_label` is a no-op on `Stop`.
1050    #[test]
1051    fn set_label_noop_on_stop() {
1052        let mut step = CompiledStep::Stop;
1053        step.set_label(Some("x".into()));
1054        assert!(
1055            matches!(step, CompiledStep::Stop),
1056            "set_label must not change a Stop step, got {step:?}"
1057        );
1058    }
1059
1060    /// Task 1.2 (span-name-enrichment): `compile_step` stamps the outcome with
1061    /// the step's `span_label` (here `to:direct` from `BuilderStep::To`).
1062    #[test]
1063    fn compile_step_stamps_label() {
1064        let pc = ProducerContext::default();
1065        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1066        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1067        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1068        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1069        let staging = FunctionStagingMode::DirectAdd;
1070        let idempotent_repositories = crate::IdempotentRegistry::new();
1071        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1072        let cache_repositories = crate::CacheRegistry::new();
1073
1074        let context = ctx(
1075            &pc,
1076            rt,
1077            &languages,
1078            &beans,
1079            component_ctx,
1080            &staging,
1081            &idempotent_repositories,
1082            &claim_check_repositories,
1083            &cache_repositories,
1084        );
1085
1086        let mut reg = StepCompilerRegistry::new();
1087        reg.register(Box::new(ToProcessCompiler));
1088
1089        let result = reg
1090            .compile_step(BuilderStep::To("direct:x".into()), 0, &context)
1091            .expect("compilation should succeed")
1092            .expect("should match");
1093
1094        match result {
1095            CompiledStep::Process { label, .. } => {
1096                assert_eq!(label.as_deref(), Some("to:direct"));
1097            }
1098            other => panic!("expected Process, got {other:?}"),
1099        }
1100    }
1101
1102    /// Task 1.2 (span-kind-hint): `set_kind_hint` is a no-op on `Stop` and
1103    /// `Segment` — only `Process` carries a kind hint, and a Segment's label
1104    /// must survive the call untouched.
1105    #[test]
1106    fn set_kind_hint_noop_on_stop_and_segment() {
1107        use camel_api::SpanKindHint;
1108
1109        // Stop: the marker produces no step span, so it carries no kind.
1110        let mut step = CompiledStep::Stop;
1111        step.set_kind_hint(SpanKindHint::Client);
1112        assert!(
1113            matches!(step, CompiledStep::Stop),
1114            "set_kind_hint must not change a Stop step, got {step:?}"
1115        );
1116
1117        // Segment: no kind_hint field; its label is unchanged.
1118        let mut step = CompiledStep::Segment {
1119            segment: camel_api::OutcomeSegment::new(Box::new(NoopPipeline)),
1120            body_contract: None,
1121            lifecycle: None,
1122            label: Some("seg".into()),
1123        };
1124        step.set_kind_hint(SpanKindHint::Client);
1125        match step {
1126            CompiledStep::Segment { label, .. } => {
1127                assert_eq!(label.as_deref(), Some("seg"));
1128            }
1129            other => panic!("expected Segment to stay a Segment, got {other:?}"),
1130        }
1131    }
1132
1133    /// Task 1.2 (span-kind-hint): `compile_step` stamps the outcome with the
1134    /// step's `span_kind_hint` (here `kafka` → Producer) alongside the label.
1135    #[test]
1136    fn compile_step_stamps_kind_hint() {
1137        use camel_api::SpanKindHint;
1138
1139        let pc = ProducerContext::default();
1140        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1141        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1142        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1143        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1144        let staging = FunctionStagingMode::DirectAdd;
1145        let idempotent_repositories = crate::IdempotentRegistry::new();
1146        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1147        let cache_repositories = crate::CacheRegistry::new();
1148
1149        let context = ctx(
1150            &pc,
1151            rt,
1152            &languages,
1153            &beans,
1154            component_ctx,
1155            &staging,
1156            &idempotent_repositories,
1157            &claim_check_repositories,
1158            &cache_repositories,
1159        );
1160
1161        let mut reg = StepCompilerRegistry::new();
1162        reg.register(Box::new(ToProcessCompiler));
1163
1164        let result = reg
1165            .compile_step(BuilderStep::To("kafka:orders".into()), 0, &context)
1166            .expect("compilation should succeed")
1167            .expect("should match");
1168
1169        match result {
1170            CompiledStep::Process {
1171                kind_hint, label, ..
1172            } => {
1173                assert_eq!(kind_hint, SpanKindHint::Producer);
1174                assert_eq!(label.as_deref(), Some("to:kafka"));
1175            }
1176            other => panic!("expected Process, got {other:?}"),
1177        }
1178    }
1179
1180    /// Test that when no compiler handles a variant, the dispatcher returns
1181    /// `Ok(None)` rather than an error.
1182    #[test]
1183    fn compile_step_unhandled_returns_ok_none() {
1184        let pc = ProducerContext::default();
1185        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1186        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1187        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1188        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1189        let staging = FunctionStagingMode::DirectAdd;
1190        let idempotent_repositories = crate::IdempotentRegistry::new();
1191        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1192        let cache_repositories = crate::CacheRegistry::new();
1193
1194        let context = ctx(
1195            &pc,
1196            rt,
1197            &languages,
1198            &beans,
1199            component_ctx,
1200            &staging,
1201            &idempotent_repositories,
1202            &claim_check_repositories,
1203            &cache_repositories,
1204        );
1205
1206        // Register a compiler that only handles BuilderStep::To.
1207        let mut reg = StepCompilerRegistry::new();
1208        reg.register(Box::new(ToStopCompiler));
1209
1210        // Send a BuilderStep::Log variant — ToStopCompiler does not match it.
1211        let result = reg.compile_step(
1212            BuilderStep::Log {
1213                level: camel_processor::LogLevel::Info,
1214                message: "unhandled".into(),
1215            },
1216            0,
1217            &context,
1218        );
1219
1220        assert!(
1221            matches!(result, Ok(None)),
1222            "expected Ok(None), got {result:?}"
1223        );
1224    }
1225
1226    /// Test that `NotHandled(step)` passes the step by-value to the next
1227    /// compiler. Compiler N returns `NotHandled(step)` for a variant that
1228    /// compiler N+1 handles — proving the step is not dropped or replaced.
1229    #[test]
1230    fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
1231        let pc = ProducerContext::default();
1232        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1233        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1234        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1235        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1236        let staging = FunctionStagingMode::DirectAdd;
1237        let idempotent_repositories = crate::IdempotentRegistry::new();
1238        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1239        let cache_repositories = crate::CacheRegistry::new();
1240
1241        let context = ctx(
1242            &pc,
1243            rt,
1244            &languages,
1245            &beans,
1246            component_ctx,
1247            &staging,
1248            &idempotent_repositories,
1249            &claim_check_repositories,
1250            &cache_repositories,
1251        );
1252
1253        // Register PassThroughCompiler FIRST (never handles anything),
1254        // ToStopCompiler SECOND (handles BuilderStep::To).
1255        let mut reg = StepCompilerRegistry::new();
1256        reg.register(Box::new(PassThroughCompiler));
1257        reg.register(Box::new(ToStopCompiler));
1258
1259        let result = reg
1260            .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
1261            .expect("compilation should succeed")
1262            .expect("should match");
1263
1264        assert!(
1265            matches!(result, CompiledStep::Stop),
1266            "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
1267        );
1268    }
1269}