Skip to main content

camel_api/
runtime.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4use crate::CamelError;
5use crate::declarative::LanguageExpressionDef;
6use crate::splitter::StreamSplitConfig;
7
8pub const CANONICAL_CONTRACT_NAME: &str = "canonical-v1";
9pub const CANONICAL_CONTRACT_VERSION: u32 = 2;
10pub const CANONICAL_CONTRACT_SUPPORTED_STEPS: &[&str] = &[
11    "to",
12    "log",
13    "wire_tap",
14    "script",
15    "filter",
16    "choice",
17    "split",
18    "aggregate",
19    "stop",
20    "delay",
21    "cache",
22    "cache_invalidate",
23    "cache_peek_stale",
24];
25pub const CANONICAL_CONTRACT_DECLARATIVE_ONLY_STEPS: &[&str] =
26    &["script", "filter", "choice", "split"];
27pub const CANONICAL_CONTRACT_EXCLUDED_DECLARATIVE_STEPS: &[&str] = &[
28    "set_header",
29    "set_property",
30    "set_body",
31    "multicast",
32    "convert_body_to",
33    "bean",
34    "marshal",
35    "unmarshal",
36];
37pub const CANONICAL_CONTRACT_RUST_ONLY_STEPS: &[&str] = &[
38    "processor",
39    "process",
40    "process_fn",
41    "map_body",
42    "set_body_fn",
43    "set_header_fn",
44    "to_layered",
45    "layered",
46];
47
48pub fn canonical_contract_supports_step(step: &str) -> bool {
49    CANONICAL_CONTRACT_SUPPORTED_STEPS.contains(&step)
50}
51
52pub fn canonical_contract_rejection_reason(step: &str) -> Option<&'static str> {
53    if CANONICAL_CONTRACT_EXCLUDED_DECLARATIVE_STEPS.contains(&step) {
54        return Some(
55            "declared out-of-scope for canonical v2; use declarative route compilation path outside CQRS canonical commands",
56        );
57    }
58
59    if CANONICAL_CONTRACT_RUST_ONLY_STEPS.contains(&step) {
60        return Some("rust-only programmable step; not representable in canonical v2 contract");
61    }
62
63    if canonical_contract_supports_step(step)
64        && CANONICAL_CONTRACT_DECLARATIVE_ONLY_STEPS.contains(&step)
65    {
66        return Some(
67            "supported only as declarative/serializable expression form; closure/processor variants are outside canonical v2",
68        );
69    }
70
71    None
72}
73
74#[derive(
75    Debug,
76    Clone,
77    PartialEq,
78    Eq,
79    serde::Serialize,
80    serde::Deserialize,
81    schemars::JsonSchema,
82    ts_rs::TS,
83)]
84#[serde(rename_all = "snake_case")]
85#[ts(rename_all = "snake_case")]
86pub struct CanonicalRouteSpec {
87    /// Stable minimal route representation for runtime command registration.
88    ///
89    /// Scope notes:
90    /// - This is intentionally a partial model and does not mirror every `BuilderStep`.
91    /// - Version 2 adds: auto_startup, startup_order, concurrency.
92    /// - Still excluded: error_handler, unit_of_work. These are set to defaults
93    ///   when compiling from canonical.
94    /// - Round-trip (YAML → Canonical → YAML) loses these fields.
95    /// - Advanced EIPs continue to use the existing RouteDefinition/BuilderStep path.
96    pub route_id: String,
97    pub from: String,
98    pub steps: Vec<CanonicalStepSpec>,
99    pub circuit_breaker: Option<CanonicalCircuitBreakerSpec>,
100    pub auto_startup: Option<bool>,
101    pub startup_order: Option<i32>,
102    pub concurrency: Option<CanonicalConcurrencySpec>,
103    pub version: u32,
104}
105
106#[derive(
107    Debug,
108    Clone,
109    PartialEq,
110    Eq,
111    serde::Serialize,
112    serde::Deserialize,
113    schemars::JsonSchema,
114    ts_rs::TS,
115)]
116#[serde(tag = "step", content = "config", rename_all = "snake_case")]
117#[ts(rename_all = "snake_case")]
118#[non_exhaustive]
119pub enum CanonicalStepSpec {
120    To {
121        uri: String,
122    },
123    Log {
124        message: String,
125    },
126    WireTap {
127        uri: String,
128    },
129    Script {
130        expression: LanguageExpressionDef,
131    },
132    Filter {
133        predicate: LanguageExpressionDef,
134        steps: Vec<CanonicalStepSpec>,
135    },
136    Choice {
137        whens: Vec<CanonicalWhenSpec>,
138        otherwise: Option<Vec<CanonicalStepSpec>>,
139    },
140    Split {
141        expression: CanonicalSplitExpressionSpec,
142        aggregation: CanonicalSplitAggregationSpec,
143        parallel: bool,
144        parallel_limit: Option<usize>,
145        /// Threshold above which split fragments start new traces (0 = off).
146        /// Absent means the runtime default applies.
147        trace_item_threshold: Option<usize>,
148        stop_on_exception: bool,
149        steps: Vec<CanonicalStepSpec>,
150    },
151    Aggregate(CanonicalAggregateSpec),
152    Stop,
153    Delay {
154        #[ts(type = "number")]
155        delay_ms: u64,
156        dynamic_header: Option<String>,
157    },
158    Cache {
159        repository: Option<String>,
160        key: String,
161        ttl: Option<String>,
162        max_entry_bytes: Option<usize>,
163        /// Coalesce concurrent misses on the same key into a single `on_miss`
164        /// run. Absent/null means `false`.
165        #[serde(default, skip_serializing_if = "Option::is_none")]
166        coalesce_misses: Option<bool>,
167        on_miss: Vec<CanonicalStepSpec>,
168    },
169    CacheInvalidate {
170        repository: Option<String>,
171        /// Exact key to invalidate (simple-language expression).
172        #[serde(default, skip_serializing_if = "Option::is_none")]
173        key: Option<String>,
174        /// Namespace prefix to invalidate (simple-language expression).
175        #[serde(default, skip_serializing_if = "Option::is_none")]
176        key_prefix: Option<String>,
177    },
178    CacheClear {
179        repository: Option<String>,
180    },
181    CacheStats {
182        repository: Option<String>,
183    },
184    CachePeekStale {
185        repository: Option<String>,
186        key: String,
187        /// On-miss policy: `"stop"` (default) or `"continue"`. Absent/null means `"stop"`.
188        #[serde(default, skip_serializing_if = "Option::is_none")]
189        on_miss: Option<String>,
190    },
191}
192
193#[derive(
194    Debug,
195    Clone,
196    PartialEq,
197    Eq,
198    serde::Serialize,
199    serde::Deserialize,
200    schemars::JsonSchema,
201    ts_rs::TS,
202)]
203#[serde(rename_all = "snake_case")]
204#[ts(rename_all = "snake_case")]
205pub struct CanonicalWhenSpec {
206    pub predicate: LanguageExpressionDef,
207    pub steps: Vec<CanonicalStepSpec>,
208}
209
210#[derive(
211    Debug,
212    Clone,
213    PartialEq,
214    Eq,
215    serde::Serialize,
216    serde::Deserialize,
217    schemars::JsonSchema,
218    ts_rs::TS,
219)]
220#[serde(rename_all = "snake_case")]
221#[ts(rename_all = "snake_case")]
222#[non_exhaustive]
223pub enum CanonicalSplitExpressionSpec {
224    BodyLines,
225    BodyJsonArray,
226    Language(LanguageExpressionDef),
227    Stream(StreamSplitConfig),
228}
229
230#[derive(
231    Debug,
232    Clone,
233    PartialEq,
234    Eq,
235    serde::Serialize,
236    serde::Deserialize,
237    schemars::JsonSchema,
238    ts_rs::TS,
239)]
240#[serde(rename_all = "snake_case")]
241#[ts(rename_all = "snake_case")]
242#[non_exhaustive]
243pub enum CanonicalSplitAggregationSpec {
244    LastWins,
245    CollectAll,
246    Original,
247}
248
249#[derive(
250    Debug,
251    Clone,
252    PartialEq,
253    Eq,
254    serde::Serialize,
255    serde::Deserialize,
256    schemars::JsonSchema,
257    ts_rs::TS,
258)]
259#[serde(rename_all = "snake_case")]
260#[ts(rename_all = "snake_case")]
261#[non_exhaustive]
262pub enum CanonicalAggregateStrategySpec {
263    CollectAll,
264}
265
266#[derive(
267    Debug,
268    Clone,
269    PartialEq,
270    Eq,
271    serde::Serialize,
272    serde::Deserialize,
273    schemars::JsonSchema,
274    ts_rs::TS,
275)]
276#[serde(rename_all = "snake_case")]
277#[ts(rename_all = "snake_case")]
278pub struct CanonicalAggregateSpec {
279    pub header: String,
280    pub completion_size: Option<usize>,
281    #[ts(type = "number")]
282    pub completion_timeout_ms: Option<u64>,
283    pub correlation_key: Option<String>,
284    pub force_completion_on_stop: Option<bool>,
285    pub discard_on_timeout: Option<bool>,
286    pub strategy: CanonicalAggregateStrategySpec,
287    pub max_buckets: Option<usize>,
288    /// Per-bucket accumulation bound (audit 2026-08-31, F6-2).
289    /// Absent = builder default (10_000).
290    #[serde(default)]
291    pub max_bucket_size: Option<usize>,
292    #[ts(type = "number")]
293    pub bucket_ttl_ms: Option<u64>,
294    /// Language expression predicate; completes the bucket when it evaluates
295    /// true against the incoming exchange (runtime-resolved via the language
296    /// registry). Mirrors `CorrelationStrategy::Expression`.
297    #[serde(default)]
298    pub completion_predicate: Option<LanguageExpressionDef>,
299}
300
301#[derive(
302    Debug,
303    Clone,
304    PartialEq,
305    Eq,
306    serde::Serialize,
307    serde::Deserialize,
308    schemars::JsonSchema,
309    ts_rs::TS,
310)]
311#[serde(rename_all = "snake_case")]
312#[ts(rename_all = "snake_case")]
313pub struct CanonicalCircuitBreakerSpec {
314    pub failure_threshold: u32,
315    #[ts(type = "number")]
316    pub open_duration_ms: u64,
317    #[serde(default, skip_serializing_if = "Vec::is_empty")]
318    pub fallback: Vec<CanonicalStepSpec>,
319}
320
321#[derive(
322    Debug,
323    Clone,
324    PartialEq,
325    Eq,
326    serde::Serialize,
327    serde::Deserialize,
328    schemars::JsonSchema,
329    ts_rs::TS,
330)]
331#[serde(tag = "mode", rename_all = "snake_case")]
332#[non_exhaustive]
333pub enum CanonicalConcurrencySpec {
334    Sequential,
335    Concurrent { max: usize },
336}
337
338impl CanonicalRouteSpec {
339    pub fn new(route_id: impl Into<String>, from: impl Into<String>) -> Self {
340        Self {
341            route_id: route_id.into(),
342            from: from.into(),
343            steps: Vec::new(),
344            circuit_breaker: None,
345            auto_startup: None,
346            startup_order: None,
347            concurrency: None,
348            version: CANONICAL_CONTRACT_VERSION,
349        }
350    }
351
352    pub fn with_auto_startup(mut self, auto: bool) -> Self {
353        self.auto_startup = Some(auto);
354        self
355    }
356
357    pub fn with_startup_order(mut self, order: i32) -> Self {
358        self.startup_order = Some(order);
359        self
360    }
361
362    pub fn with_concurrency(mut self, concurrency: CanonicalConcurrencySpec) -> Self {
363        self.concurrency = Some(concurrency);
364        self
365    }
366
367    pub fn validate_contract(&self) -> Result<(), CamelError> {
368        if self.route_id.trim().is_empty() {
369            return Err(CamelError::RouteError(
370                "canonical contract violation: route_id cannot be empty".to_string(),
371            ));
372        }
373        if self.from.trim().is_empty() {
374            return Err(CamelError::RouteError(
375                "canonical contract violation: from cannot be empty".to_string(),
376            ));
377        }
378        if self.version == 0 || self.version > CANONICAL_CONTRACT_VERSION {
379            return Err(CamelError::RouteError(format!(
380                "canonical contract violation: expected version {}, got {}",
381                CANONICAL_CONTRACT_VERSION, self.version
382            )));
383        }
384        validate_steps(&self.steps)?;
385        if let Some(cb) = &self.circuit_breaker {
386            if cb.failure_threshold == 0 {
387                return Err(CamelError::RouteError(
388                    "canonical contract violation: circuit_breaker.failure_threshold must be > 0"
389                        .to_string(),
390                ));
391            }
392            if cb.open_duration_ms == 0 {
393                return Err(CamelError::RouteError(
394                    "canonical contract violation: circuit_breaker.open_duration_ms must be > 0"
395                        .to_string(),
396                ));
397            }
398            validate_steps(&cb.fallback)?;
399        }
400        if let Some(CanonicalConcurrencySpec::Concurrent { max: 0 }) = &self.concurrency {
401            return Err(CamelError::RouteError(
402                "canonical contract violation: concurrency max must be > 0".to_string(),
403            ));
404        }
405        Ok(())
406    }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
410pub struct CanonicalFieldLoss {
411    pub field: &'static str,
412    pub reason: String,
413    pub target_version: u32,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize)]
417pub struct CanonicalLossReport {
418    pub dropped_fields: Vec<CanonicalFieldLoss>,
419}
420
421impl CanonicalLossReport {
422    pub fn from_field(field: &'static str, reason: &str, target_version: u32) -> Self {
423        Self {
424            dropped_fields: vec![CanonicalFieldLoss {
425                field,
426                reason: reason.to_string(),
427                target_version,
428            }],
429        }
430    }
431
432    pub fn is_empty(&self) -> bool {
433        self.dropped_fields.is_empty()
434    }
435}
436
437fn validate_steps(steps: &[CanonicalStepSpec]) -> Result<(), CamelError> {
438    for step in steps {
439        match step {
440            CanonicalStepSpec::To { uri } | CanonicalStepSpec::WireTap { uri } => {
441                if uri.trim().is_empty() {
442                    return Err(CamelError::RouteError(
443                        "canonical contract violation: endpoint uri cannot be empty".to_string(),
444                    ));
445                }
446            }
447            CanonicalStepSpec::Filter { steps, .. } => validate_steps(steps)?,
448            CanonicalStepSpec::Choice { whens, otherwise } => {
449                for when in whens {
450                    validate_steps(&when.steps)?;
451                }
452                if let Some(otherwise) = otherwise {
453                    validate_steps(otherwise)?;
454                }
455            }
456            CanonicalStepSpec::Split {
457                parallel_limit,
458                steps,
459                ..
460            } => {
461                if let Some(limit) = parallel_limit
462                    && *limit == 0
463                {
464                    return Err(CamelError::RouteError(
465                        "canonical contract violation: split.parallel_limit must be > 0"
466                            .to_string(),
467                    ));
468                }
469                validate_steps(steps)?;
470            }
471            CanonicalStepSpec::Aggregate(config) => {
472                let header_present = !config.header.trim().is_empty();
473                let key_present = config
474                    .correlation_key
475                    .as_deref()
476                    .is_some_and(|k| !k.trim().is_empty());
477                if !key_present && config.correlation_key.is_some() {
478                    return Err(CamelError::RouteError(
479                        "canonical contract violation: aggregate.correlation_key cannot be empty"
480                            .to_string(),
481                    ));
482                }
483                if !header_present && !key_present {
484                    return Err(CamelError::RouteError(
485                        "canonical contract violation: aggregate requires a correlation source: header or correlation_key"
486                            .to_string(),
487                    ));
488                }
489                if let Some(size) = config.completion_size
490                    && size == 0
491                {
492                    return Err(CamelError::RouteError(
493                        "canonical contract violation: aggregate.completion_size must be > 0"
494                            .to_string(),
495                    ));
496                }
497            }
498            CanonicalStepSpec::Cache { on_miss, .. } => {
499                validate_steps(on_miss)?;
500            }
501            CanonicalStepSpec::Log { .. }
502            | CanonicalStepSpec::Script { .. }
503            | CanonicalStepSpec::Stop
504            | CanonicalStepSpec::Delay { .. }
505            | CanonicalStepSpec::CacheInvalidate { .. }
506            | CanonicalStepSpec::CacheClear { .. }
507            | CanonicalStepSpec::CacheStats { .. }
508            | CanonicalStepSpec::CachePeekStale { .. } => {}
509        }
510    }
511    Ok(())
512}
513
514#[derive(Debug, Clone, PartialEq, Eq)]
515#[non_exhaustive]
516pub enum RuntimeCommand {
517    RegisterRoute {
518        spec: CanonicalRouteSpec,
519        command_id: String,
520        causation_id: Option<String>,
521    },
522    StartRoute {
523        route_id: String,
524        command_id: String,
525        causation_id: Option<String>,
526    },
527    StopRoute {
528        route_id: String,
529        command_id: String,
530        causation_id: Option<String>,
531        /// Effective graceful stop budget. `None` preserves the legacy
532        /// default shutdown bound (the route registry's
533        /// `DEFAULT_SHUTDOWN_TIMEOUT`); `Some(budget)` bounds the stop with
534        /// the given budget. Future graceful-shutdown producers (CLI,
535        /// supervisor, orchestrators) MUST send `Some(budget)` explicitly —
536        /// `None` is reserved for the registry/reload semantics (legacy
537        /// default bound), not a default for graceful stops.
538        stop_budget: Option<std::time::Duration>,
539    },
540    SuspendRoute {
541        route_id: String,
542        command_id: String,
543        causation_id: Option<String>,
544    },
545    ResumeRoute {
546        route_id: String,
547        command_id: String,
548        causation_id: Option<String>,
549    },
550    ReloadRoute {
551        route_id: String,
552        command_id: String,
553        causation_id: Option<String>,
554    },
555    /// Internal lifecycle command emitted by runtime adapters when a route crashes at runtime.
556    ///
557    /// This keeps aggregate/projection state aligned with controller-observed failures.
558    FailRoute {
559        route_id: String,
560        error: String,
561        command_id: String,
562        causation_id: Option<String>,
563    },
564    RemoveRoute {
565        route_id: String,
566        command_id: String,
567        causation_id: Option<String>,
568    },
569    ReloadTlsCerts {
570        scheme: String,
571        host: String,
572        port: u16,
573        command_id: String,
574        causation_id: Option<String>,
575    },
576    /// Reload template sources for a route (infrastructure command).
577    ///
578    /// Intercepted in `RuntimeBus::execute` BEFORE journal recovery + dedup,
579    /// exactly like `ReloadTlsCerts`: it is idempotent, NOT journaled, and does
580    /// not mutate `RouteStatus`. Dispatches to
581    /// `TemplateReloadRegistry::reload_route`.
582    ReloadTemplates {
583        route_id: String,
584        command_id: String,
585        causation_id: Option<String>,
586    },
587}
588
589impl RuntimeCommand {
590    pub fn command_id(&self) -> &str {
591        match self {
592            RuntimeCommand::RegisterRoute { command_id, .. }
593            | RuntimeCommand::StartRoute { command_id, .. }
594            | RuntimeCommand::StopRoute { command_id, .. }
595            | RuntimeCommand::SuspendRoute { command_id, .. }
596            | RuntimeCommand::ResumeRoute { command_id, .. }
597            | RuntimeCommand::ReloadRoute { command_id, .. }
598            | RuntimeCommand::FailRoute { command_id, .. }
599            | RuntimeCommand::RemoveRoute { command_id, .. }
600            | RuntimeCommand::ReloadTlsCerts { command_id, .. }
601            | RuntimeCommand::ReloadTemplates { command_id, .. } => command_id,
602        }
603    }
604
605    pub fn causation_id(&self) -> Option<&str> {
606        match self {
607            RuntimeCommand::RegisterRoute { causation_id, .. }
608            | RuntimeCommand::StartRoute { causation_id, .. }
609            | RuntimeCommand::StopRoute { causation_id, .. }
610            | RuntimeCommand::SuspendRoute { causation_id, .. }
611            | RuntimeCommand::ResumeRoute { causation_id, .. }
612            | RuntimeCommand::ReloadRoute { causation_id, .. }
613            | RuntimeCommand::FailRoute { causation_id, .. }
614            | RuntimeCommand::RemoveRoute { causation_id, .. }
615            | RuntimeCommand::ReloadTlsCerts { causation_id, .. }
616            | RuntimeCommand::ReloadTemplates { causation_id, .. } => causation_id.as_deref(),
617        }
618    }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq)]
622#[non_exhaustive]
623pub enum RuntimeCommandResult {
624    Accepted,
625    Duplicate {
626        command_id: String,
627    },
628    RouteRegistered {
629        route_id: String,
630    },
631    RouteStateChanged {
632        route_id: String,
633        status: String,
634    },
635    TlsCertsReloaded {
636        scheme: String,
637        host: String,
638        port: u16,
639    },
640    TemplatesReloaded {
641        route_id: String,
642    },
643}
644
645#[derive(Debug, Clone, PartialEq, Eq)]
646#[non_exhaustive]
647pub enum RuntimeQuery {
648    GetRouteStatus {
649        route_id: String,
650    },
651    /// **Note:** This variant is intercepted by `RuntimeBus::ask` *before* reaching
652    /// `execute_query`. Do not handle it in `execute_query` — it has no access to
653    /// the in-flight counter. See `runtime_bus.rs` for the intercept.
654    InFlightCount {
655        route_id: String,
656    },
657    ListRoutes,
658}
659
660#[derive(Debug, Clone, PartialEq, Eq)]
661#[non_exhaustive]
662pub enum RuntimeQueryResult {
663    InFlightCount { route_id: String, count: u64 },
664    RouteNotFound { route_id: String },
665    RouteStatus { route_id: String, status: String },
666    Routes { route_ids: Vec<String> },
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670#[non_exhaustive]
671pub enum RuntimeEvent {
672    RouteRegistered { route_id: String },
673    RouteStartRequested { route_id: String },
674    RouteStarted { route_id: String },
675    RouteFailed { route_id: String, error: String },
676    RouteStopped { route_id: String },
677    RouteSuspended { route_id: String },
678    RouteResumed { route_id: String },
679    RouteReloaded { route_id: String },
680    RouteRemoved { route_id: String },
681}
682
683#[async_trait]
684pub trait RuntimeCommandBus: Send + Sync {
685    async fn execute(&self, cmd: RuntimeCommand) -> Result<RuntimeCommandResult, CamelError>;
686}
687
688#[async_trait]
689pub trait RuntimeQueryBus: Send + Sync {
690    async fn ask(&self, query: RuntimeQuery) -> Result<RuntimeQueryResult, CamelError>;
691}
692
693pub trait RuntimeHandle: RuntimeCommandBus + RuntimeQueryBus {}
694
695impl<T> RuntimeHandle for T where T: RuntimeCommandBus + RuntimeQueryBus {}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use async_trait::async_trait;
701    use futures::executor::block_on;
702
703    struct NoopRuntime;
704
705    #[async_trait]
706    impl RuntimeCommandBus for NoopRuntime {
707        async fn execute(&self, cmd: RuntimeCommand) -> Result<RuntimeCommandResult, CamelError> {
708            Ok(match cmd {
709                RuntimeCommand::RegisterRoute { spec, .. } => {
710                    RuntimeCommandResult::RouteRegistered {
711                        route_id: spec.route_id,
712                    }
713                }
714                RuntimeCommand::StartRoute { route_id, .. }
715                | RuntimeCommand::StopRoute { route_id, .. }
716                | RuntimeCommand::SuspendRoute { route_id, .. }
717                | RuntimeCommand::ResumeRoute { route_id, .. }
718                | RuntimeCommand::ReloadRoute { route_id, .. }
719                | RuntimeCommand::FailRoute { route_id, .. }
720                | RuntimeCommand::RemoveRoute { route_id, .. } => {
721                    RuntimeCommandResult::RouteStateChanged {
722                        route_id,
723                        status: "ok".to_string(),
724                    }
725                }
726                RuntimeCommand::ReloadTlsCerts {
727                    scheme, host, port, ..
728                } => RuntimeCommandResult::TlsCertsReloaded { scheme, host, port },
729                RuntimeCommand::ReloadTemplates { route_id, .. } => {
730                    RuntimeCommandResult::TemplatesReloaded { route_id }
731                }
732            })
733        }
734    }
735
736    #[async_trait]
737    impl RuntimeQueryBus for NoopRuntime {
738        async fn ask(&self, query: RuntimeQuery) -> Result<RuntimeQueryResult, CamelError> {
739            Ok(match query {
740                RuntimeQuery::GetRouteStatus { route_id } => RuntimeQueryResult::RouteStatus {
741                    route_id,
742                    status: "Started".to_string(),
743                },
744                RuntimeQuery::InFlightCount { route_id } => {
745                    RuntimeQueryResult::InFlightCount { route_id, count: 0 }
746                }
747                RuntimeQuery::ListRoutes => RuntimeQueryResult::Routes {
748                    route_ids: vec!["r1".to_string()],
749                },
750            })
751        }
752    }
753
754    #[test]
755    fn command_and_query_ids_are_exposed() {
756        let cmd = RuntimeCommand::StartRoute {
757            route_id: "r1".into(),
758            command_id: "c1".into(),
759            causation_id: None,
760        };
761        assert_eq!(cmd.command_id(), "c1");
762    }
763
764    #[test]
765    fn canonical_spec_requires_route_id_and_from() {
766        let spec = CanonicalRouteSpec::new("r1", "timer:tick");
767        assert_eq!(spec.route_id, "r1");
768        assert_eq!(spec.from, "timer:tick");
769        assert_eq!(spec.version, CANONICAL_CONTRACT_VERSION);
770        assert!(spec.steps.is_empty());
771        assert!(spec.circuit_breaker.is_none());
772    }
773
774    #[test]
775    fn canonical_contract_rejects_invalid_version() {
776        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
777        spec.version = 3;
778        let err = spec.validate_contract().unwrap_err().to_string();
779        assert!(err.contains("expected version"));
780    }
781
782    #[test]
783    fn canonical_contract_declares_subset_scope() {
784        assert!(canonical_contract_supports_step("to"));
785        assert!(canonical_contract_supports_step("split"));
786        assert!(!canonical_contract_supports_step("set_header"));
787        assert!(!canonical_contract_supports_step("set_property"));
788
789        assert!(CANONICAL_CONTRACT_DECLARATIVE_ONLY_STEPS.contains(&"split"));
790        assert!(CANONICAL_CONTRACT_EXCLUDED_DECLARATIVE_STEPS.contains(&"set_header"));
791        assert!(CANONICAL_CONTRACT_EXCLUDED_DECLARATIVE_STEPS.contains(&"set_property"));
792        assert!(CANONICAL_CONTRACT_RUST_ONLY_STEPS.contains(&"processor"));
793    }
794
795    #[test]
796    fn canonical_contract_rejection_reason_is_explicit() {
797        let set_header_reason = canonical_contract_rejection_reason("set_header")
798            .expect("set_header should have explicit reason");
799        assert!(set_header_reason.contains("out-of-scope"));
800
801        let set_property_reason = canonical_contract_rejection_reason("set_property")
802            .expect("set_property should have explicit reason");
803        assert!(set_property_reason.contains("out-of-scope"));
804
805        let processor_reason = canonical_contract_rejection_reason("processor")
806            .expect("processor should be rust-only");
807        assert!(processor_reason.contains("rust-only"));
808
809        let split_reason = canonical_contract_rejection_reason("split")
810            .expect("split should require declarative form");
811        assert!(split_reason.contains("declarative"));
812    }
813
814    /// Task 2.1 (builder-layer-middleware): layered builder steps are
815    /// Rust-only programmable steps, so canonical v2 must reject them with the
816    /// explicit rust-only reason rather than report them as unknown.
817    #[test]
818    fn canonical_rejection_lists_include_layered_steps() {
819        for step in ["to_layered", "layered"] {
820            let reason = canonical_contract_rejection_reason(step)
821                .unwrap_or_else(|| panic!("{step} must be explicitly rejected"));
822            assert!(
823                reason.contains("rust-only programmable step"),
824                "{step} rejection reason was: {reason}"
825            );
826        }
827    }
828
829    #[test]
830    fn command_causation_id_is_exposed() {
831        let cmd = RuntimeCommand::StopRoute {
832            route_id: "r1".into(),
833            command_id: "c2".into(),
834            causation_id: Some("c1".into()),
835            stop_budget: None,
836        };
837        assert_eq!(cmd.command_id(), "c2");
838        assert_eq!(cmd.causation_id(), Some("c1"));
839    }
840
841    #[test]
842    fn stop_route_command_carries_budget() {
843        let cmd = RuntimeCommand::StopRoute {
844            route_id: "r1".into(),
845            command_id: "c2".into(),
846            causation_id: None,
847            stop_budget: Some(std::time::Duration::from_secs(10)),
848        };
849        match cmd {
850            RuntimeCommand::StopRoute { stop_budget, .. } => {
851                assert_eq!(stop_budget, Some(std::time::Duration::from_secs(10)));
852            }
853            other => panic!("expected StopRoute, got {other:?}"),
854        }
855    }
856
857    #[test]
858    fn canonical_contract_rejects_empty_route_id_and_from() {
859        let spec = CanonicalRouteSpec::new("   ", "timer:tick");
860        let err = spec.validate_contract().unwrap_err().to_string();
861        assert!(err.contains("route_id cannot be empty"));
862
863        let spec = CanonicalRouteSpec::new("r1", "  ");
864        let err = spec.validate_contract().unwrap_err().to_string();
865        assert!(err.contains("from cannot be empty"));
866    }
867
868    #[test]
869    fn canonical_contract_rejects_invalid_nested_steps() {
870        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
871        spec.steps = vec![CanonicalStepSpec::Split {
872            expression: CanonicalSplitExpressionSpec::BodyLines,
873            aggregation: CanonicalSplitAggregationSpec::CollectAll,
874            parallel: true,
875            parallel_limit: Some(0),
876            trace_item_threshold: None,
877            stop_on_exception: false,
878            steps: vec![CanonicalStepSpec::To {
879                uri: "log:ok".to_string(),
880            }],
881        }];
882        let err = spec.validate_contract().unwrap_err().to_string();
883        assert!(err.contains("split.parallel_limit must be > 0"));
884
885        spec.steps = vec![CanonicalStepSpec::To {
886            uri: "   ".to_string(),
887        }];
888        let err = spec.validate_contract().unwrap_err().to_string();
889        assert!(err.contains("endpoint uri cannot be empty"));
890    }
891
892    #[test]
893    fn canonical_contract_rejects_invalid_aggregate_and_circuit_breaker() {
894        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
895        spec.steps = vec![CanonicalStepSpec::Aggregate(CanonicalAggregateSpec {
896            header: " ".to_string(),
897            completion_size: Some(1),
898            completion_timeout_ms: None,
899            correlation_key: None,
900            force_completion_on_stop: None,
901            discard_on_timeout: None,
902            strategy: CanonicalAggregateStrategySpec::CollectAll,
903            max_buckets: None,
904            max_bucket_size: None,
905            bucket_ttl_ms: None,
906            completion_predicate: None,
907        })];
908        let err = spec.validate_contract().unwrap_err().to_string();
909        assert!(err.contains("correlation source"));
910
911        spec.steps = vec![CanonicalStepSpec::Aggregate(CanonicalAggregateSpec {
912            header: "k".to_string(),
913            completion_size: Some(0),
914            completion_timeout_ms: None,
915            correlation_key: None,
916            force_completion_on_stop: None,
917            discard_on_timeout: None,
918            strategy: CanonicalAggregateStrategySpec::CollectAll,
919            max_buckets: None,
920            max_bucket_size: None,
921            bucket_ttl_ms: None,
922            completion_predicate: None,
923        })];
924        let err = spec.validate_contract().unwrap_err().to_string();
925        assert!(err.contains("aggregate.completion_size must be > 0"));
926
927        spec.steps = vec![];
928        spec.circuit_breaker = Some(CanonicalCircuitBreakerSpec {
929            failure_threshold: 0,
930            open_duration_ms: 10,
931            fallback: vec![],
932        });
933        let err = spec.validate_contract().unwrap_err().to_string();
934        assert!(err.contains("failure_threshold must be > 0"));
935
936        spec.circuit_breaker = Some(CanonicalCircuitBreakerSpec {
937            failure_threshold: 1,
938            open_duration_ms: 0,
939            fallback: vec![],
940        });
941        let err = spec.validate_contract().unwrap_err().to_string();
942        assert!(err.contains("open_duration_ms must be > 0"));
943    }
944
945    #[test]
946    fn contract_accepts_expression_only_aggregate() {
947        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
948        spec.steps = vec![CanonicalStepSpec::Aggregate(CanonicalAggregateSpec {
949            header: String::new(),
950            completion_size: None,
951            completion_timeout_ms: None,
952            correlation_key: Some("${header.orderId}".to_string()),
953            force_completion_on_stop: None,
954            discard_on_timeout: None,
955            strategy: CanonicalAggregateStrategySpec::CollectAll,
956            max_buckets: None,
957            max_bucket_size: None,
958            bucket_ttl_ms: None,
959            completion_predicate: None,
960        })];
961        assert!(spec.validate_contract().is_ok());
962    }
963
964    #[test]
965    fn contract_rejects_aggregate_missing_both_sources() {
966        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
967        spec.steps = vec![CanonicalStepSpec::Aggregate(CanonicalAggregateSpec {
968            header: String::new(),
969            completion_size: None,
970            completion_timeout_ms: None,
971            correlation_key: None,
972            force_completion_on_stop: None,
973            discard_on_timeout: None,
974            strategy: CanonicalAggregateStrategySpec::CollectAll,
975            max_buckets: None,
976            max_bucket_size: None,
977            bucket_ttl_ms: None,
978            completion_predicate: None,
979        })];
980        let err = spec.validate_contract().unwrap_err().to_string();
981        assert!(err.contains("correlation source"));
982    }
983
984    #[test]
985    fn contract_rejects_empty_correlation_key() {
986        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
987        spec.steps = vec![CanonicalStepSpec::Aggregate(CanonicalAggregateSpec {
988            header: "region".to_string(),
989            completion_size: None,
990            completion_timeout_ms: None,
991            correlation_key: Some(String::new()),
992            force_completion_on_stop: None,
993            discard_on_timeout: None,
994            strategy: CanonicalAggregateStrategySpec::CollectAll,
995            max_buckets: None,
996            max_bucket_size: None,
997            bucket_ttl_ms: None,
998            completion_predicate: None,
999        })];
1000        let err = spec.validate_contract().unwrap_err().to_string();
1001        assert!(err.contains("correlation_key cannot be empty"));
1002    }
1003
1004    #[test]
1005    fn canonical_contract_rejects_invalid_fallback_step() {
1006        let mut spec = CanonicalRouteSpec::new("r1", "timer:tick");
1007        spec.circuit_breaker = Some(CanonicalCircuitBreakerSpec {
1008            failure_threshold: 1,
1009            open_duration_ms: 10,
1010            fallback: vec![CanonicalStepSpec::To {
1011                uri: "   ".to_string(),
1012            }],
1013        });
1014        let err = spec.validate_contract().unwrap_err().to_string();
1015        assert!(err.contains("endpoint uri cannot be empty"));
1016    }
1017
1018    #[test]
1019    fn canonical_contract_rejection_reason_none_for_regular_steps() {
1020        assert!(canonical_contract_rejection_reason("to").is_none());
1021        assert!(canonical_contract_rejection_reason("unknown-step").is_none());
1022    }
1023
1024    #[test]
1025    fn command_helpers_cover_all_variants() {
1026        let spec = CanonicalRouteSpec::new("r1", "timer:tick");
1027        let cmds = [
1028            RuntimeCommand::RegisterRoute {
1029                spec,
1030                command_id: "c1".into(),
1031                causation_id: Some("root".into()),
1032            },
1033            RuntimeCommand::StartRoute {
1034                route_id: "r1".into(),
1035                command_id: "c2".into(),
1036                causation_id: None,
1037            },
1038            RuntimeCommand::StopRoute {
1039                route_id: "r1".into(),
1040                command_id: "c3".into(),
1041                causation_id: None,
1042                stop_budget: None,
1043            },
1044            RuntimeCommand::SuspendRoute {
1045                route_id: "r1".into(),
1046                command_id: "c4".into(),
1047                causation_id: None,
1048            },
1049            RuntimeCommand::ResumeRoute {
1050                route_id: "r1".into(),
1051                command_id: "c5".into(),
1052                causation_id: None,
1053            },
1054            RuntimeCommand::ReloadRoute {
1055                route_id: "r1".into(),
1056                command_id: "c6".into(),
1057                causation_id: None,
1058            },
1059            RuntimeCommand::FailRoute {
1060                route_id: "r1".into(),
1061                error: "boom".into(),
1062                command_id: "c7".into(),
1063                causation_id: None,
1064            },
1065            RuntimeCommand::RemoveRoute {
1066                route_id: "r1".into(),
1067                command_id: "c8".into(),
1068                causation_id: None,
1069            },
1070            RuntimeCommand::ReloadTlsCerts {
1071                scheme: "https".into(),
1072                host: "example.com".into(),
1073                port: 8443,
1074                command_id: "c9".into(),
1075                causation_id: None,
1076            },
1077        ];
1078
1079        let ids: Vec<&str> = cmds.iter().map(RuntimeCommand::command_id).collect();
1080        assert_eq!(
1081            ids,
1082            vec!["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9"]
1083        );
1084        assert_eq!(cmds[0].causation_id(), Some("root"));
1085        assert_eq!(cmds[1].causation_id(), None);
1086    }
1087
1088    #[test]
1089    fn canonical_route_spec_serde_roundtrip() {
1090        let mut spec = CanonicalRouteSpec::new("test-route", "timer:tick?period=1000");
1091        spec.steps.push(CanonicalStepSpec::Log {
1092            message: "Hello".into(),
1093        });
1094        spec.steps.push(CanonicalStepSpec::To {
1095            uri: "log:info".into(),
1096        });
1097        spec.steps.push(CanonicalStepSpec::Stop);
1098
1099        let json = serde_json::to_string(&spec).unwrap();
1100        let deserialized: CanonicalRouteSpec = serde_json::from_str(&json).unwrap();
1101        assert_eq!(spec, deserialized);
1102    }
1103
1104    #[test]
1105    fn canonical_step_spec_serde_variants() {
1106        let steps = vec![
1107            CanonicalStepSpec::To {
1108                uri: "direct:a".into(),
1109            },
1110            CanonicalStepSpec::Log {
1111                message: "msg".into(),
1112            },
1113            CanonicalStepSpec::WireTap {
1114                uri: "direct:audit".into(),
1115            },
1116            CanonicalStepSpec::Stop,
1117            CanonicalStepSpec::Delay {
1118                delay_ms: 100,
1119                dynamic_header: None,
1120            },
1121        ];
1122        let json = serde_json::to_string_pretty(&steps).unwrap();
1123        let back: Vec<CanonicalStepSpec> = serde_json::from_str(&json).unwrap();
1124        assert_eq!(steps, back);
1125    }
1126
1127    #[test]
1128    fn canonical_cache_peek_stale_on_miss_round_trip() {
1129        let some = CanonicalStepSpec::CachePeekStale {
1130            repository: None,
1131            key: "k".into(),
1132            on_miss: Some("continue".into()),
1133        };
1134        let json = serde_json::to_string(&some).unwrap();
1135        assert!(json.contains("continue"));
1136        let back: CanonicalStepSpec = serde_json::from_str(&json).unwrap();
1137        assert_eq!(some, back);
1138
1139        let none = CanonicalStepSpec::CachePeekStale {
1140            repository: None,
1141            key: "k".into(),
1142            on_miss: None,
1143        };
1144        let json = serde_json::to_string(&none).unwrap();
1145        assert!(
1146            !json.contains("on_miss"),
1147            "on_miss: None must omit the key from JSON, got: {json}"
1148        );
1149        let back: CanonicalStepSpec = serde_json::from_str(&json).unwrap();
1150        assert_eq!(none, back);
1151    }
1152
1153    #[test]
1154    fn canonical_cache_invalidate_prefix_round_trip() {
1155        let prefixed = CanonicalStepSpec::CacheInvalidate {
1156            repository: Some("persistent".into()),
1157            key: None,
1158            key_prefix: Some("ns:".into()),
1159        };
1160        let json = serde_json::to_string(&prefixed).unwrap();
1161        assert!(json.contains("key_prefix"), "must emit key_prefix: {json}");
1162        let back: CanonicalStepSpec = serde_json::from_str(&json).unwrap();
1163        assert_eq!(prefixed, back);
1164
1165        // Legacy wire form carrying only `key` still deserializes with
1166        // `key_prefix: None` (no key-iteration field).
1167        let legacy = r#"{"step":"cache_invalidate","config":{"repository":null,"key":"k"}}"#;
1168        let parsed: CanonicalStepSpec = serde_json::from_str(legacy).unwrap();
1169        assert_eq!(
1170            parsed,
1171            CanonicalStepSpec::CacheInvalidate {
1172                repository: None,
1173                key: Some("k".into()),
1174                key_prefix: None,
1175            }
1176        );
1177    }
1178
1179    #[test]
1180    fn canonical_cache_clear_stats_round_trip() {
1181        let clear = CanonicalStepSpec::CacheClear {
1182            repository: Some("persistent".into()),
1183        };
1184        let json = serde_json::to_string(&clear).unwrap();
1185        assert!(json.contains("cache_clear"));
1186        let back: CanonicalStepSpec = serde_json::from_str(&json).unwrap();
1187        assert_eq!(clear, back);
1188
1189        let stats = CanonicalStepSpec::CacheStats { repository: None };
1190        let json = serde_json::to_string(&stats).unwrap();
1191        assert!(json.contains("cache_stats"));
1192        let back: CanonicalStepSpec = serde_json::from_str(&json).unwrap();
1193        assert_eq!(stats, back);
1194    }
1195
1196    #[test]
1197    fn canonical_circuit_breaker_fallback_roundtrip() {
1198        let mut spec = CanonicalRouteSpec::new("cb-fallback", "direct:start");
1199        spec.circuit_breaker = Some(CanonicalCircuitBreakerSpec {
1200            failure_threshold: 1,
1201            open_duration_ms: 60000,
1202            fallback: vec![CanonicalStepSpec::CachePeekStale {
1203                repository: Some("persistent".into()),
1204                key: "tile-xyz".into(),
1205                on_miss: None,
1206            }],
1207        });
1208
1209        let json = serde_json::to_string(&spec).unwrap();
1210        let back: CanonicalRouteSpec = serde_json::from_str(&json).unwrap();
1211        assert_eq!(spec, back);
1212        assert_eq!(back.circuit_breaker.as_ref().unwrap().fallback.len(), 1);
1213
1214        // A spec serialized without the fallback key deserializes with empty fallback.
1215        let without = CanonicalCircuitBreakerSpec {
1216            failure_threshold: 1,
1217            open_duration_ms: 60000,
1218            fallback: vec![],
1219        };
1220        let json = serde_json::to_string(&without).unwrap();
1221        assert!(
1222            !json.contains("fallback"),
1223            "empty fallback must omit the key from JSON, got: {json}"
1224        );
1225        let back: CanonicalCircuitBreakerSpec = serde_json::from_str(&json).unwrap();
1226        assert!(back.fallback.is_empty());
1227    }
1228
1229    #[test]
1230    fn canonical_route_spec_json_schema_generates() {
1231        let schema = schemars::schema_for!(CanonicalRouteSpec);
1232        let json = serde_json::to_string(&schema).unwrap();
1233        assert!(json.contains("CanonicalRouteSpec"));
1234        assert!(json.contains("route_id"));
1235    }
1236
1237    #[test]
1238    fn canonical_json_schema_has_no_function_step() {
1239        let schema = schemars::schema_for!(CanonicalRouteSpec);
1240        let json = serde_json::to_string(&schema).unwrap();
1241        assert!(
1242            !json.contains("\"function\""),
1243            "canonical JSON schema must not contain 'function' step"
1244        );
1245    }
1246
1247    #[test]
1248    fn canonical_contract_does_not_support_function() {
1249        assert!(
1250            !canonical_contract_supports_step("function"),
1251            "function must not be in CANONICAL_CONTRACT_SUPPORTED_STEPS"
1252        );
1253    }
1254
1255    #[test]
1256    fn runtime_command_result_all_variants_are_distinct() {
1257        let accepted = RuntimeCommandResult::Accepted;
1258        let dup = RuntimeCommandResult::Duplicate {
1259            command_id: "c1".into(),
1260        };
1261        let registered = RuntimeCommandResult::RouteRegistered {
1262            route_id: "r1".into(),
1263        };
1264        let changed = RuntimeCommandResult::RouteStateChanged {
1265            route_id: "r1".into(),
1266            status: "Started".into(),
1267        };
1268
1269        assert_ne!(accepted, dup);
1270        assert_ne!(dup, registered);
1271        assert_ne!(registered, changed);
1272
1273        let dup2 = RuntimeCommandResult::Duplicate {
1274            command_id: "c1".into(),
1275        };
1276        assert_eq!(dup, dup2);
1277    }
1278
1279    #[test]
1280    fn runtime_event_serialization_round_trip() {
1281        let event = RuntimeEvent::RouteFailed {
1282            route_id: "route-a".to_string(),
1283            error: "boom".to_string(),
1284        };
1285        let json = serde_json::to_string(&event).unwrap();
1286        let back: RuntimeEvent = serde_json::from_str(&json).unwrap();
1287        assert_eq!(event, back);
1288    }
1289
1290    #[test]
1291    fn noop_runtime_execute_and_ask_return_expected_shapes() {
1292        let rt = NoopRuntime;
1293        let cmd = RuntimeCommand::RegisterRoute {
1294            spec: CanonicalRouteSpec::new("r2", "timer:tick"),
1295            command_id: "c1".into(),
1296            causation_id: None,
1297        };
1298        let cmd_result = block_on(rt.execute(cmd)).unwrap();
1299        assert_eq!(
1300            cmd_result,
1301            RuntimeCommandResult::RouteRegistered {
1302                route_id: "r2".into()
1303            }
1304        );
1305
1306        let query_result = block_on(rt.ask(RuntimeQuery::GetRouteStatus {
1307            route_id: "r2".into(),
1308        }))
1309        .unwrap();
1310        assert_eq!(
1311            query_result,
1312            RuntimeQueryResult::RouteStatus {
1313                route_id: "r2".into(),
1314                status: "Started".into()
1315            }
1316        );
1317    }
1318
1319    #[test]
1320    fn canonical_contract_name_and_version_constants_match() {
1321        assert_eq!(CANONICAL_CONTRACT_NAME, "canonical-v1");
1322        assert_eq!(CANONICAL_CONTRACT_VERSION, 2);
1323    }
1324
1325    #[test]
1326    fn canonical_concurrency_spec_rejects_zero_max() {
1327        let spec = CanonicalRouteSpec::new("r1", "timer:tick")
1328            .with_concurrency(CanonicalConcurrencySpec::Concurrent { max: 0 });
1329        let err = spec.validate_contract().unwrap_err().to_string();
1330        assert!(err.contains("concurrency max must be > 0"), "{err}");
1331    }
1332
1333    #[test]
1334    fn canonical_v2_round_trip() {
1335        let spec = CanonicalRouteSpec::new("r1", "timer:tick")
1336            .with_auto_startup(false)
1337            .with_startup_order(42)
1338            .with_concurrency(CanonicalConcurrencySpec::Concurrent { max: 8 });
1339        spec.validate_contract().unwrap();
1340    }
1341
1342    #[test]
1343    fn canonical_v2_version_is_2() {
1344        assert_eq!(CANONICAL_CONTRACT_VERSION, 2);
1345    }
1346
1347    #[test]
1348    fn canonical_loss_report_builder() {
1349        let report =
1350            CanonicalLossReport::from_field("error_handler", "not supported by canonical path", 2);
1351        assert_eq!(report.dropped_fields.len(), 1);
1352        assert_eq!(report.dropped_fields[0].field, "error_handler");
1353    }
1354
1355    #[test]
1356    fn canonical_v1_json_deserializes_in_v2() {
1357        let json = r#"{"route_id":"r1","from":"timer:tick","steps":[],"version":1}"#;
1358        let spec: CanonicalRouteSpec = serde_json::from_str(json).unwrap();
1359        assert_eq!(spec.route_id, "r1");
1360        assert!(spec.auto_startup.is_none());
1361        assert!(spec.startup_order.is_none());
1362        assert!(spec.concurrency.is_none());
1363        // CRITICAL: v1 specs must pass validation in v2 runtime (backward compat)
1364        spec.validate_contract().unwrap();
1365    }
1366}