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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 112995b37383 CAMEL-26774: camel-chaos - experiments in the journal of 
an exploration: a hypothesis, its rules and expectations, and a verdict (#29480)
112995b37383 is described below

commit 112995b37383081db2a295a5a6b0c0ca26bfee35
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 16:14:49 2026 +0100

    CAMEL-26774: camel-chaos - experiments in the journal of an exploration: a 
hypothesis, its rules and expectations, and a verdict (#29480)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       |  44 ++++
 components/camel-chaos/src/main/docs/chaos.adoc    |  44 ++++
 .../camel/component/chaos/ChaosExperiment.java     | 242 +++++++++++++++++++++
 .../camel/component/chaos/ChaosExploration.java    |  51 ++++-
 .../apache/camel/component/chaos/ChaosManager.java | 117 +++++++++-
 .../camel/component/chaos/ChaosExperimentTest.java | 186 ++++++++++++++++
 .../apache/camel/cli/connector/ChaosAction.java    |  19 +-
 .../LocalCliConnectorChaosActionTest.java          |  33 +++
 .../dsl/jbang/core/commands/process/ListChaos.java |  61 +++++-
 .../jbang/core/commands/process/ListChaosTest.java |  25 ++-
 10 files changed, 810 insertions(+), 12 deletions(-)

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 746bab5ac884..00bdae584b34 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
@@ -376,6 +376,50 @@ $ camel get chaos --report
     baseline (before the first rule): orders 30 completed, 0 failed
 ----
 
+==== Experiments
+
+The agent tries one thing at a time as an experiment: a hypothesis, such as 
"an order never fails when payment answers
+503", and the rules that test it. At least one of the rules has an expectation 
(`expect=`), which decides the
+hypothesis. The rules of an experiment are added together, all or none, and 
the counts per route are read when it
+begins, so its end says what changed while it ran. One experiment is open at a 
time, and only while an exploration
+runs.
+
+When it ends, camel-chaos reads what came of it and then removes its rules: 
per route the difference of the counts, the
+faults of each rule, the state of each expectation, and a verdict:
+
+* `FAIL`: an expectation is unmet.
+* `PASS`: all expectations are met.
+* `PENDING`: it is still open and an expectation is not decided yet.
+* `INCONCLUSIVE`: it ended before an expectation was decided, so the messages 
may still have come.
+
+The experiment is ended with the exploration when that finishes, or when its 
time is up, with what came of it so far.
+
+The `chaos` action of the CLI connector begins one (`{"action": "chaos", 
"command": "experiment-begin", "hypothesis":
+"an order never fails", "rule": "endpoint=http:payment* error status=503 
until=1m expect=orders:failed<=0
+within=70s"}`), with several rules separated by `;`, and ends it 
(`experiment-end`, with an optional `note` of what the
+agent makes of it). In Java, these are 
`ChaosManager.beginExperiment(hypothesis, rules)` and `endExperiment(note)`.
+
+The journal has them as `experiments`, each with:
+
+* `number`, `hypothesis`, the `rules` (their ids) and `ruleTexts`
+* `open`, `startedAt` and `elapsed`
+* the `verdict` and the `expectations`: per rule its `expect`, `state` (`met`, 
`unmet` or `pending`), the `actual` value,
+and the `value` in words
+* the `faults` per rule
+* the `routes` that did something while it ran, with the same counts as the 
baseline
+* once ended, `endedAt`, `endedBy` (`ended`, `finished` or `deadline`) and the 
`note`
+
+[source,text]
+----
+$ camel get chaos --report
+...
+    experiments:
+      1 FAIL  an order never fails (r1, r2, ended after 1m4s)
+        expect orders:failed<=0: unmet, 9 failed
+        routes: orders 41 completed, 9 failed
+        note: orders has no error handler
+----
+
 == Scenarios
 
 A scenario is the rules of an experiment in a file of the project, so it can 
be run again, shared and kept with the
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 746bab5ac884..00bdae584b34 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -376,6 +376,50 @@ $ camel get chaos --report
     baseline (before the first rule): orders 30 completed, 0 failed
 ----
 
+==== Experiments
+
+The agent tries one thing at a time as an experiment: a hypothesis, such as 
"an order never fails when payment answers
+503", and the rules that test it. At least one of the rules has an expectation 
(`expect=`), which decides the
+hypothesis. The rules of an experiment are added together, all or none, and 
the counts per route are read when it
+begins, so its end says what changed while it ran. One experiment is open at a 
time, and only while an exploration
+runs.
+
+When it ends, camel-chaos reads what came of it and then removes its rules: 
per route the difference of the counts, the
+faults of each rule, the state of each expectation, and a verdict:
+
+* `FAIL`: an expectation is unmet.
+* `PASS`: all expectations are met.
+* `PENDING`: it is still open and an expectation is not decided yet.
+* `INCONCLUSIVE`: it ended before an expectation was decided, so the messages 
may still have come.
+
+The experiment is ended with the exploration when that finishes, or when its 
time is up, with what came of it so far.
+
+The `chaos` action of the CLI connector begins one (`{"action": "chaos", 
"command": "experiment-begin", "hypothesis":
+"an order never fails", "rule": "endpoint=http:payment* error status=503 
until=1m expect=orders:failed<=0
+within=70s"}`), with several rules separated by `;`, and ends it 
(`experiment-end`, with an optional `note` of what the
+agent makes of it). In Java, these are 
`ChaosManager.beginExperiment(hypothesis, rules)` and `endExperiment(note)`.
+
+The journal has them as `experiments`, each with:
+
+* `number`, `hypothesis`, the `rules` (their ids) and `ruleTexts`
+* `open`, `startedAt` and `elapsed`
+* the `verdict` and the `expectations`: per rule its `expect`, `state` (`met`, 
`unmet` or `pending`), the `actual` value,
+and the `value` in words
+* the `faults` per rule
+* the `routes` that did something while it ran, with the same counts as the 
baseline
+* once ended, `endedAt`, `endedBy` (`ended`, `finished` or `deadline`) and the 
`note`
+
+[source,text]
+----
+$ camel get chaos --report
+...
+    experiments:
+      1 FAIL  an order never fails (r1, r2, ended after 1m4s)
+        expect orders:failed<=0: unmet, 9 failed
+        routes: orders 41 completed, 9 failed
+        note: orders has no error handler
+----
+
 == Scenarios
 
 A scenario is the rules of an experiment in a file of the project, so it can 
be run again, shared and kept with the
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
new file mode 100644
index 000000000000..fe2a5bd5c87b
--- /dev/null
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
@@ -0,0 +1,242 @@
+/*
+ * 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 java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.function.Function;
+
+import org.apache.camel.component.chaos.ChaosRule.Expectation;
+import org.apache.camel.component.chaos.ChaosRule.Expectation.Outcome;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * An experiment of an exploration (CAMEL-26774): a hypothesis, the rules that 
test it with their expectations, and what
+ * came of it. It begins with its rules added together and the counts per 
route read, and ends with its rules removed:
+ * the difference of the counts, the faults of each rule and the states of the 
expectations are kept in the journal,
+ * with a verdict.
+ * <p/>
+ * The verdict is {@code FAIL} when an expectation is unmet and {@code PASS} 
when all are met. While it runs and an
+ * expectation is not decided it is {@code PENDING}, and an experiment that 
ended before an expectation was decided is
+ * {@code INCONCLUSIVE}: the messages may still have come. The manager guards 
it with its lock.
+ */
+final class ChaosExperiment {
+
+    static final String PASS = "PASS";
+    static final String FAIL = "FAIL";
+    static final String PENDING = "PENDING";
+    static final String INCONCLUSIVE = "INCONCLUSIVE";
+
+    /** How an experiment ended. */
+    enum End {
+        // endExperiment
+        ENDED("ended"),
+        // the exploration was finished with the experiment open
+        FINISHED("finished"),
+        // the time of the exploration was up
+        DEADLINE("deadline");
+
+        final String key;
+
+        End(String key) {
+            this.key = key;
+        }
+    }
+
+    final int number;
+    final String hypothesis;
+    final List<String> rules;
+    final long startedAt;
+    // the counts per route when it began: completed, failed, kept by the 
consumer, handled, redeliveries
+    private final Map<String, long[]> before = new LinkedHashMap<>();
+    // the report was reset while it ran: the counts of the routes started 
again from zero
+    private boolean countsReset;
+    // what came of it, once it ended
+    private JsonObject result;
+    private String resultText;
+
+    ChaosExperiment(int number, String hypothesis, List<String> rules, 
Map<String, ChaosManager.RouteCounts> counts,
+                    long startedAt) {
+        this.number = number;
+        this.hypothesis = hypothesis != null && !hypothesis.isBlank() ? 
hypothesis.strip() : null;
+        this.rules = List.copyOf(rules);
+        this.startedAt = startedAt;
+        counts.forEach((route, c) -> before.put(route, read(c)));
+    }
+
+    boolean isOpen() {
+        return result == null;
+    }
+
+    /** The counts of the routes were reset (a scenario was loaded): they 
count from zero now. */
+    void reportReset() {
+        before.clear();
+        countsReset = true;
+    }
+
+    /**
+     * Ends it: what came of it is read now, before its rules are removed (a 
removed rule cancels its expectation if not
+     * decided).
+     */
+    void end(
+            Function<String, ChaosRule> rule, Map<String, 
ChaosManager.RouteCounts> counts, long now, End by,
+            String note) {
+        JsonObject jo = read(rule, counts, now, true);
+        jo.put("endedAt", now);
+        jo.put("endedBy", by.key);
+        if (note != null && !note.isBlank()) {
+            jo.put("note", note.strip());
+        }
+        resultText = text(jo);
+        result = jo;
+    }
+
+    /** What came of it as it ended, or null while it runs. */
+    JsonObject result() {
+        return result;
+    }
+
+    /** What came of it: as it ended, or as it is now while it runs. */
+    JsonObject toJson(Function<String, ChaosRule> rule, Map<String, 
ChaosManager.RouteCounts> counts, long now) {
+        return result != null ? result : read(rule, counts, now, false);
+    }
+
+    String text(Function<String, ChaosRule> rule, Map<String, 
ChaosManager.RouteCounts> counts, long now) {
+        return result != null ? resultText : text(read(rule, counts, now, 
false));
+    }
+
+    private JsonObject read(
+            Function<String, ChaosRule> rule, Map<String, 
ChaosManager.RouteCounts> counts, long now, boolean ended) {
+        JsonObject jo = new JsonObject();
+        jo.put("number", number);
+        if (hypothesis != null) {
+            jo.put("hypothesis", hypothesis);
+        }
+        jo.put("open", !ended);
+        jo.put("startedAt", startedAt);
+        jo.put("elapsed", now - startedAt);
+        jo.put("rules", new JsonArray(rules));
+        boolean unmet = false;
+        boolean undecided = false;
+        JsonArray texts = new JsonArray();
+        JsonObject faults = new JsonObject();
+        JsonArray expectations = new JsonArray();
+        for (String id : rules) {
+            ChaosRule r = rule.apply(id);
+            if (r == null) {
+                continue;
+            }
+            texts.add(r.text);
+            long fired = 0;
+            for (ChaosRule.Hook hook : ChaosRule.Hook.values()) {
+                fired += r.firedAt(hook);
+            }
+            faults.put(id, fired);
+            Expectation e = r.expectation;
+            if (e != null) {
+                Expectation.Reading reading = e.read(now);
+                // an expectation the experiment did not wait for is 
undecided, not cancelled
+                Outcome outcome = reading.outcome() == Outcome.CANCELLED ? 
Outcome.PENDING : reading.outcome();
+                unmet |= outcome == Outcome.UNMET;
+                undecided |= outcome == Outcome.PENDING;
+                JsonObject x = new JsonObject();
+                x.put("rule", id);
+                x.put("expect", e.spec());
+                x.put("state", outcome.key);
+                x.put("actual", reading.value());
+                x.put("value", e.valueText(reading));
+                expectations.add(x);
+            }
+        }
+        jo.put("ruleTexts", texts);
+        jo.put("faults", faults);
+        jo.put("expectations", expectations);
+        jo.put("verdict", unmet ? FAIL : !undecided ? PASS : ended ? 
INCONCLUSIVE : PENDING);
+        JsonArray routes = new JsonArray();
+        counts.forEach((route, c) -> {
+            long[] now0 = read(c);
+            long[] then = before.getOrDefault(route, new long[5]);
+            long[] delta = new long[5];
+            boolean any = false;
+            for (int i = 0; i < delta.length; i++) {
+                delta[i] = Math.max(0, now0[i] - then[i]);
+                any |= delta[i] > 0;
+            }
+            if (any) {
+                JsonObject r = new JsonObject();
+                r.put("route", route);
+                r.put("completed", delta[0]);
+                r.put("failed", delta[1]);
+                r.put("failedKeptByConsumer", delta[2]);
+                r.put("handled", delta[3]);
+                r.put("redeliveries", delta[4]);
+                routes.add(r);
+            }
+        });
+        jo.put("routes", routes);
+        if (countsReset) {
+            jo.put("countsReset", true);
+        }
+        return jo;
+    }
+
+    private static long[] read(ChaosManager.RouteCounts c) {
+        return new long[] {
+                c.completed.sum(), c.failed.sum(), c.failedKept.sum(), 
c.handled.sum(), c.redeliveries.sum() };
+    }
+
+    /** One line for the experiment and one per expectation, as the report 
prints them. */
+    private static String text(JsonObject jo) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("\n      ").append(jo.getInteger("number")).append(' 
').append(jo.getString("verdict"));
+        String hypothesis = jo.getString("hypothesis");
+        if (hypothesis != null) {
+            sb.append("  ").append(hypothesis);
+        }
+        sb.append(" (").append(String.join(", ", jo.<List<String>> 
getCollection("rules"))).append(", ");
+        if (jo.getBooleanOrDefault("open", false)) {
+            sb.append("running for 
").append(ChaosRuleParser.printTime(jo.getLong("elapsed")));
+        } else {
+            String by = jo.getString("endedBy");
+            sb.append(End.DEADLINE.key.equals(by)
+                    ? "ended at the deadline" : End.FINISHED.key.equals(by) ? 
"ended with the exploration" : "ended")
+                    .append(" after 
").append(ChaosRuleParser.printTime(jo.getLong("elapsed")));
+        }
+        sb.append(')');
+        for (Object o : jo.getCollection("expectations")) {
+            JsonObject x = (JsonObject) o;
+            sb.append("\n        expect 
").append(x.getString("expect")).append(": ").append(x.getString("state"))
+                    .append(", ").append(x.getString("value"));
+        }
+        StringJoiner sj = new StringJoiner(", ");
+        for (Object o : jo.getCollection("routes")) {
+            JsonObject r = (JsonObject) o;
+            sj.add(r.getString("route") + " " + r.getLong("completed") + " 
completed, " + r.getLong("failed") + " failed");
+        }
+        if (sj.length() > 0) {
+            sb.append("\n        routes: ").append(sj);
+        }
+        String note = jo.getString("note");
+        if (note != null) {
+            sb.append("\n        note: ").append(note);
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExploration.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExploration.java
index 89a9e3560e9f..511bc4c8d5f7 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExploration.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExploration.java
@@ -21,6 +21,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.StringJoiner;
+import java.util.function.Function;
 
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -32,7 +33,8 @@ import org.apache.camel.util.json.JsonObject;
  * the deadline the rules are removed and the exploration is finished, 
whatever the agent does.
  * <p/>
  * It has the baseline, the counts per route when it started, so the failures 
that were there before are not blamed on
- * chaos, and the rules added while it ran. The manager guards it with its 
lock.
+ * chaos, the rules added while it ran, and its experiments (CAMEL-26774): a 
hypothesis and the rules that test it, one
+ * at a time. The manager guards it with its lock.
  */
 final class ChaosExploration {
 
@@ -66,6 +68,8 @@ final class ChaosExploration {
     private boolean baselineReset;
     // the ids of the rules added while it ran (a changed rule keeps its id)
     private final List<String> rules = new ArrayList<>();
+    // the experiments, the open one last
+    private final List<ChaosExperiment> experiments = new ArrayList<>();
     private long finishedAt;
     private Finish finishedBy;
     private int removed;
@@ -97,6 +101,28 @@ final class ChaosExploration {
     void reportReset() {
         baseline.clear();
         baselineReset = true;
+        ChaosExperiment open = openExperiment();
+        if (open != null) {
+            open.reportReset();
+        }
+    }
+
+    /** The experiment that is open, or null. */
+    ChaosExperiment openExperiment() {
+        ChaosExperiment last = experiments.isEmpty() ? null : 
experiments.get(experiments.size() - 1);
+        return last != null && last.isOpen() ? last : null;
+    }
+
+    /** Begins an experiment with the rules just added for it (and taken in 
the rules tried). */
+    ChaosExperiment beginExperiment(
+            String hypothesis, List<String> ruleIds, Map<String, 
ChaosManager.RouteCounts> counts, long now) {
+        ChaosExperiment experiment = new ChaosExperiment(experiments.size() + 
1, hypothesis, ruleIds, counts, now);
+        experiments.add(experiment);
+        return experiment;
+    }
+
+    int experimentCount() {
+        return experiments.size();
     }
 
     void finished(long now, Finish by, int removedRules) {
@@ -106,6 +132,14 @@ final class ChaosExploration {
     }
 
     JsonObject toJson(long now) {
+        return toJson(now, id -> null, Map.of());
+    }
+
+    /**
+     * As JSON, with the experiments: the rules of an open one are looked up 
by their id, and the counts of the routes
+     * now give what it did so far.
+     */
+    JsonObject toJson(long now, Function<String, ChaosRule> rule, Map<String, 
ChaosManager.RouteCounts> counts) {
         JsonObject jo = new JsonObject();
         jo.put("running", isRunning());
         jo.put("startedAt", startedAt);
@@ -141,6 +175,11 @@ final class ChaosExploration {
             jo.put("finishedBy", finishedBy.key);
             jo.put("removed", removed);
         }
+        JsonArray tried = new JsonArray();
+        for (ChaosExperiment experiment : experiments) {
+            tried.add(experiment.toJson(rule, counts, now));
+        }
+        jo.put("experiments", tried);
         return jo;
     }
 
@@ -149,8 +188,18 @@ final class ChaosExploration {
     }
 
     String text(long now) {
+        return text(now, id -> null, Map.of());
+    }
+
+    String text(long now, Function<String, ChaosRule> rule, Map<String, 
ChaosManager.RouteCounts> counts) {
         StringBuilder sb = new StringBuilder();
         appendText(sb, now);
+        if (!experiments.isEmpty()) {
+            sb.append("\n    experiments:");
+            for (ChaosExperiment experiment : experiments) {
+                sb.append(experiment.text(rule, counts, now));
+            }
+        }
         return sb.toString();
     }
 
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 5c88b3888480..404e77c66d24 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
@@ -23,6 +23,7 @@ import java.util.Collection;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -655,7 +656,8 @@ public final class ChaosManager {
                 lostAtDlq, recentFaults.latestFirst(), 
ChaosTargets.reach(camelContext), tried);
         if (explored != null) {
             synchronized (lock) {
-                report.exploration(explored.toJson(now), explored.text(now));
+                Map<String, RouteCounts> live = new TreeMap<>(routes);
+                report.exploration(explored.toJson(now, this::findRule, live), 
explored.text(now, this::findRule, live));
             }
         }
         return report;
@@ -715,8 +717,109 @@ public final class ChaosManager {
                 throw new IllegalStateException("No exploration is running");
             }
             finish(now, ChaosExploration.Finish.FINISHED);
-            return exploration.toJson(now);
+            return exploration.toJson(now, this::findRule, new 
TreeMap<>(routes));
+        }
+    }
+
+    /**
+     * Begins an experiment of the running exploration (CAMEL-26774): a 
hypothesis and the rules that test it, added
+     * together (all or none), with at least one expectation ({@code expect=}) 
to decide it. The counts per route are
+     * read now, so its end says what changed while it ran. One experiment is 
open at a time.
+     *
+     * @param  hypothesis               what the experiment tests, in a few 
words, such as "a failed refund is read
+     *                                  again"
+     * @param  rules                    the rules in their one-line text form, 
separated by {@code ;}
+     * @return                          the experiment, as in the report
+     * @throws IllegalArgumentException when a rule is not valid, or no rule 
has an expectation
+     * @throws IllegalStateException    when no exploration is running, or an 
experiment is open
+     */
+    public Map<String, Object> beginExperiment(String hypothesis, String 
rules) {
+        List<String> texts = ChaosRuleParser.splitRules(rules);
+        if (texts.isEmpty()) {
+            throw new IllegalArgumentException("The chaos rule is empty");
+        }
+        List<ChaosRule.Builder> definitions = new ArrayList<>();
+        for (String text : texts) {
+            definitions.add(parse(text));
+        }
+        if (definitions.stream().allMatch(d -> d.expectRoute == null)) {
+            throw new IllegalArgumentException(
+                    "An experiment needs an expectation (expect=) on one of 
its rules to decide its hypothesis, such as"
+                                               + " expect=orders:failed<=0 
within=1m");
+        }
+        synchronized (lock) {
+            long now = TimeSource.currentTimeMillis();
+            checkDeadline(now);
+            if (exploration == null || !exploration.isRunning()) {
+                throw new IllegalStateException("No exploration is running: an 
experiment is part of one");
+            }
+            ChaosExperiment open = exploration.openExperiment();
+            if (open != null) {
+                throw new IllegalStateException("Experiment " + open.number + 
" is open: end it first");
+            }
+            Map<String, RouteCounts> counts = new TreeMap<>(routes);
+            List<String> ids = add(definitions);
+            ChaosExperiment experiment = 
exploration.beginExperiment(hypothesis, ids, counts, now);
+            LOG.info("Chaos experiment {} began with rules {}{}", 
experiment.number, ids,
+                    experiment.hypothesis != null ? ": " + 
experiment.hypothesis : "");
+            return experiment.toJson(this::findRule, counts, now);
+        }
+    }
+
+    /**
+     * Ends the open experiment: what came of it is read (the difference of 
the counts per route, the faults of each
+     * rule, the states of the expectations and the verdict), its rules are 
removed, and it stays in the journal.
+     *
+     * @param  note                  what the agent makes of it (optional)
+     * @return                       the experiment, as in the report
+     * @throws IllegalStateException when no experiment is open
+     */
+    public Map<String, Object> endExperiment(String note) {
+        synchronized (lock) {
+            long now = TimeSource.currentTimeMillis();
+            // an experiment open at the deadline was ended with the 
exploration
+            checkDeadline(now);
+            ChaosExperiment open = exploration != null ? 
exploration.openExperiment() : null;
+            if (open == null) {
+                throw new IllegalStateException("No experiment is open");
+            }
+            endExperiment(open, now, ChaosExperiment.End.ENDED, note);
+            return open.result();
+        }
+    }
+
+    // reads what came of the experiment, and then removes its rules; with the 
lock
+    private int endExperiment(ChaosExperiment experiment, long now, 
ChaosExperiment.End by, String note) {
+        experiment.end(this::findRule, new TreeMap<>(routes), now, by, note);
+        int count = 0;
+        for (String id : experiment.rules) {
+            int index = indexOf(id);
+            if (index >= 0) {
+                ChaosRule removed = rules.remove(index);
+                removed.removed(State.REMOVED, now);
+                remember(removed);
+                count++;
+            }
+        }
+        update();
+        LOG.info("Chaos experiment {} ended: {}", experiment.number,
+                experiment.result().getString("verdict"));
+        return count;
+    }
+
+    // the rule with the id: the one in place, else the last one removed (a 
changed rule keeps its id); with the lock
+    private ChaosRule findRule(String id) {
+        int index = indexOf(id);
+        if (index >= 0) {
+            return rules.get(index);
+        }
+        for (Iterator<ChaosRule> it = history.descendingIterator(); 
it.hasNext();) {
+            ChaosRule rule = it.next();
+            if (rule.id.equals(id)) {
+                return rule;
+            }
         }
+        return null;
     }
 
     // finishes the running exploration whose time is up; with the lock
@@ -727,7 +830,15 @@ public final class ChaosManager {
     }
 
     private void finish(long now, ChaosExploration.Finish by) {
-        int removedRules = clearRules();
+        ChaosExperiment open = exploration.openExperiment();
+        int removedRules = 0;
+        if (open != null) {
+            // read before the rules are removed
+            removedRules = endExperiment(open, now,
+                    by == ChaosExploration.Finish.DEADLINE ? 
ChaosExperiment.End.DEADLINE : ChaosExperiment.End.FINISHED,
+                    null);
+        }
+        removedRules += clearRules();
         exploration.finished(now, by, removedRules);
         // no rule is left and the exploration is done: the counting stops
         update();
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
new file mode 100644
index 000000000000..7b58a415864f
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * The experiments of an exploration (CAMEL-26774): a hypothesis and the rules 
that test it, and what came of it in the
+ * journal: the counts per route while it ran, the faults of each rule, the 
expectations and the verdict.
+ */
+class ChaosExperimentTest 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:shipped");
+            }
+        };
+    }
+
+    private void send(int count) {
+        for (int i = 0; i < count; i++) {
+            // a failed exchange is counted by chaos, and not thrown here
+            template.send("direct:orders", e -> 
e.getMessage().setBody("order"));
+        }
+    }
+
+    private JsonObject experiment(int number) {
+        JsonObject exploration = (JsonObject) 
chaos().report().toJson().get("exploration");
+        return (JsonObject) list(exploration, "experiments").get(number - 1);
+    }
+
+    private static List<Object> list(JsonObject jo, String key) {
+        return jo.getCollection(key);
+    }
+
+    private static JsonObject first(JsonObject jo, String key) {
+        return (JsonObject) list(jo, key).get(0);
+    }
+
+    @Test
+    void aHypothesisThatFails() {
+        chaos().startExploration("10m", null);
+        // before the experiment: not counted in it
+        send(1);
+
+        Map<String, Object> begun = chaos().beginExperiment("an order never 
fails",
+                "endpoint=mock:shipped error rate=every:2 
expect=orders:failed<=0 within=1m");
+        assertThat(begun).containsEntry("number", 1).containsEntry("open", 
true).containsEntry("verdict", "PENDING");
+        send(4);
+        clock.advance(5_000);
+
+        // the expectation is unmet as soon as an order fails, before the 
experiment ends
+        assertThat(experiment(1).getString("verdict")).isEqualTo("FAIL");
+        Map<String, Object> ended = chaos().endExperiment("orders has no error 
handler");
+
+        assertThat(ended).containsEntry("open", 
false).containsEntry("verdict", "FAIL").containsEntry("endedBy", "ended")
+                .containsEntry("elapsed", 5_000L).containsEntry("note", 
"orders has no error handler");
+        JsonObject result = experiment(1);
+        assertThat(list(result, "ruleTexts")).containsExactly(
+                "endpoint=mock:shipped error rate=every:2 
expect=orders:failed<=0 within=1m");
+        assertThat(((JsonObject) 
result.get("faults")).getLong("r1")).isEqualTo(2);
+        JsonObject expectation = first(result, "expectations");
+        
assertThat(expectation.getString("expect")).isEqualTo("orders:failed<=0");
+        assertThat(expectation.getString("state")).isEqualTo("unmet");
+        assertThat(expectation.getLong("actual")).isEqualTo(2);
+        JsonObject orders = first(result, "routes");
+        assertThat(orders.getString("route")).isEqualTo("orders");
+        assertThat(orders.getLong("completed")).isEqualTo(2);
+        assertThat(orders.getLong("failed")).isEqualTo(2);
+        // its rules are removed, and it stays in the journal
+        assertThat(chaos().rules()).isEmpty();
+        assertThat(chaos().report().toText()).contains(
+                "experiments:", "1 FAIL  an order never fails (r1, ended after 
5s)",
+                "expect orders:failed<=0: unmet, 2 failed", "routes: orders 2 
completed, 2 failed",
+                "note: orders has no error handler");
+    }
+
+    @Test
+    void aHypothesisThatHoldsAndOneNotDecided() {
+        chaos().startExploration("10m", null);
+        chaos().beginExperiment("orders keep up with a slow shipping",
+                "endpoint=mock:shipped delay duration=10ms expect=orders:2 
within=1m");
+        send(1);
+        assertThat(experiment(1).getString("verdict")).isEqualTo("PENDING");
+        send(1);
+        assertThat(experiment(1).getString("verdict")).isEqualTo("PASS");
+        assertThat(chaos().endExperiment(null)).containsEntry("verdict", 
"PASS").doesNotContainKey("note");
+
+        // ended before its expectation was decided: the messages may still 
have come
+        chaos().beginExperiment(null, "endpoint=mock:shipped delay 
duration=10ms expect=orders:5 within=1m");
+        send(1);
+        Map<String, Object> ended = chaos().endExperiment(null);
+        assertThat(ended).containsEntry("number", 2).containsEntry("verdict", 
"INCONCLUSIVE");
+        assertThat(first(experiment(2), 
"expectations").getString("state")).isEqualTo("pending");
+        assertThat(list((JsonObject) 
chaos().report().toJson().get("exploration"), "rules"))
+                .containsExactly("r1", "r2");
+    }
+
+    @Test
+    void oneAtATimeWithAnExpectationAndInAnExploration() {
+        String rule = "endpoint=mock:shipped error expect=orders:failed<=0 
within=1m";
+        assertThatThrownBy(() -> chaos().beginExperiment("h", 
rule)).isInstanceOf(IllegalStateException.class)
+                .hasMessageContaining("No exploration is running");
+
+        chaos().startExploration("10m", null);
+        assertThatThrownBy(() -> chaos().beginExperiment("h", 
"endpoint=mock:shipped error"))
+                
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("needs an 
expectation (expect=)");
+        assertThatThrownBy(() -> chaos().beginExperiment("h", rule + "; 
endpoint=mock:shipped nonsense"))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThat(chaos().rules()).isEmpty();
+        assertThatThrownBy(() -> 
chaos().endExperiment(null)).isInstanceOf(IllegalStateException.class)
+                .hasMessage("No experiment is open");
+
+        // a pair of faults: two rules, one of them with the expectation
+        Map<String, Object> begun = chaos().beginExperiment("h", rule + "; 
endpoint=mock:shipped delay duration=10ms");
+        assertThat(list(new JsonObject(begun), "rules")).containsExactly("r1", 
"r2");
+        assertThatThrownBy(() -> chaos().beginExperiment("h", 
rule)).isInstanceOf(IllegalStateException.class)
+                .hasMessage("Experiment 1 is open: end it first");
+        // a rule added by hand is not part of the experiment, and stays when 
it ends
+        String other = chaos().addRule("endpoint=mock:shipped delay 
duration=1ms");
+        chaos().endExperiment(null);
+        assertThat(chaos().rules()).extracting(r -> 
r.id).containsExactly(other);
+    }
+
+    @Test
+    void anExperimentOpenAtTheDeadlineEndsWithTheExploration() {
+        chaos().startExploration("1m", null);
+        chaos().beginExperiment("h", "endpoint=mock:shipped error 
expect=orders:failed<=0 within=5m");
+        send(1);
+        clock.advance(60_000);
+
+        JsonObject experiment = experiment(1);
+        assertThat(experiment.getString("endedBy")).isEqualTo("deadline");
+        assertThat(experiment.getString("verdict")).isEqualTo("FAIL");
+        JsonObject exploration = (JsonObject) 
chaos().report().toJson().get("exploration");
+        assertThat(exploration.getLong("removed")).isEqualTo(1);
+        assertThat(chaos().report().toText()).contains("1 FAIL  h (r1, ended 
at the deadline after 1m0s)");
+
+        // finished by hand, with an experiment open
+        chaos().startExploration("1m", null);
+        chaos().beginExperiment("h", "endpoint=mock:shipped delay duration=1ms 
expect=orders:1 within=5m");
+        chaos().finishExploration();
+        assertThat(experiment(1).getString("endedBy")).isEqualTo("finished");
+        
assertThat(experiment(1).getString("verdict")).isEqualTo("INCONCLUSIVE");
+        assertThat(chaos().rules()).isEmpty();
+    }
+}
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
index 554ac4415f43..e313d421dca3 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
@@ -151,9 +151,26 @@ final class ChaosAction {
                 @SuppressWarnings("unchecked")
                 Map<String, Object> finished = (Map<String, Object>) 
call(type, chaos, "finishExploration");
                 answer.putAll(finished);
+            } else if ("experiment-begin".equals(command)) {
+                if (isBlank(rule)) {
+                    output.error("The rules of the experiment are required");
+                    return;
+                }
+                // an experiment of the exploration (CAMEL-26774): its rules 
are added together, with an expectation
+                @SuppressWarnings("unchecked")
+                Map<String, Object> begun = (Map<String, Object>) call(type, 
chaos, "beginExperiment",
+                        root.getString("hypothesis"), rule);
+                answer.putAll(begun);
+            } else if ("experiment-end".equals(command)) {
+                // what came of it, and its rules are removed
+                @SuppressWarnings("unchecked")
+                Map<String, Object> ended = (Map<String, Object>) call(type, 
chaos, "endExperiment",
+                        root.getString("note"));
+                answer.putAll(ended);
             } else {
                 output.error("Unknown chaos command: " + command
-                             + " (add, check, change, remove, clear, load, 
suggest, explore-start or explore-finish)");
+                             + " (add, check, change, remove, clear, load, 
suggest, explore-start, explore-finish,"
+                             + " experiment-begin or experiment-end)");
                 return;
             }
         } catch (IllegalArgumentException | IllegalStateException e) {
diff --git 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
index 73b090e315a0..eae9ee24a76d 100644
--- 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
+++ 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.chaos.ChaosManager;
 import org.apache.camel.impl.DefaultCamelContext;
@@ -248,6 +249,38 @@ class LocalCliConnectorChaosActionTest {
         assertThat(chaos("explore-finish", null, null).error()).isEqualTo("No 
exploration is running");
     }
 
+    @Test
+    void anExperimentBeginsAndEnds() throws Exception {
+        start(null, Map.of("camel.chaos.enabled", "true"));
+        assertThat(chaos("explore-start", null, null).error()).isNull();
+        JsonObject action = new JsonObject();
+        action.put("action", "chaos");
+        action.put("command", "experiment-begin");
+        action.put("hypothesis", "start never fails");
+        action.put("rule", "endpoint=mock:target error expect=start:failed<=0 
within=1m");
+
+        CliActionOutcome begun = CliActionOutcome.dispatch(connector, 
action).get(10, TimeUnit.SECONDS);
+        assertThat(begun.error()).isNull();
+        assertThat(begun.result().getInteger("number")).isEqualTo(1);
+        assertThat(begun.result().getString("verdict")).isEqualTo("PENDING");
+        // one at a time: the reason goes to the tool
+        assertThat(CliActionOutcome.dispatch(connector, action).get(10, 
TimeUnit.SECONDS).error())
+                .isEqualTo("Experiment 1 is open: end it first");
+
+        Exchange failed = 
context.createProducerTemplate().send("direct:start", e -> 
e.getMessage().setBody("x"));
+        assertThat(failed.getException()).isNotNull();
+        JsonObject end = new JsonObject();
+        end.put("action", "chaos");
+        end.put("command", "experiment-end");
+        end.put("note", "no error handler");
+        CliActionOutcome ended = CliActionOutcome.dispatch(connector, 
end).get(10, TimeUnit.SECONDS);
+        assertThat(ended.error()).isNull();
+        assertThat(ended.result().getString("verdict")).isEqualTo("FAIL");
+        assertThat(ended.result().getString("note")).isEqualTo("no error 
handler");
+        assertThat(chaos("experiment-end", null, null).error()).isEqualTo("No 
experiment is open");
+        assertThat(chaos("experiment-begin", null, 
null).error()).isEqualTo("The rules of the experiment are required");
+    }
+
     @Test
     void rulesAreAddedTogetherWithWarningsForTheOnesThatMatchNothing() throws 
Exception {
         start(null, Map.of("camel.chaos.enabled", "true"));
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 34c710feb859..f81650fb1806 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
@@ -308,8 +308,8 @@ public class ListChaos extends ProcessWatchCommand {
     }
 
     /**
-     * The exploration running now or the last one (CAMEL-26773): its time, 
the rules tried, and the baseline before its
-     * first rule.
+     * The exploration running now or the last one (CAMEL-26773): its time, 
the rules tried, the baseline before its
+     * first rule, and its experiments (CAMEL-26774).
      */
     private void printExploration(JsonObject report) {
         JsonObject exploration = report.getMap("exploration");
@@ -338,16 +338,65 @@ public class ListChaos extends ProcessWatchCommand {
             printer().println("    baseline: taken when the first rule is 
added");
             return;
         }
+        printer().println("    baseline (before the first rule): " + 
routeCounts(exploration.getCollection("baseline")));
+        List<Object> experiments = exploration.getCollection("experiments");
+        if (experiments != null && !experiments.isEmpty()) {
+            printer().println("    experiments:");
+            for (Object o : experiments) {
+                printExperiment((JsonObject) o);
+            }
+        }
+    }
+
+    /** An experiment: its verdict and hypothesis, its rules, its 
expectations, and what the routes did meanwhile. */
+    private void printExperiment(JsonObject experiment) {
+        StringBuilder sb = new StringBuilder("      
").append(experiment.getIntegerOrDefault("number", 0)).append(' ')
+                .append(experiment.getString("verdict"));
+        String hypothesis = experiment.getString("hypothesis");
+        if (hypothesis != null) {
+            sb.append("  ").append(hypothesis);
+        }
+        List<Object> rules = experiment.getCollection("rules");
+        sb.append(" (").append(rules == null ? "" : 
rules.stream().map(String::valueOf).collect(Collectors.joining(", ")))
+                .append(", ");
+        String elapsed = 
TimeUtils.printDuration(experiment.getLongOrDefault("elapsed", 0));
+        if (experiment.getBooleanOrDefault("open", false)) {
+            sb.append("running for ").append(elapsed);
+        } else {
+            String by = experiment.getString("endedBy");
+            sb.append("deadline".equals(by)
+                    ? "ended at the deadline" : "finished".equals(by) ? "ended 
with the exploration" : "ended")
+                    .append(" after ").append(elapsed);
+        }
+        printer().println(sb.append(')').toString());
+        List<Object> expectations = experiment.getCollection("expectations");
+        if (expectations != null) {
+            for (Object o : expectations) {
+                JsonObject x = (JsonObject) o;
+                printer().println("        expect " + x.getString("expect") + 
": " + x.getString("state") + ", "
+                                  + x.getString("value"));
+            }
+        }
+        List<Object> routes = experiment.getCollection("routes");
+        if (routes != null && !routes.isEmpty()) {
+            printer().println("        routes: " + routeCounts(routes));
+        }
+        String note = experiment.getString("note");
+        if (note != null) {
+            printer().println("        note: " + note);
+        }
+    }
+
+    private static String routeCounts(List<Object> routes) {
         StringJoiner sj = new StringJoiner(", ");
-        List<Object> baseline = exploration.getCollection("baseline");
-        if (baseline != null) {
-            for (Object o : baseline) {
+        if (routes != null) {
+            for (Object o : routes) {
                 JsonObject route = (JsonObject) o;
                 sj.add(route.getString("route") + " " + 
route.getLongOrDefault("completed", 0) + " completed, "
                        + route.getLongOrDefault("failed", 0) + " failed");
             }
         }
-        printer().println("    baseline (before the first rule): " + 
(sj.length() == 0 ? "no exchanges" : sj));
+        return sj.length() == 0 ? "no exchanges" : sj.toString();
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
index 56107e222941..6763a6dfd9f9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
@@ -201,6 +201,26 @@ class ListChaosTest extends ProcessCommandTestSupport {
         orders.put("completed", 30L);
         orders.put("failed", 0L);
         exploration.put("baseline", new JsonArray(List.of(orders)));
+        JsonObject experiment = new JsonObject();
+        experiment.put("number", 1);
+        experiment.put("hypothesis", "an order never fails");
+        experiment.put("open", false);
+        experiment.put("endedBy", "ended");
+        experiment.put("elapsed", 64_000L);
+        experiment.put("rules", new JsonArray(List.of("r1", "r2")));
+        experiment.put("verdict", "FAIL");
+        JsonObject expectation = new JsonObject();
+        expectation.put("expect", "orders:failed<=0");
+        expectation.put("state", "unmet");
+        expectation.put("value", "9 failed");
+        experiment.put("expectations", new JsonArray(List.of(expectation)));
+        JsonObject during = new JsonObject();
+        during.put("route", "orders");
+        during.put("completed", 41L);
+        during.put("failed", 9L);
+        experiment.put("routes", new JsonArray(List.of(during)));
+        experiment.put("note", "orders has no error handler");
+        exploration.put("experiments", new JsonArray(List.of(experiment)));
         ((JsonObject) chaos.get("report")).put("exploration", exploration);
         writeStatus(chaos);
 
@@ -208,7 +228,10 @@ class ListChaosTest extends ProcessCommandTestSupport {
 
         String out = printer.getStdout();
         assertThat(out).contains("exploration: running, 6m12s left of 10m0s, 
focus: orders",
-                "rules tried: r1, r2", "baseline (before the first rule): 
orders 30 completed, 0 failed");
+                "rules tried: r1, r2", "baseline (before the first rule): 
orders 30 completed, 0 failed",
+                "experiments:", "1 FAIL  an order never fails (r1, r2, ended 
after 1m4s)",
+                "expect orders:failed<=0: unmet, 9 failed", "routes: orders 41 
completed, 9 failed",
+                "note: orders has no error handler");
     }
 
     @Test

Reply via email to