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);