This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26726-chaos-expect-time-loss
in repository https://gitbox.apache.org/repos/asf/camel.git

commit dd161c542b82e17abea1391d358edc1df94e2988
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 7 23:14:29 2026 +0100

    CAMEL-26726: camel-chaos - expectations on the time of a route and on the 
messages lost between two routes
    
    - expect=orders:meanTime<=2s and expect=orders:maxTime<=5s: the time of
      the exchanges in the route, from entering it until the route is done
      with them, measured by chaos through TimeSource (virtual time in a
      simulation test). maxTime is unmet as soon as one exchange is slower,
      meanTime is decided when the time is up; a time of no exchange is not
      met.
    - expect=orders->shipping:lost<=0: the messages completed in the first
      route and not in the second, counted until the time is up; the missing
      ones count as lost in the report.
    - The report (reason, text, JSON spec/toRoute/samples), the rule in
      plain words, the chaos model, camel get chaos and the chaos screen say
      them; docs: Expectations on time and loss.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/chaos-model.json      |  14 +-
 .../org/apache/camel/catalog/docs/chaos.adoc       |  49 +++++-
 .../org/apache/camel/chaos/chaos-model.json        |  14 +-
 components/camel-chaos/src/main/docs/chaos.adoc    |  49 +++++-
 .../camel/component/chaos/ChaosExchangeState.java  |   4 +
 .../apache/camel/component/chaos/ChaosManager.java |  27 ++++
 .../apache/camel/component/chaos/ChaosModel.java   |   6 +-
 .../apache/camel/component/chaos/ChaosReport.java  |  29 +++-
 .../component/chaos/ChaosRoutePolicyFactory.java   |  15 +-
 .../apache/camel/component/chaos/ChaosRule.java    | 155 +++++++++++++++---
 .../camel/component/chaos/ChaosRuleParser.java     |  48 +++++-
 .../chaos/ChaosExpectTimeAndLossTest.java          | 180 +++++++++++++++++++++
 .../camel/component/chaos/ChaosRuleParserTest.java |   2 +-
 design/chaos.adoc                                  |   4 +
 .../dsl/jbang/core/commands/process/ListChaos.java |  22 ++-
 .../dsl/jbang/core/commands/tui/ChaosAiPrompt.java |   5 +-
 .../dsl/jbang/core/commands/tui/ChaosInfo.java     |   3 +
 .../dsl/jbang/core/commands/tui/ChaosTab.java      |  10 +-
 18 files changed, 591 insertions(+), 45 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
index 214f24a833aa..1e7ad670ecd5 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
@@ -448,7 +448,7 @@
         "name": "expect",
         "type": "expectation",
         "required": false,
-        "description": "An expectation of a route after the rule is added: 
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+        "description": "An expectation of a route after the rule is added: 
<route>:<count> (at least that many completed), <route>:<kind><op><count>, a 
time (<route>:meanTime<=2s) or the messages lost between two routes 
(<route>-><route>:lost<=0)",
         "example": "orders:handled<=0"
       },
       {
@@ -519,6 +519,18 @@
         {
           "name": "redeliveries",
           "description": "The redeliveries in the route"
+        },
+        {
+          "name": "meanTime",
+          "description": "The mean time of the exchanges in the route, from 
entering it until done, such as meanTime<=2s"
+        },
+        {
+          "name": "maxTime",
+          "description": "The longest time of an exchange in the route, from 
entering it until done, such as maxTime<=5s"
+        },
+        {
+          "name": "lost",
+          "description": "The messages completed in the route and not in the 
next one, such as orders->shipping:lost<=0"
         }
       ],
       "ops": [
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index ba00e70049de..67b3ebfdbabf 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -211,7 +211,10 @@ until:   a number of matches (until=100) or a time 
(until=2m), counted from the
 time:    a number with a unit (ms, s, m, h, d), also combined as 1m30s; a 
number alone is milliseconds
 expect:  expect=<route id>:<count> (at least that many completed exchanges), or
          expect=<route id>:<kind><op><count> with kind completed | handled | 
failed | redeliveries
-         and op >= | <= | = (such as expect=orders:handled<=0); with an 
optional within=<time>
+         and op >= | <= | = (such as expect=orders:handled<=0), or a time
+         expect=<route id>:meanTime|maxTime<op><time> (such as 
expect=orders:meanTime<=2s), or the
+         messages lost between two routes expect=<route id>-><route 
id>:lost<op><count> (such as
+         expect=orders->shipping:lost<=0); with an optional within=<time>
 ----
 
 The patterns of the targets are matched as text, with `*` for any characters; 
they are not regular expressions. The
@@ -657,6 +660,50 @@ give their reason only.
 In JSON an expectation has its `kind`, `op`, `count` and the `value` counted 
so far (and `completed`, the same value, for
 an expectation of completed exchanges).
 
+=== Expectations on time and loss
+
+The delay faults slow a route down, and the question is whether the route 
stays fast enough: an expectation on time
+checks it. The question people ask about a lost message is often "did every 
order reach shipping", and an expectation on
+two routes checks that.
+
+[cols="2,3"]
+|===
+|Expectation |Is met when
+
+|`orders:meanTime<=2s` |The mean time of the exchanges in route `orders` is at 
most 2 s when the time is up.
+|`orders:maxTime<=5s` |No exchange of route `orders` took longer than 5 s. One 
that does makes it unmet at once.
+|`orders->shipping:lost<=0` (or `lost=0`) |Every message that completed in 
route `orders` also completed in route
+`shipping`: the count of `shipping` is at most 0 behind the count of `orders` 
when the time is up.
+|===
+
+* The time of an exchange in a route is measured by chaos, from when the 
exchange enters the route until the route is
+done with it the first time (whether it completed, was handled by the error 
handler or failed), including the
+redeliveries in between. It is read through `TimeSource`, so in a 
deterministic simulation test it is virtual time. A
+time is any time of the rules, such as `500ms`, `2s` or `1m30s`; the op is 
`<=` or `>=` (a `meanTime>=1s` checks that a
+delay rule slows the route as it should).
+* A time of no exchange says nothing, so an expectation on time whose route 
did no exchange in the time is unmet.
+* A loss counts the exchanges completed in both routes from when the rule is 
added until the time is up, and the
+difference is the messages lost (never below 0, as the second route may also 
get messages from elsewhere). Messages still
+on their way when the time is up count as lost too: let `within=` run past the 
`until=` of the faults, so the last ones
+can arrive. The messages it counts as lost are added to the lost of the 
report. The op is `<=` or `=`.
+
+For example, a stock service that is slow for a minute, and the orders that 
must still ship, fast enough:
+
+[source,properties]
+----
+camel.chaos.rules = \
+  endpoint=http:stock* delay duration=1s..3s rate=0.3 until=1m 
expect=orders:meanTime<=2s within=2m;\
+  endpoint=seda:shipping drop rate=0.01 until=1m 
expect=orders->shipping:lost<=0 within=2m
+----
+
+An unmet one gives its reason in the report, such as `route orders: mean 
2s340ms of 118 exchanges within 2m0s, at most
+2s expected (rule r1)` or `routes orders->shipping: 1 lost (118 completed in 
orders, 117 in shipping) within 2m0s, at
+most 0 expected (rule r2): 1 lost`. In JSON these expectations have their 
`spec` as the rule writes it, the `count` and
+the `value` in milliseconds for a time, the `toRoute` of a loss, and the 
`samples` (the exchanges of the time, or the
+ones completed in the first route).
+
+The route statistics of Camel have no percentiles, so there is no `p99` yet.
+
 === A rule in plain words
 
 Each rule of the list and the report has its `description`: what it does, to 
which share of what, with the
diff --git 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
index 214f24a833aa..1e7ad670ecd5 100644
--- 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
+++ 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
@@ -448,7 +448,7 @@
         "name": "expect",
         "type": "expectation",
         "required": false,
-        "description": "An expectation of a route after the rule is added: 
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+        "description": "An expectation of a route after the rule is added: 
<route>:<count> (at least that many completed), <route>:<kind><op><count>, a 
time (<route>:meanTime<=2s) or the messages lost between two routes 
(<route>-><route>:lost<=0)",
         "example": "orders:handled<=0"
       },
       {
@@ -519,6 +519,18 @@
         {
           "name": "redeliveries",
           "description": "The redeliveries in the route"
+        },
+        {
+          "name": "meanTime",
+          "description": "The mean time of the exchanges in the route, from 
entering it until done, such as meanTime<=2s"
+        },
+        {
+          "name": "maxTime",
+          "description": "The longest time of an exchange in the route, from 
entering it until done, such as maxTime<=5s"
+        },
+        {
+          "name": "lost",
+          "description": "The messages completed in the route and not in the 
next one, such as orders->shipping:lost<=0"
         }
       ],
       "ops": [
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index ba00e70049de..67b3ebfdbabf 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -211,7 +211,10 @@ until:   a number of matches (until=100) or a time 
(until=2m), counted from the
 time:    a number with a unit (ms, s, m, h, d), also combined as 1m30s; a 
number alone is milliseconds
 expect:  expect=<route id>:<count> (at least that many completed exchanges), or
          expect=<route id>:<kind><op><count> with kind completed | handled | 
failed | redeliveries
-         and op >= | <= | = (such as expect=orders:handled<=0); with an 
optional within=<time>
+         and op >= | <= | = (such as expect=orders:handled<=0), or a time
+         expect=<route id>:meanTime|maxTime<op><time> (such as 
expect=orders:meanTime<=2s), or the
+         messages lost between two routes expect=<route id>-><route 
id>:lost<op><count> (such as
+         expect=orders->shipping:lost<=0); with an optional within=<time>
 ----
 
 The patterns of the targets are matched as text, with `*` for any characters; 
they are not regular expressions. The
@@ -657,6 +660,50 @@ give their reason only.
 In JSON an expectation has its `kind`, `op`, `count` and the `value` counted 
so far (and `completed`, the same value, for
 an expectation of completed exchanges).
 
+=== Expectations on time and loss
+
+The delay faults slow a route down, and the question is whether the route 
stays fast enough: an expectation on time
+checks it. The question people ask about a lost message is often "did every 
order reach shipping", and an expectation on
+two routes checks that.
+
+[cols="2,3"]
+|===
+|Expectation |Is met when
+
+|`orders:meanTime<=2s` |The mean time of the exchanges in route `orders` is at 
most 2 s when the time is up.
+|`orders:maxTime<=5s` |No exchange of route `orders` took longer than 5 s. One 
that does makes it unmet at once.
+|`orders->shipping:lost<=0` (or `lost=0`) |Every message that completed in 
route `orders` also completed in route
+`shipping`: the count of `shipping` is at most 0 behind the count of `orders` 
when the time is up.
+|===
+
+* The time of an exchange in a route is measured by chaos, from when the 
exchange enters the route until the route is
+done with it the first time (whether it completed, was handled by the error 
handler or failed), including the
+redeliveries in between. It is read through `TimeSource`, so in a 
deterministic simulation test it is virtual time. A
+time is any time of the rules, such as `500ms`, `2s` or `1m30s`; the op is 
`<=` or `>=` (a `meanTime>=1s` checks that a
+delay rule slows the route as it should).
+* A time of no exchange says nothing, so an expectation on time whose route 
did no exchange in the time is unmet.
+* A loss counts the exchanges completed in both routes from when the rule is 
added until the time is up, and the
+difference is the messages lost (never below 0, as the second route may also 
get messages from elsewhere). Messages still
+on their way when the time is up count as lost too: let `within=` run past the 
`until=` of the faults, so the last ones
+can arrive. The messages it counts as lost are added to the lost of the 
report. The op is `<=` or `=`.
+
+For example, a stock service that is slow for a minute, and the orders that 
must still ship, fast enough:
+
+[source,properties]
+----
+camel.chaos.rules = \
+  endpoint=http:stock* delay duration=1s..3s rate=0.3 until=1m 
expect=orders:meanTime<=2s within=2m;\
+  endpoint=seda:shipping drop rate=0.01 until=1m 
expect=orders->shipping:lost<=0 within=2m
+----
+
+An unmet one gives its reason in the report, such as `route orders: mean 
2s340ms of 118 exchanges within 2m0s, at most
+2s expected (rule r1)` or `routes orders->shipping: 1 lost (118 completed in 
orders, 117 in shipping) within 2m0s, at
+most 0 expected (rule r2): 1 lost`. In JSON these expectations have their 
`spec` as the rule writes it, the `count` and
+the `value` in milliseconds for a time, the `toRoute` of a loss, and the 
`samples` (the exchanges of the time, or the
+ones completed in the first route).
+
+The route statistics of Camel have no percentiles, so there is no `p99` yet.
+
 === A rule in plain words
 
 Each rule of the list and the report has its `description`: what it does, to 
which share of what, with the
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
index 1dff5138ff72..ed6dc216308f 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
@@ -70,6 +70,10 @@ final class ChaosExchangeState implements SafeCopyProperty {
         boolean expectedCompleted;
         boolean expectedHandled;
         boolean expectedFailed;
+        // when the exchange entered the route, for the expectations on time 
(meanTime, maxTime), else -1; and whether
+        // they counted its time
+        long enteredAt = -1;
+        boolean expectedTime;
     }
 
     private final Map<String, RouteState> routes = new HashMap<>(4);
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
index 87c4bcdf578d..e8512258a934 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
@@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.LongAdder;
+import java.util.stream.Collectors;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Consumer;
@@ -352,6 +353,10 @@ public final class ChaosManager {
         if (rule.expectation != null && 
camelContext.getRoute(rule.expectation.route) == null) {
             answer.add("expects route " + rule.expectation.route + ", but 
there is no route with that id (yet)");
         }
+        if (rule.expectation != null && rule.expectation.toRoute != null
+                && camelContext.getRoute(rule.expectation.toRoute) == null) {
+            answer.add("expects route " + rule.expectation.toRoute + ", but 
there is no route with that id (yet)");
+        }
         String target = ChaosTargets.check(camelContext, rule);
         if (target != null) {
             answer.add(target);
@@ -607,6 +612,17 @@ public final class ChaosManager {
             state.expectedFailed = true;
             record(routeId, ChaosRule.Expectation.Kind.FAILED);
         }
+        if (state.enteredAt >= 0 && !state.expectedTime && outcome != 
ChaosExchangeState.Outcome.ENTERED) {
+            // the time of the exchange in the route, from when it entered it 
until the route was done with it the
+            // first time
+            state.expectedTime = true;
+            long millis = Math.max(0, TimeSource.currentTimeMillis() - 
state.enteredAt);
+            for (ChaosRule.Expectation expectation : ruleSet.expectations) {
+                if (expectation.kind.time() && 
expectation.route.equals(routeId)) {
+                    expectation.recordTime(millis);
+                }
+            }
+        }
     }
 
     /** The error handler redelivers a step of the route: counted for the 
report and the expectations. */
@@ -619,6 +635,13 @@ public final class ChaosManager {
         for (ChaosRule.Expectation expectation : ruleSet.expectations) {
             if (expectation.kind == kind && expectation.route.equals(routeId)) 
{
                 expectation.record();
+            } else if (kind == ChaosRule.Expectation.Kind.COMPLETED && 
expectation.kind == ChaosRule.Expectation.Kind.LOST) {
+                // the messages one route completed, and those the next route 
completed
+                if (expectation.route.equals(routeId)) {
+                    expectation.recordLost(false);
+                } else if (routeId.equals(expectation.toRoute)) {
+                    expectation.recordLost(true);
+                }
             }
         }
     }
@@ -888,6 +911,8 @@ public final class ChaosManager {
         final ChaosRule[] lifecycle;
         final ChaosRule.Expectation[] expectations;
         final ChaosRule[] expectationRules;
+        // the routes with an expectation on their time, whose route policy 
notes when an exchange enters
+        final Set<String> timedRoutes;
 
         RuleSet(List<ChaosRule> rules, long now) {
             // also the rules that start later (from=): the hooks skip them 
until then
@@ -905,6 +930,8 @@ public final class ChaosManager {
             expectations = Arrays.stream(expectationRules).map(r -> 
r.expectation)
                     .filter(e -> e.outcome(now) == 
ChaosRule.Expectation.Outcome.PENDING && !e.frozen())
                     .toArray(ChaosRule.Expectation[]::new);
+            timedRoutes = Arrays.stream(expectations).filter(e -> 
e.kind.time()).map(e -> e.route)
+                    .collect(Collectors.toUnmodifiableSet());
         }
     }
 
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
index 80159d7452a2..576d2287d61b 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
@@ -339,7 +339,9 @@ final class ChaosModel {
                         "When the rule starts after it is added, such as after 
the warm-up (+30s or 30s)", "+30s");
                 case "expect" -> param("expect", "expectation", false,
                         "An expectation of a route after the rule is added: 
<route>:<count> (at least that many"
-                                                                       + " 
completed) or <route>:<kind><op><count>",
+                                                                       + " 
completed), <route>:<kind><op><count>, a time"
+                                                                       + " 
(<route>:meanTime<=2s) or the messages lost"
+                                                                       + " 
between two routes (<route>-><route>:lost<=0)",
                         "orders:handled<=0");
                 case "within" -> param("within", "duration", false,
                         "The time of the expectation (by default until the 
rule ends)", "2m");
@@ -355,7 +357,7 @@ final class ChaosModel {
         for (Expectation.Kind kind : Expectation.Kind.values()) {
             JsonObject k = new JsonObject();
             k.put("name", kind.key);
-            k.put("description", "The " + kind.words(2) + " in the route");
+            k.put("description", kind.description);
             kinds.add(k);
         }
         jo.put("kinds", kinds);
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
index 440cbc5f3f4f..a86487664681 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
@@ -103,6 +103,13 @@ public final class ChaosReport {
                     reasons.add("route " + e.route + " completed " + 
entry.value + " of " + e.count + " exchanges within "
                                 + ChaosRuleParser.printTime(rule.within) + " 
(rule " + rule.id + "): " + entry.lost
                                 + " lost");
+                } else if (e.kind.time() || e.kind == 
ChaosRule.Expectation.Kind.LOST) {
+                    // a time, or the messages lost between two routes 
(CAMEL-26726)
+                    String limit = e.kind.time() ? 
ChaosRuleParser.printTime(e.count) : Long.toString(e.count);
+                    reasons.add((e.toRoute != null ? "routes " + e.route + 
"->" + e.toRoute : "route " + e.route) + ": "
+                                + entry.valueText + " within " + 
ChaosRuleParser.printTime(rule.within) + ", "
+                                + e.op.words + " " + limit + " expected (rule 
" + rule.id + ")"
+                                + (entry.lost > 0 ? ": " + entry.lost + " 
lost" : ""));
                 } else {
                     reasons.add("route " + e.route + ": " + entry.value + " " 
+ e.kind.words(entry.value) + " within "
                                 + ChaosRuleParser.printTime(rule.within) + ", 
" + e.op.words + " " + e.count
@@ -231,7 +238,7 @@ public final class ChaosReport {
             if (r.outcome != null) {
                 sb.append("\n      expect ").append(r.expectSpec).append(" 
within ")
                         .append(ChaosRuleParser.printTime(r.within)).append(": 
").append(r.outcome.key)
-                        .append(", ").append(r.value).append(' 
').append(r.expectKind.key);
+                        .append(", ").append(r.valueText);
                 if (r.outcome == Outcome.PENDING) {
                     sb.append(", 
").append(ChaosRuleParser.printTime(r.timeLeft)).append(" left");
                 } else if (r.outcome == Outcome.CANCELLED) {
@@ -468,6 +475,15 @@ public final class ChaosReport {
                 if (r.expectKind == ChaosRule.Expectation.Kind.COMPLETED) {
                     e.put("completed", r.value);
                 }
+                // the expectation as the rule writes it, and for a time 
(count and value in milliseconds) or the
+                // messages lost between two routes, the exchanges it counted 
(CAMEL-26726)
+                e.put("spec", r.expectSpec);
+                if (r.expectToRoute != null) {
+                    e.put("toRoute", r.expectToRoute);
+                }
+                if (r.expectKind.time() || r.expectToRoute != null) {
+                    e.put("samples", r.samples);
+                }
                 e.put("outcome", r.outcome.key);
                 if (r.outcome == Outcome.PENDING) {
                     e.put("timeLeft", r.timeLeft);
@@ -658,6 +674,11 @@ public final class ChaosReport {
         final long within;
         final Outcome outcome;
         final long value;
+        // the value as the report says it, such as 3 completed or mean 
1s200ms of 12 exchanges
+        final String valueText;
+        // the next route of a lost expectation, and the exchanges a time or a 
lost expectation counted
+        final String expectToRoute;
+        final long samples;
         final long lost;
         final long timeLeft;
         // a lifecycle rule: its action, route, duration and what it did, else 
null
@@ -726,6 +747,9 @@ public final class ChaosReport {
                 ChaosRule.Expectation.Reading reading = expectation.read(now);
                 outcome = reading.outcome();
                 value = reading.value();
+                valueText = expectation.valueText(reading);
+                expectToRoute = expectation.toRoute;
+                samples = reading.samples();
                 lost = expectation.lost(reading);
                 timeLeft = Math.max(0, expectation.deadline() - now);
             } else {
@@ -736,6 +760,9 @@ public final class ChaosReport {
                 expectCount = 0;
                 outcome = null;
                 value = 0;
+                valueText = null;
+                expectToRoute = null;
+                samples = 0;
                 lost = 0;
                 timeLeft = 0;
             }
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
index a042dc0240d7..b452c41c4858 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
@@ -60,11 +60,22 @@ final class ChaosRoutePolicyFactory implements 
RoutePolicyFactory {
 
         @Override
         public void onExchangeBegin(Route route, Exchange exchange) {
-            ChaosRule[] rules = manager.ruleSet().entry;
-            if (rules.length == 0 || 
!ChaosExchangeState.of(exchange).enter(route.getRouteId())) {
+            ChaosManager.RuleSet set = manager.ruleSet();
+            ChaosRule[] rules = set.entry;
+            // an expectation on the time of the route needs when the exchange 
entered it (CAMEL-26726)
+            boolean timed = set.timedRoutes.contains(route.getRouteId());
+            if (rules.length == 0 && !timed) {
                 return;
             }
+            ChaosExchangeState exchangeState = ChaosExchangeState.of(exchange);
+            boolean first = exchangeState.enter(route.getRouteId());
             long now = TimeSource.currentTimeMillis();
+            if (timed && first) {
+                exchangeState.route(route.getRouteId()).enteredAt = now;
+            }
+            if (rules.length == 0 || !first) {
+                return;
+            }
             for (ChaosRule rule : rules) {
                 if (rule.live(now) && rule.matchesId(route.getRouteId()) && 
rule.matchesWhen(exchange)
                         && rule.fires(now)) {
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
index ce273c6469ab..29887ed736f6 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
@@ -197,6 +197,8 @@ final class ChaosRule {
     final long untilCount;
     final long untilDuration;
     final String expectRoute;
+    // the next route of a lost expectation (orders->shipping:lost<=0), else 
null
+    final String expectToRoute;
     final long expectCount;
     final long within;
     // the action of a lifecycle rule (stall is suspend), else null
@@ -277,6 +279,7 @@ final class ChaosRule {
         this.untilCount = b.untilCount;
         this.untilDuration = b.untilDuration;
         this.expectRoute = b.expectRoute;
+        this.expectToRoute = b.expectToRoute;
         this.expectCount = b.expectCount;
         this.action = b.action;
         this.stall = b.stall;
@@ -290,7 +293,8 @@ final class ChaosRule {
         this.endsAt = plus(startsAt, runFor);
         this.within = b.within > 0 ? b.within : plus(from, untilDuration > 0 ? 
untilDuration : maxDuration);
         this.expectation = expectRoute != null
-                ? new Expectation(expectRoute, b.expectKind, b.expectOp, 
expectCount, plus(addedAt, within)) : null;
+                ? new Expectation(expectRoute, expectToRoute, b.expectKind, 
b.expectOp, expectCount, plus(addedAt, within))
+                : null;
         for (int i = 0; i < fired.length; i++) {
             fired[i] = new LongAdder();
         }
@@ -510,12 +514,24 @@ final class ChaosRule {
         if (expectation != null) {
             Expectation e = expectation;
             sb.append("; expects ");
-            if (e.op == Expectation.Op.AT_MOST && e.count == 0) {
-                sb.append("no ").append(e.kind.words(0));
+            if (e.kind == Expectation.Kind.MEAN_TIME) {
+                sb.append("a mean time of ").append(e.op.words).append(' 
').append(ChaosWords.time(e.count))
+                        .append(" in route ").append(e.route);
+            } else if (e.kind == Expectation.Kind.MAX_TIME) {
+                sb.append("every exchange in route ").append(e.route).append(" 
to take ").append(e.op.words).append(' ')
+                        .append(ChaosWords.time(e.count));
+            } else if (e.kind == Expectation.Kind.LOST) {
+                sb.append(e.count == 0 ? "no message" : e.op.words + " " + 
e.count + " " + e.kind.words(e.count))
+                        .append(e.count == 0 ? " lost" : "").append(" between 
route ").append(e.route).append(" and route ")
+                        .append(e.toRoute);
             } else {
-                sb.append(e.op.words).append(' ').append(e.count).append(' 
').append(e.kind.words(e.count));
+                if (e.op == Expectation.Op.AT_MOST && e.count == 0) {
+                    sb.append("no ").append(e.kind.words(0));
+                } else {
+                    sb.append(e.op.words).append(' ').append(e.count).append(' 
').append(e.kind.words(e.count));
+                }
+                sb.append(" in route ").append(e.route);
             }
-            sb.append(" in route ").append(e.route);
             if (within != plus(from, runFor)) {
                 // a within= of its own (the default is until the rule ends)
                 sb.append(" within ").append(ChaosWords.time(within));
@@ -863,25 +879,41 @@ final class ChaosRule {
 
         /** What an expectation counts in its route. */
         enum Kind {
-            COMPLETED("completed", "exchange completed", "exchanges 
completed"),
-            HANDLED("handled", "exchange handled by the error handler", 
"exchanges handled by the error handler"),
-            FAILED("failed", "exchange failed", "exchanges failed"),
-            REDELIVERIES("redeliveries", "redelivery", "redeliveries");
+            COMPLETED("completed", "exchange completed", "exchanges 
completed", "The exchanges completed in the route"),
+            HANDLED("handled", "exchange handled by the error handler", 
"exchanges handled by the error handler",
+                    "The exchanges handled by the error handler in the route"),
+            FAILED("failed", "exchange failed", "exchanges failed", "The 
exchanges failed in the route"),
+            REDELIVERIES("redeliveries", "redelivery", "redeliveries", "The 
redeliveries in the route"),
+            // the time an exchange is in the route, from when it enters until 
the route is done with it (CAMEL-26726)
+            MEAN_TIME("meanTime", "mean time", "mean time",
+                      "The mean time of the exchanges in the route, from 
entering it until done, such as meanTime<=2s"),
+            MAX_TIME("maxTime", "max time", "max time",
+                     "The longest time of an exchange in the route, from 
entering it until done, such as maxTime<=5s"),
+            // the messages one route completed that the next one did not: 
<route>-><next route>:lost<=0
+            LOST("lost", "message lost", "messages lost",
+                 "The messages completed in the route and not in the next one, 
such as orders->shipping:lost<=0");
 
             final String key;
             // for the reason of an unmet expectation
             private final String one;
             private final String many;
+            final String description;
 
-            Kind(String key, String one, String many) {
+            Kind(String key, String one, String many, String description) {
                 this.key = key;
                 this.one = one;
                 this.many = many;
+                this.description = description;
             }
 
             String words(long n) {
                 return n == 1 ? one : many;
             }
+
+            /** Whether the count of the kind is a time (in milliseconds), not 
a number of exchanges. */
+            boolean time() {
+                return this == MEAN_TIME || this == MAX_TIME;
+            }
         }
 
         /** How the count of an expectation compares. */
@@ -900,11 +932,19 @@ final class ChaosRule {
         }
 
         final String route;
+        // the next route of a lost expectation, else null
+        final String toRoute;
         final Kind kind;
         final Op op;
+        // a number, or a time in milliseconds for a time kind
         final long count;
         private long deadline;
         private long value;
+        // a time kind: the exchanges done, their time together and the 
longest; lost: the completed of both routes
+        private long samples;
+        private long sum;
+        private long max;
+        private long toValue;
         // when it was decided before the time was up (met for at least, unmet 
for at most and exactly), and how
         private long decidedAt = -1;
         private Outcome decided;
@@ -915,7 +955,12 @@ final class ChaosRule {
         }
 
         Expectation(String route, Kind kind, Op op, long count, long deadline) 
{
+            this(route, null, kind, op, count, deadline);
+        }
+
+        Expectation(String route, String toRoute, Kind kind, Op op, long 
count, long deadline) {
             this.route = route;
+            this.toRoute = toRoute;
             this.kind = kind;
             this.op = op;
             this.count = count;
@@ -943,6 +988,39 @@ final class ChaosRule {
             }
         }
 
+        /** An exchange was done in the route, after it was so long in it (a 
time kind). */
+        synchronized void recordTime(long millis) {
+            recordTimeAt(millis, TimeSource.currentTimeMillis());
+        }
+
+        synchronized void recordTimeAt(long millis, long now) {
+            if (frozenAt >= 0 || now > deadline) {
+                return;
+            }
+            samples++;
+            sum += millis;
+            max = Math.max(max, millis);
+            if (decided == null && kind == Kind.MAX_TIME && op == Op.AT_MOST 
&& millis > count) {
+                // one exchange took too long: it cannot become met
+                decided = Outcome.UNMET;
+                decidedAt = now;
+            }
+        }
+
+        /**
+         * An exchange completed in the route ({@code next} false) or in the 
next route (true) of a lost expectation.
+         */
+        synchronized void recordLost(boolean next) {
+            if (frozenAt >= 0 || TimeSource.currentTimeMillis() > deadline) {
+                return;
+            }
+            if (next) {
+                toValue++;
+            } else {
+                samples++;
+            }
+        }
+
         synchronized long deadline() {
             return deadline;
         }
@@ -961,7 +1039,12 @@ final class ChaosRule {
         }
 
         synchronized long value() {
-            return value;
+            return switch (kind) {
+                case MEAN_TIME -> samples > 0 ? sum / samples : 0;
+                case MAX_TIME -> max;
+                case LOST -> Math.max(0, samples - toValue);
+                default -> value;
+            };
         }
 
         synchronized Outcome outcome(long now) {
@@ -975,12 +1058,17 @@ final class ChaosRule {
             if (now <= deadline) {
                 return Outcome.PENDING;
             }
-            // the time is up
-            return switch (op) {
-                case AT_LEAST -> Outcome.UNMET;
-                case AT_MOST -> Outcome.MET;
-                case EXACTLY -> value == count ? Outcome.MET : Outcome.UNMET;
+            // the time is up: a time of no exchange says nothing, so it is 
not met
+            if (kind.time() && samples == 0) {
+                return Outcome.UNMET;
+            }
+            long v = value();
+            boolean met = switch (op) {
+                case AT_LEAST -> v >= count;
+                case AT_MOST -> v <= count;
+                case EXACTLY -> v == count;
             };
+            return met ? Outcome.MET : Outcome.UNMET;
         }
 
         synchronized boolean frozen() {
@@ -989,7 +1077,7 @@ final class ChaosRule {
 
         /** The outcome, the counted value and whether it is frozen, read 
together (for the report). */
         synchronized Reading read(long now) {
-            return new Reading(outcome(now), value, frozenAt >= 0);
+            return new Reading(outcome(now), value(), frozenAt >= 0, samples, 
toValue);
         }
 
         /**
@@ -997,18 +1085,40 @@ final class ChaosRule {
          * many, misses the ones that did not complete.
          */
         long lost(Reading reading) {
-            return reading.outcome() == Outcome.UNMET && kind == 
Kind.COMPLETED && op != Op.AT_MOST
-                    ? Math.max(0, count - reading.value()) : 0;
+            if (reading.outcome() != Outcome.UNMET) {
+                return 0;
+            }
+            if (kind == Kind.LOST) {
+                // the messages the next route did not complete
+                return reading.value();
+            }
+            return kind == Kind.COMPLETED && op != Op.AT_MOST ? Math.max(0, 
count - reading.value()) : 0;
         }
 
         /** The expectation as the rule writes it, such as orders:100 or 
orders:handled<=0. */
         String spec() {
-            return kind == Kind.COMPLETED && op == Op.AT_LEAST
-                    ? route + ":" + count : route + ":" + kind.key + op.key + 
count;
+            if (kind == Kind.COMPLETED && op == Op.AT_LEAST) {
+                return route + ":" + count;
+            }
+            String routes = toRoute != null ? route + "->" + toRoute : route;
+            return routes + ":" + kind.key + op.key + (kind.time() ? 
ChaosRuleParser.printTime(count) : count);
+        }
+
+        /** The value as the report says it, such as {@code 3 completed} or 
{@code mean 1s200ms of 12 exchanges}. */
+        String valueText(Reading reading) {
+            return switch (kind) {
+                case MEAN_TIME, MAX_TIME -> reading.samples() == 0
+                        ? "no exchange done yet"
+                        : (kind == Kind.MEAN_TIME ? "mean " : "max ") + 
ChaosRuleParser.printTime(reading.value()) + " of "
+                          + reading.samples() + (reading.samples() == 1 ? " 
exchange" : " exchanges");
+                case LOST -> reading.value() + " lost (" + reading.samples() + 
" completed in " + route + ", "
+                             + reading.next() + " in " + toRoute + ")";
+                default -> reading.value() + " " + kind.key;
+            };
         }
 
         /** What an expectation reads at one time. */
-        record Reading(Outcome outcome, long value, boolean frozen) {
+        record Reading(Outcome outcome, long value, boolean frozen, long 
samples, long next) {
         }
     }
 
@@ -1091,6 +1201,7 @@ final class ChaosRule {
         long untilCount;
         long untilDuration;
         String expectRoute;
+        String expectToRoute;
         Expectation.Kind expectKind = Expectation.Kind.COMPLETED;
         Expectation.Op expectOp = Expectation.Op.AT_LEAST;
         long expectCount;
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
index e7ed74c0d784..73f83bedf216 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
@@ -927,7 +927,17 @@ final class ChaosRuleParser {
                                                + " or 
expect=orders:handled<=0");
         }
         b.expectRoute = expect.substring(0, colon);
-        if (b.expectRoute.indexOf('*') >= 0) {
+        int arrow = b.expectRoute.indexOf("->");
+        if (arrow >= 0) {
+            // the messages one route completed that the next one did not 
(CAMEL-26726)
+            b.expectToRoute = b.expectRoute.substring(arrow + 2);
+            b.expectRoute = b.expectRoute.substring(0, arrow);
+            if (b.expectRoute.isEmpty() || b.expectToRoute.isEmpty() || 
b.expectRoute.equals(b.expectToRoute)) {
+                throw new IllegalArgumentException(
+                        "expect=" + expect + " needs two routes, such as 
expect=orders->shipping:lost<=0");
+            }
+        }
+        if (b.expectRoute.indexOf('*') >= 0 || b.expectToRoute != null && 
b.expectToRoute.indexOf('*') >= 0) {
             throw new IllegalArgumentException(
                     "expect=" + expect + " needs the id of one route, not a 
pattern, such as expect=orders:100");
         }
@@ -949,8 +959,9 @@ final class ChaosRuleParser {
             if (b.expectKind == null) {
                 throw new IllegalArgumentException(
                         "expect=" + expect + " counts " + (kind.isEmpty() ? 
"nothing" : kind)
-                                                   + ": the kinds are 
completed, handled, failed and redeliveries,"
-                                                   + " such as 
expect=orders:handled<=0");
+                                                   + ": the kinds are 
completed, handled, failed, redeliveries, meanTime,"
+                                                   + " maxTime and lost, such 
as expect=orders:handled<=0,"
+                                                   + " 
expect=orders:meanTime<=2s or expect=orders->shipping:lost<=0");
             }
             String rest = spec.substring(op);
             if (rest.startsWith(">=")) {
@@ -966,6 +977,32 @@ final class ChaosRuleParser {
             }
             count = rest.substring(b.expectOp.key.length());
         }
+        if ((b.expectKind == ChaosRule.Expectation.Kind.LOST) != 
(b.expectToRoute != null)) {
+            throw new IllegalArgumentException(
+                    b.expectToRoute != null
+                            ? "expect=" + expect + " has two routes, which 
only lost counts: such as"
+                              + " expect=" + b.expectRoute + "->" + 
b.expectToRoute + ":lost<=0"
+                            : "expect=" + expect + " counts the messages lost 
between two routes, such as"
+                              + " expect=orders->shipping:lost<=0");
+        }
+        if (b.expectKind.time()) {
+            if (b.expectOp == ChaosRule.Expectation.Op.EXACTLY) {
+                throw new IllegalArgumentException(
+                        "expect=" + expect + " needs <= or >= for a time, such 
as expect=" + b.expectRoute + ":"
+                                                   + b.expectKind.key + 
"<=2s");
+            }
+            b.expectCount = duration("expect", count);
+            if (b.expectCount < 1) {
+                throw new IllegalArgumentException("expect=" + expect + " 
needs a time longer than 0");
+            }
+            withinOf(b, within, maxDuration);
+            return;
+        }
+        if (b.expectKind == ChaosRule.Expectation.Kind.LOST && b.expectOp == 
ChaosRule.Expectation.Op.AT_LEAST) {
+            throw new IllegalArgumentException(
+                    "expect=" + expect + " needs <= or = for the messages 
lost, such as expect=" + b.expectRoute + "->"
+                                               + b.expectToRoute + ":lost<=0");
+        }
         if (!isDigits(count) || count.length() > 18) {
             throw new IllegalArgumentException(
                     "expect=" + expect + " needs a count that is a whole 
number of at most 18 digits");
@@ -975,6 +1012,11 @@ final class ChaosRuleParser {
             // at least 0 is always met
             throw new IllegalArgumentException("expect=" + expect + " needs a 
count of 1 or more");
         }
+        withinOf(b, within, maxDuration);
+    }
+
+    /** The time of the expectation (within=), at most camel.chaos.maxDuration 
after the start of the rule. */
+    private static void withinOf(ChaosRule.Builder b, String within, long 
maxDuration) {
         if (within != null) {
             b.within = duration("within", within);
             if (b.within > maxDuration + b.from) {
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
new file mode 100644
index 000000000000..07bc14b73daf
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * CAMEL-26726: expectations on the time of a route (meanTime, maxTime), 
measured by chaos from when an exchange enters
+ * the route until the route is done with it, and on the messages lost between 
two routes (orders->ship:lost<=0). The
+ * clock is virtual: a delay moves it by its length.
+ */
+@Isolated
+class ChaosExpectTimeAndLossTest extends ChaosTestSupport {
+
+    private FakeClock clock;
+
+    @Override
+    protected void setupResources() throws Exception {
+        super.setupResources();
+        clock = FakeClock.install();
+    }
+
+    @Override
+    protected void cleanupResources() throws Exception {
+        clock.close();
+        super.cleanupResources();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:orders").routeId("orders")
+                        .to("mock:stock").id("callStock")
+                        .to("direct:ship");
+                from("direct:ship").routeId("ship").to("mock:ship");
+            }
+        };
+    }
+
+    private void orders(int n) {
+        for (int i = 0; i < n; i++) {
+            template.sendBody("direct:orders", "order " + i);
+        }
+    }
+
+    private JsonObject expectation(String id) {
+        return (JsonObject) ruleReport(id).get("expectation");
+    }
+
+    @Test
+    void theMeanTimeOfARouteIsDecidedWhenTheTimeIsUp() {
+        // every second call to stock is 3s slow: a mean of 1.5s over 4 orders
+        String id = chaos().addRule("step=callStock delay duration=3s 
rate=every:2 expect=orders:meanTime<=2s within=1m");
+        orders(4);
+
+        assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+        clock.advance(60_001);
+        JsonObject e = expectation(id);
+        assertThat(e.getString("outcome")).isEqualTo("met");
+        assertThat(e.getLong("value")).isEqualTo(1500);
+        assertThat(e.getLong("samples")).isEqualTo(4);
+        assertThat(e.getString("spec")).isEqualTo("orders:meanTime<=2s");
+        assertThat(chaos().report().getVerdict()).isEqualTo(ChaosReport.PASS);
+        assertThat(chaos().report().toText())
+                .contains("expect orders:meanTime<=2s within 1m0s: met, mean 
1s500ms of 4 exchanges");
+        assertThat(chaos().describeRule(id))
+                .endsWith("; expects a mean time of at most 2 seconds in route 
orders within 1 minute");
+    }
+
+    @Test
+    void oneSlowExchangeFailsTheMaxTimeAtOnce() {
+        String id = chaos().addRule("step=callStock delay duration=3s 
rate=every:3 expect=orders:maxTime<=1s within=1m");
+        orders(2);
+        assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+        orders(1);
+
+        // decided before the time is up
+        JsonObject e = expectation(id);
+        assertThat(e.getString("outcome")).isEqualTo("unmet");
+        assertThat(e.getLong("value")).isEqualTo(3000);
+        ChaosReport report = chaos().report();
+        assertThat(report.getVerdict()).isEqualTo(ChaosReport.FAIL);
+        assertThat(report.getReasons()).containsExactly(
+                "route orders: max 3s of 3 exchanges within 1m0s, at most 1s 
expected (rule " + id + ")");
+        // a time is not a message lost
+        assertThat(report.getLost()).isZero();
+        assertThat(chaos().describeRule(id)).contains("expects every exchange 
in route orders to take at most 1 second");
+    }
+
+    @Test
+    void aTimeOfNoExchangeIsNotMet() {
+        String id = chaos().addRule("step=callStock delay duration=1ms 
expect=ship:meanTime<=1s within=10s");
+        clock.advance(10_001);
+
+        JsonObject e = expectation(id);
+        assertThat(e.getString("outcome")).isEqualTo("unmet");
+        assertThat(chaos().report().getReasons()).singleElement().asString()
+                .startsWith("route ship: no exchange done yet within 10s");
+    }
+
+    @Test
+    void theMessagesLostBetweenTwoRoutes() {
+        // every second order never reaches the ship route
+        String id = chaos().addRule("endpoint=direct:ship drop rate=every:2 
expect=orders->ship:lost<=0 within=1m");
+        orders(4);
+        assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+        clock.advance(60_001);
+
+        JsonObject e = expectation(id);
+        assertThat(e.getString("outcome")).isEqualTo("unmet");
+        assertThat(e.getString("toRoute")).isEqualTo("ship");
+        assertThat(e.getLong("value")).isEqualTo(2);
+        ChaosReport report = chaos().report();
+        assertThat(report.getLost()).isEqualTo(2);
+        assertThat(report.getReasons()).containsExactly(
+                "routes orders->ship: 2 lost (4 completed in orders, 2 in 
ship) within 1m0s, at most 0 expected (rule " + id
+                                                        + "): 2 lost");
+        assertThat(chaos().describeRule(id))
+                .endsWith("; expects no message lost between route orders and 
route ship within 1 minute");
+    }
+
+    @Test
+    void nothingLostWhenTheNextRouteCompletesThemAll() {
+        String id = chaos().addRule("step=callStock delay duration=10ms 
expect=orders->ship:lost=0 within=1m");
+        orders(3);
+        clock.advance(60_001);
+
+        assertThat(expectation(id).getString("outcome")).isEqualTo("met");
+        assertThat(chaos().report().toText()).contains("met, 0 lost (3 
completed in orders, 3 in ship)");
+    }
+
+    @Test
+    void theExampleOfTheDocumentation() {
+        assertThat(chaos().addRules(
+                "endpoint=http:stock* delay duration=1s..3s rate=0.3 until=1m 
expect=orders:meanTime<=2s within=2m;"
+                                    + "endpoint=seda:shipping drop rate=0.01 
until=1m expect=orders->shipping:lost<=0 within=2m"))
+                .hasSize(2);
+        assertThat(chaos().addRule("step=callStock delay duration=1s 
expect=orders:meanTime>=1s")).isNotNull();
+    }
+
+    @Test
+    void theFormsThatAreRefused() {
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders:meanTime=2s"))
+                .hasMessageContaining("needs <= or >= for a time");
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders:maxTime<=soon"))
+                .hasMessageContaining("expect=");
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders:lost<=0"))
+                .hasMessageContaining("counts the messages lost between two 
routes, such as expect=orders->shipping:lost<=0");
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders->ship:completed>=1"))
+                .hasMessageContaining("has two routes, which only lost 
counts");
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders->ship:lost>=1"))
+                .hasMessageContaining("needs <= or = for the messages lost");
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders->orders:lost<=0"))
+                .hasMessageContaining("needs two routes");
+        assertThatThrownBy(() -> chaos().addRule("step=callStock delay 
duration=1ms expect=orders:speed<=1"))
+                .hasMessageContaining("the kinds are completed, handled, 
failed, redeliveries, meanTime, maxTime and lost");
+    }
+}
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
index c776ad4e0201..375aec13f1fe 100644
--- 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
@@ -463,7 +463,7 @@ class ChaosRuleParserTest {
             "endpoint=x error expect=orders:|is not <route>:<count>",
             "endpoint=x error expect=orders:0|needs a count of 1 or more",
             "endpoint=x error expect=orders:completed>=0|needs a count of 1 or 
more",
-            "endpoint=x error expect=orders:lost<=0|counts lost: the kinds are 
completed, handled, failed and redeliveries",
+            "endpoint=x error expect=orders:speed<=0|counts speed: the kinds 
are completed, handled, failed, redeliveries, meanTime, maxTime and lost",
             "endpoint=x error expect=orders:<=0|counts nothing",
             "endpoint=x error expect=orders:handled<0|needs >=, <= or = after 
handled",
             "endpoint=x error expect=orders:handled<=x|needs a count that is a 
whole number",
diff --git a/design/chaos.adoc b/design/chaos.adoc
index 7f8d504d67a0..74cefe8d1196 100644
--- a/design/chaos.adoc
+++ b/design/chaos.adoc
@@ -383,6 +383,10 @@ when the context stops first (the report of 
`reportOnShutdown`) or the rule is r
 arrive, so they are not called lost. A tool must treat a pending expectation 
as not yet decided, so a PASS with one
 pending is not final.
 
+Stage 2 (CAMEL-26726) adds expectations on time and loss: 
`orders:meanTime<=2s` and `orders:maxTime<=5s`, with the time
+of an exchange in the route measured by chaos through `TimeSource` (from 
entering the route until done), and
+`orders->shipping:lost<=0`, the messages one route completed and the next did 
not, which count as lost.
+
 === Control
 
 - A `chaos` dev console lists the rules and shows the report, so it is 
`@DevConsole(readOnly = true)`. It does not add,
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
index 75ea342f94ce..f34c418d3c07 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
@@ -432,12 +432,24 @@ public class ListChaos extends ProcessWatchCommand {
         String op = e.getStringOrDefault("op", ">=");
         Object value = e.get("value") != null ? e.get("value") : 
e.get("completed");
         StringBuilder sb = new StringBuilder();
-        sb.append(e.getString("route")).append(':');
-        if (!"completed".equals(kind) || !">=".equals(op)) {
-            sb.append(kind).append(op);
+        String spec = e.getString("spec");
+        if (spec != null) {
+            // as the rule writes it, such as orders:meanTime<=2s or 
orders->shipping:lost<=0 (CAMEL-26726)
+            sb.append(spec);
+        } else {
+            sb.append(e.getString("route")).append(':');
+            if (!"completed".equals(kind) || !">=".equals(op)) {
+                sb.append(kind).append(op);
+            }
+            sb.append(e.get("count"));
+        }
+        sb.append(' ').append(e.getString("outcome")).append(" (");
+        if (("meanTime".equals(kind) || "maxTime".equals(kind)) && value 
instanceof Number n) {
+            // a time in milliseconds
+            sb.append(TimeUtils.printDuration(n.longValue())).append(' 
').append(kind);
+        } else {
+            sb.append(value).append(' ').append(kind);
         }
-        sb.append(e.get("count")).append(' ').append(e.getString("outcome"))
-                .append(" (").append(value).append(' ').append(kind);
         if (e.get("timeLeft") instanceof Number n) {
             sb.append(", 
").append(TimeUtils.printDuration(n.longValue())).append(" left");
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
index b9a0fb741d0a..1b26175a4045 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
@@ -189,8 +189,9 @@ final class ChaosAiPrompt {
             sb.append("It fired at 
").append(rule.firedAt.get("poll")).append(" polls of a consumer (no 
exchange).\n");
         }
         if (rule.expectRoute != null) {
-            sb.append("Expectation: 
").append(rule.expectRoute).append(':').append(rule.expectKind).append(rule.expectOp)
-                    .append(rule.expectCount).append(", 
").append(rule.expectOutcome != null ? rule.expectOutcome : "pending")
+            sb.append("Expectation: ").append(rule.expectSpec != null
+                    ? rule.expectSpec : rule.expectRoute + ":" + 
rule.expectKind + rule.expectOp + rule.expectCount)
+                    .append(", ").append(rule.expectOutcome != null ? 
rule.expectOutcome : "pending")
                     .append(" (").append(rule.expectValue).append(' 
').append(rule.expectKind).append(").\n");
         }
         if (rule.lostAtDlq > 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
index 997dc728130b..bfd42f763839 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
@@ -82,6 +82,8 @@ final class ChaosInfo {
         long expectValue;
         String expectOutcome;
         long expectTimeLeft = -1;
+        // the expectation as the rule writes it, such as orders:meanTime<=2s 
(null before CAMEL-26726)
+        String expectSpec;
         // what a stall or lifecycle rule did to its route, or null
         String lifecycleAction;
         String lifecycleRoute;
@@ -373,6 +375,7 @@ final class ChaosInfo {
             r.expectValue = num(e.get("value") != null ? e.get("value") : 
e.get("completed"), 0);
             r.expectOutcome = e.getString("outcome");
             r.expectTimeLeft = num(e.get("timeLeft"), -1);
+            r.expectSpec = e.getString("spec");
         }
         if (jo.get("lifecycle") instanceof JsonObject l) {
             r.lifecycleAction = l.getString("action");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
index 1949295904ee..8d9da595d920 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
@@ -468,8 +468,9 @@ class ChaosTab extends AbstractTableTab {
         }
         addImpact(lines, info, chaos, r);
         if (r.expectRoute != null) {
-            String spec = "completed".equals(r.expectKind) && 
">=".equals(r.expectOp)
-                    ? r.expectRoute + ":" + r.expectCount
+            String spec = r.expectSpec != null ? r.expectSpec
+                    : "completed".equals(r.expectKind) && 
">=".equals(r.expectOp)
+                            ? r.expectRoute + ":" + r.expectCount
                     : r.expectRoute + ":" + r.expectKind + r.expectOp + 
r.expectCount;
             Style outcome = switch (r.expectOutcome != null ? r.expectOutcome 
: "") {
                 case "met" -> Theme.success().bold();
@@ -481,7 +482,10 @@ class ChaosTab extends AbstractTableTab {
             expect.add(Span.styled(" Expect    ", label));
             expect.add(Span.raw(spec + "  "));
             expect.add(Span.styled(r.expectOutcome != null ? 
r.expectOutcome.toUpperCase(Locale.ENGLISH) : "-", outcome));
-            expect.add(Span.raw("  " + r.expectValue + " " + r.expectKind));
+            // a time (meanTime, maxTime) is in milliseconds
+            boolean time = "meanTime".equals(r.expectKind) || 
"maxTime".equals(r.expectKind);
+            expect.add(Span.raw("  " + (time ? shortTime(r.expectValue) : 
String.valueOf(r.expectValue)) + " "
+                                + r.expectKind));
             if (r.expectTimeLeft >= 0 && "pending".equals(r.expectOutcome)) {
                 expect.add(Span.styled("  " + shortTime(r.expectTimeLeft) + " 
left", label));
             }

Reply via email to