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

davsclaus pushed a commit to branch CAMEL-26788-chaos-explore-save
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/CAMEL-26788-chaos-explore-save 
by this push:
     new feabaddee350 CAMEL-26788: camel-jbang - an experiment refuses a feed 
that reaches no route, and chaos-explore keeps the AI to the chaos tools (found 
in a recorded run)
feabaddee350 is described below

commit feabaddee35054ffa985b79b68de8804f17ac0a7
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 17:51:01 2026 +0100

    CAMEL-26788: camel-jbang - an experiment refuses a feed that reaches no 
route, and chaos-explore keeps the AI to the chaos tools (found in a recorded 
run)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/ChaosExplore.java      |  5 ++-
 .../action/CamelChaosExperimentAction.java         |  4 ++
 .../core/commands/ai/ChaosExplorePlaybook.java     |  3 +-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  7 ++++
 .../dsl/jbang/core/common/ChaosExperimentRun.java  | 45 ++++++++++++++++++++++
 .../dsl/jbang/core/commands/ChaosExploreTest.java  |  4 +-
 .../jbang/core/common/ChaosExperimentRunTest.java  | 27 +++++++++++++
 7 files changed, 90 insertions(+), 5 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
index dabb039c7024..fa2cfe646e19 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
@@ -202,8 +202,9 @@ public class ChaosExplore extends CamelCommand {
         ask.showTools = showTools;
         ask.showStats = true;
         ask.verbose = verbose;
-        // the exploration runs with the budget of the user: the AI does not 
start another one
-        ask.hiddenTools = Set.of("chaos_explore_start");
+        // the exploration runs with the budget of the user: the AI does not 
start another one, not even with a camel
+        // command, and the chaos tools are all it needs
+        ask.hiddenTools = Set.of("chaos_explore_start", "cli_exec");
         return ask;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
index e2b2404ec08c..449ec6763a88 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
@@ -145,6 +145,10 @@ public class CamelChaosExperimentAction extends 
CamelChaosAction {
                         feed, absolute(body), headers != null ? 
String.join("\n", headers) : null,
                         repeat, interval)
                 : null;
+        String why = traffic != null ? 
ChaosExperimentRun.checkFeed(RuntimeHelper.readStatus(process.pid()), feed) : 
null;
+        if (why != null) {
+            throw CliException.usage(why.replace("The feed", "--feed"));
+        }
         JsonObject result = ChaosExperimentRun.run(new 
ChaosExperimentRun.Integration() {
             @Override
             public JsonObject chaos(String command, Consumer<JsonObject> 
options) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
index dc0e24ba7f08..23f0adce1d1b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
@@ -91,7 +91,8 @@ public final class ChaosExplorePlaybook {
                 Routes fed over http or from files cannot be fed by an 
experiment yet: try them only when they \
                 feed themselves
                    - a rule at=dlq fails the dead letter channel, so it fires 
only when a message goes there: use \
-                the combined suggestions, which add the fault that sends 
messages there too
+                the combined suggestions, which add the fault that sends 
messages there too, and expect \
+                <route>:failed<=0, as a message the dead letter channel did 
not take fails in its route
                    - wait: longer than the within= of the expectation
                    - note: what you make of the result, in one sentence
                 4. Read the result: the verdict (FAIL, PASS or INCONCLUSIVE), 
the expectations, what the routes \
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 0c9f44d0454f..848bf5a7c9b6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -1005,6 +1005,13 @@ public final class ToolRegistry {
                                     feed, args.get("body"), 
args.get("headers"), repeat,
                                     args.get("interval"))
                             : null;
+                    if (traffic != null) {
+                        // an experiment without messages says nothing: the AI 
gets the routes to feed instead
+                        String why = 
ChaosExperimentRun.checkFeed(ctx.readFullStatus(), feed);
+                        if (why != null) {
+                            throw new ToolExecutionException(why);
+                        }
+                    }
                     return ChaosExperimentRun.run(new 
ChaosExperimentRun.Integration() {
                         @Override
                         public JsonObject chaos(String command, 
Consumer<JsonObject> options) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
index 742c29a7c6dc..ddd2bf47bd77 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.common;
 
 import java.util.List;
+import java.util.StringJoiner;
 import java.util.function.Consumer;
 
 import org.apache.camel.util.TimeUtils;
@@ -132,6 +133,13 @@ public final class ChaosExperimentRun {
     static JsonObject run(
             Integration integration, String hypothesis, String rules, Feed 
feed, long wait, String note, Clock clock)
             throws InterruptedException {
+        if (feed != null) {
+            // a feed that reaches no route makes an experiment without 
messages, whose verdict says nothing
+            String why = checkFeed(integration.status(), feed.endpoint());
+            if (why != null) {
+                throw new IllegalArgumentException(why);
+            }
+        }
         JsonObject begun = integration.chaos("experiment-begin", root -> {
             if (hypothesis != null && !hypothesis.isBlank()) {
                 root.put("hypothesis", hypothesis);
@@ -219,6 +227,43 @@ public final class ChaosExperimentRun {
         }
     }
 
+    /**
+     * Why the feed reaches no route of the integration, or null when it does 
(or the status does not tell): the feed is
+     * the id of a route, a pattern, or the endpoint a route starts from, as 
camel cmd send finds its target.
+     */
+    public static String checkFeed(JsonObject status, String feed) {
+        List<Object> routes = status != null ? status.getCollection("routes") 
: null;
+        if (routes == null || routes.isEmpty() || feed.endsWith("*")) {
+            return null;
+        }
+        String endpoint = normalize(feed);
+        StringJoiner inputs = new StringJoiner(", ");
+        for (Object o : routes) {
+            if (!(o instanceof JsonObject route)) {
+                continue;
+            }
+            String id = route.getString("routeId");
+            String from = route.getString("from");
+            if (feed.equals(id)) {
+                return null;
+            }
+            if (from != null) {
+                String input = normalize(from);
+                if (input.equals(endpoint) || input.startsWith(endpoint + 
"?")) {
+                    return null;
+                }
+            }
+            inputs.add(id + " (" + from + ")");
+        }
+        return "The feed " + feed + " is not where a route starts, so no 
message would reach the integration: give the"
+               + " id of a route, or the endpoint it starts from: " + inputs;
+    }
+
+    // kafka://refunds and kafka:refunds are the same endpoint
+    private static String normalize(String uri) {
+        return uri.replace("://", ":");
+    }
+
     /** The experiment with the number in the report of the status, or null. */
     static JsonObject experiment(JsonObject status, int number) {
         JsonObject exploration = exploration(status);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
index 902a97c7d675..0741551c995a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
@@ -57,8 +57,8 @@ class ChaosExploreTest {
         assertThat(ask.timeout).isEqualTo(300);
         assertThat(ask.maxIterations).isEqualTo(100);
         assertThat(ask.yes).isFalse();
-        // the exploration runs with the budget of the user: the AI does not 
get the tool to start another one
-        assertThat(ask.hiddenTools).containsExactly("chaos_explore_start");
+        // the exploration runs with the budget of the user: the AI does not 
get the tools to start another one
+        
assertThat(ask.hiddenTools).containsExactlyInAnyOrder("chaos_explore_start", 
"cli_exec");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
index b799e19b21d7..6cd5633f692c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
@@ -121,6 +121,8 @@ class ChaosExperimentRunTest {
 
     @Test
     void theTrafficThenTheWaitUntilTheVerdict() throws Exception {
+        // read first by the check of the feed
+        integration.shows("PENDING", true);
         integration.shows("PENDING", true);
         integration.shows("PENDING", true);
         integration.shows("FAIL", true);
@@ -194,6 +196,31 @@ class ChaosExperimentRunTest {
         assertThat(integration.sent).isNull();
     }
 
+    @Test
+    void aFeedThatReachesNoRouteIsRefusedBeforeTheExperiment() {
+        JsonObject status = new JsonObject(
+                Map.of("routes", new JsonArray(
+                        List.of(
+                                new JsonObject(Map.of("routeId", "orders", 
"from", "timer://orders?period=1000")),
+                                new JsonObject(
+                                        Map.of("routeId", "refunds", "from", 
"kafka://refunds?brokers=localhost:9092"))))));
+        integration.statuses.add(status);
+
+        // the route id, the endpoint it starts from (with or without //), and 
a pattern reach a route
+        assertThat(ChaosExperimentRun.checkFeed(status, "refunds")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, 
"kafka:refunds")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, 
"kafka://refunds")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, "ref*")).isNull();
+        // an endpoint no route starts from: no message would arrive, so the 
verdict would say nothing
+        assertThatThrownBy(() -> ChaosExperimentRun.run(integration, null, 
RULE,
+                new ChaosExperimentRun.Feed("direct:refunds", "R-1", null, 5, 
null), 60_000, null, clock))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("The feed direct:refunds is not where a route 
starts, so no message would reach the"
+                            + " integration: give the id of a route, or the 
endpoint it starts from:"
+                            + " orders (timer://orders?period=1000), refunds 
(kafka://refunds?brokers=localhost:9092)");
+        assertThat(integration.actions).isEmpty();
+    }
+
     @Test
     void theWait() {
         
assertThat(ChaosExperimentRun.waitMillis(null)).isEqualTo(ChaosExperimentRun.DEFAULT_WAIT);

Reply via email to